samagotchi 0.135.0 → 0.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -1
- data/docs/cli.md +4 -4
- data/docs/configuration.md +10 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +9 -8
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +3 -3
- data/docs/testing.md +11 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -4
- data/lib/samagotchi/web/message_display.rb +16 -0
- data/lib/samagotchi/web/public/answer_fold.js +98 -29
- data/lib/samagotchi/web/public/app.js +17 -40
- data/lib/samagotchi/web/public/bubbles.js +27 -7
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/history_reveal.js +2 -2
- data/lib/samagotchi/web/public/index.html +67 -44
- data/lib/samagotchi/web/public/info_bar.js +11 -12
- data/lib/samagotchi/web/public/info_bar_model.js +8 -10
- data/lib/samagotchi/web/public/level_chip.js +33 -95
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/revise_stack.js +9 -3
- data/lib/samagotchi/web/public/session_fields.js +4 -4
- data/lib/samagotchi/web/public/session_state.js +7 -5
- data/lib/samagotchi/web/public/session_store.js +1 -1
- data/lib/samagotchi/web/public/settings_chip.js +212 -0
- data/lib/samagotchi/web/public/settings_panel.js +121 -0
- data/lib/samagotchi/web/public/shell_command.js +26 -0
- data/lib/samagotchi/web/public/start_settings.js +25 -88
- data/lib/samagotchi/web/public/thinking_chip.js +7 -45
- data/lib/samagotchi/web/public/time_gaps.js +5 -2
- data/lib/samagotchi/web/public/timing.js +13 -3
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/turn_block.js +6 -1
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +16 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- metadata +4 -1
|
@@ -57,14 +57,21 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
57
57
|
/* A generation opens its answer bubble empty: hidden until text streams (the thinking box shows progress meanwhile). */
|
|
58
58
|
.bubble.output.streaming:empty{display:none}
|
|
59
59
|
/* The answer's pop: the promoted bubble is held at the narration box's height (inline max-height, scrolled to its tail) until the rendered markdown lands, then grows to its content while the content fades in; both classes go 400 ms later. */
|
|
60
|
-
/* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, styled as a closed turn's steps
|
|
61
|
-
.bubble.
|
|
60
|
+
/* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, styled as a closed turn's steps, reading the answer's first line and size; folded, the bubble is hidden; unfolded, the row is the bubble's header. */
|
|
61
|
+
/* Folded: the bubble hidden, an answer or a delegate report (#history's: outranks .bubble.user's display). */
|
|
62
|
+
#history>.bubble.folded{display:none}
|
|
62
63
|
.answer-fold{display:flex;align-items:baseline;gap:0;align-self:flex-start;max-width:100%;min-width:0;margin:0;padding:3px 10px;font:inherit;font-size:12px;font-weight:500;line-height:1.4;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:transparent;border:1px solid transparent;border-left:3px solid var(--accent-strong);border-radius:6px;transition:background .12s ease}
|
|
63
64
|
.answer-fold:hover{background:var(--bg-panel)}
|
|
64
65
|
.answer-fold:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
65
66
|
.answer-fold::before{content:"▾\00a0";color:var(--accent);flex:none}
|
|
66
67
|
.answer-fold.folded::before{content:"▸\00a0"}
|
|
67
|
-
|
|
68
|
+
/* Open: the bubble's header, flush on top of it (-12px: #history's gap), its bar running on into the bubble's. */
|
|
69
|
+
.answer-fold:not(.folded){margin-bottom:-12px;border-bottom-left-radius:0}
|
|
70
|
+
/* A delegate report's row: where the report sits (right, the user accent; dashed when merged mid-turn). Open, the row and
|
|
71
|
+
the report share one width, so the row's bar runs on into the report's. */
|
|
72
|
+
.answer-fold.report{align-self:flex-end;max-width:85%;border-left-color:var(--accent)}
|
|
73
|
+
.answer-fold.report.steered{border-left-style:dashed}
|
|
74
|
+
.answer-fold.report:not(.folded),.answer-fold.report:not(.folded)+.bubble.user{width:85%}
|
|
68
75
|
.answer-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);font-size:13px}
|
|
69
76
|
.answer-fold-label:empty{display:none}
|
|
70
77
|
.answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
@@ -103,11 +110,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
103
110
|
.time-gap{align-self:stretch;text-align:center;color:var(--time-gap);font-size:12px;line-height:1.4;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:2px 0;user-select:none}
|
|
104
111
|
.turn-timing{margin-top:8px;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
|
|
105
112
|
.turn-timing.live{color:var(--accent)}
|
|
106
|
-
.bubble.output.markdown{white-space:normal}
|
|
107
|
-
.bubble.output.markdown pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
|
|
108
|
-
.bubble.output.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
109
|
-
.bubble.output.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;color:var(--text)}
|
|
110
|
-
.bubble.output.markdown pre{font-size:13px;line-height:1.45;border:1px solid var(--border)}
|
|
113
|
+
.bubble.output.markdown,.bubble.user>.user-message.markdown{white-space:normal}
|
|
114
|
+
.bubble.output.markdown pre,.bubble.user>.user-message.markdown pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
|
|
115
|
+
.bubble.output.markdown code,.bubble.user>.user-message.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
116
|
+
.bubble.output.markdown :not(pre)>code,.bubble.user>.user-message.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;color:var(--text)}
|
|
117
|
+
.bubble.output.markdown pre,.bubble.user>.user-message.markdown pre{font-size:13px;line-height:1.45;border:1px solid var(--border)}
|
|
111
118
|
/* Code highlighting (markdown_renderer.rb): syntect scope classes, hl-prefixed, coloured from the palette's --code-* tokens. More classes win over fewer (a function name is also "entity"). */
|
|
112
119
|
.syntax-highlighting{color:var(--code-text)}
|
|
113
120
|
.syntax-highlighting .hl-keyword,.syntax-highlighting .hl-storage{color:var(--code-keyword)}
|
|
@@ -120,24 +127,24 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
120
127
|
.syntax-highlighting .hl-markup.hl-inserted{color:var(--diff-add-text)}
|
|
121
128
|
.syntax-highlighting .hl-markup.hl-deleted{color:var(--diff-del-text)}
|
|
122
129
|
.syntax-highlighting .hl-invalid{color:var(--danger-text)}
|
|
123
|
-
.bubble.output.markdown h1,.bubble.output.markdown h2,.bubble.output.markdown h3,.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-weight:600;line-height:1.3;margin:18px 0 8px;color:var(--text-bright)}
|
|
124
|
-
.bubble.output.markdown h1{font-size:19px}
|
|
125
|
-
.bubble.output.markdown h2{font-size:17px}
|
|
126
|
-
.bubble.output.markdown h3{font-size:15.5px}
|
|
127
|
-
.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-size:15px;color:var(--text-2)}
|
|
128
|
-
.bubble.output.markdown h1:first-child,.bubble.output.markdown h2:first-child,.bubble.output.markdown h3:first-child,.bubble.output.markdown>.copy-btn:first-child+:is(h1,h2,h3){margin-top:2px}
|
|
129
|
-
.bubble.output.markdown strong{color:var(--text-bright)}
|
|
130
|
-
.bubble.output.markdown hr{border:0;border-top:1px solid var(--border);margin:14px 0}
|
|
131
|
-
.bubble.output.markdown img{max-width:100%}
|
|
132
|
-
.bubble.output.markdown p,.bubble.output.markdown ul,.bubble.output.markdown ol,.bubble.output.markdown blockquote,.bubble.output.markdown table,.bubble.output.markdown pre{margin:0 0 12px}
|
|
133
|
-
.bubble.output.markdown p:last-child,.bubble.output.markdown ul:last-child,.bubble.output.markdown ol:last-child,.bubble.output.markdown blockquote:last-child,.bubble.output.markdown table:last-child,.bubble.output.markdown pre:last-child{margin-bottom:0}
|
|
134
|
-
.bubble.output.markdown ul,.bubble.output.markdown ol{padding-left:1.5em}
|
|
135
|
-
.bubble.output.markdown li+li{margin-top:4px}
|
|
136
|
-
.bubble.output.markdown li>ul,.bubble.output.markdown li>ol{margin:4px 0 0}
|
|
137
|
-
.bubble.output.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-muted)}
|
|
138
|
-
.bubble.output.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
|
|
139
|
-
.bubble.output.markdown th,.bubble.output.markdown td{border:1px solid var(--border-strong);padding:5px 8px;font-size:14px}
|
|
140
|
-
.bubble.output.markdown th{background:var(--bg-inset);font-weight:600}
|
|
130
|
+
.bubble.output.markdown h1,.bubble.output.markdown h2,.bubble.output.markdown h3,.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6,.bubble.user>.user-message.markdown h1,.bubble.user>.user-message.markdown h2,.bubble.user>.user-message.markdown h3,.bubble.user>.user-message.markdown h4,.bubble.user>.user-message.markdown h5,.bubble.user>.user-message.markdown h6{font-weight:600;line-height:1.3;margin:18px 0 8px;color:var(--text-bright)}
|
|
131
|
+
.bubble.output.markdown h1,.bubble.user>.user-message.markdown h1{font-size:19px}
|
|
132
|
+
.bubble.output.markdown h2,.bubble.user>.user-message.markdown h2{font-size:17px}
|
|
133
|
+
.bubble.output.markdown h3,.bubble.user>.user-message.markdown h3{font-size:15.5px}
|
|
134
|
+
.bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6,.bubble.user>.user-message.markdown h4,.bubble.user>.user-message.markdown h5,.bubble.user>.user-message.markdown h6{font-size:15px;color:var(--text-2)}
|
|
135
|
+
.bubble.output.markdown h1:first-child,.bubble.output.markdown h2:first-child,.bubble.output.markdown h3:first-child,.bubble.output.markdown>.copy-btn:first-child+:is(h1,h2,h3),.bubble.user>.user-message.markdown h1:first-child,.bubble.user>.user-message.markdown h2:first-child,.bubble.user>.user-message.markdown h3:first-child,.bubble.user>.user-message.markdown>.copy-btn:first-child+:is(h1,h2,h3){margin-top:2px}
|
|
136
|
+
.bubble.output.markdown strong,.bubble.user>.user-message.markdown strong{color:var(--text-bright)}
|
|
137
|
+
.bubble.output.markdown hr,.bubble.user>.user-message.markdown hr{border:0;border-top:1px solid var(--border);margin:14px 0}
|
|
138
|
+
.bubble.output.markdown img,.bubble.user>.user-message.markdown img{max-width:100%}
|
|
139
|
+
.bubble.output.markdown p,.bubble.output.markdown ul,.bubble.output.markdown ol,.bubble.output.markdown blockquote,.bubble.output.markdown table,.bubble.output.markdown pre,.bubble.user>.user-message.markdown p,.bubble.user>.user-message.markdown ul,.bubble.user>.user-message.markdown ol,.bubble.user>.user-message.markdown blockquote,.bubble.user>.user-message.markdown table,.bubble.user>.user-message.markdown pre{margin:0 0 12px}
|
|
140
|
+
.bubble.output.markdown p:last-child,.bubble.output.markdown ul:last-child,.bubble.output.markdown ol:last-child,.bubble.output.markdown blockquote:last-child,.bubble.output.markdown table:last-child,.bubble.output.markdown pre:last-child,.bubble.user>.user-message.markdown p:last-child,.bubble.user>.user-message.markdown ul:last-child,.bubble.user>.user-message.markdown ol:last-child,.bubble.user>.user-message.markdown blockquote:last-child,.bubble.user>.user-message.markdown table:last-child,.bubble.user>.user-message.markdown pre:last-child{margin-bottom:0}
|
|
141
|
+
.bubble.output.markdown ul,.bubble.output.markdown ol,.bubble.user>.user-message.markdown ul,.bubble.user>.user-message.markdown ol{padding-left:1.5em}
|
|
142
|
+
.bubble.output.markdown li+li,.bubble.user>.user-message.markdown li+li{margin-top:4px}
|
|
143
|
+
.bubble.output.markdown li>ul,.bubble.output.markdown li>ol,.bubble.user>.user-message.markdown li>ul,.bubble.user>.user-message.markdown li>ol{margin:4px 0 0}
|
|
144
|
+
.bubble.output.markdown blockquote,.bubble.user>.user-message.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-muted)}
|
|
145
|
+
.bubble.output.markdown table,.bubble.user>.user-message.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
|
|
146
|
+
.bubble.output.markdown th,.bubble.output.markdown td,.bubble.user>.user-message.markdown th,.bubble.user>.user-message.markdown td{border:1px solid var(--border-strong);padding:5px 8px;font-size:14px}
|
|
147
|
+
.bubble.output.markdown th,.bubble.user>.user-message.markdown th{background:var(--bg-inset);font-weight:600}
|
|
141
148
|
/* Plugins' slow setup (chi.init, init_row.js): a quiet line per task over the composer. */
|
|
142
149
|
#initRow{display:flex;flex-direction:column;gap:2px;padding:6px 12px 2px;font-size:12px;line-height:1.4;color:var(--text-3)}
|
|
143
150
|
#initRow[hidden]{display:none}
|
|
@@ -187,6 +194,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
187
194
|
.review-apply-note{border-left:0;padding-left:0;text-align:center}
|
|
188
195
|
}
|
|
189
196
|
.bubble.user .user-message{flex:1;min-width:0}
|
|
197
|
+
/* A delegate report in the history (long, markdown): its label and badge on a row of their own above it at any width,
|
|
198
|
+
not centred beside a tall body. */
|
|
199
|
+
#history>.bubble.user[data-source="delegate_report"]{flex-wrap:wrap}
|
|
200
|
+
#history>.bubble.user[data-source="delegate_report"] :is(.origin-label,.state-badge){order:0}
|
|
201
|
+
#history>.bubble.user[data-source="delegate_report"] .user-message{order:1;flex-basis:100%}
|
|
190
202
|
.bubble.user blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-3);margin:2px 0 6px}
|
|
191
203
|
.bubble.user blockquote:last-child{margin-bottom:0}
|
|
192
204
|
.bubble.tool{border-left:3px solid var(--ok);background:var(--bg-panel);font-size:13px;color:var(--text-2)}
|
|
@@ -194,7 +206,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
194
206
|
/* A stop someone chose (a cancel, the session's stop): the ■ STOPPED rows' colours, not an error's. */
|
|
195
207
|
.bubble.cancel.stopped{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,var(--bg-panel));color:var(--warn-text)}
|
|
196
208
|
.bubble.recap{border-left:3px solid var(--think-text);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));font-size:13px}
|
|
197
|
-
.bubble.recap summary{cursor:pointer;color:var(--think-text);font-size:12px}
|
|
209
|
+
.bubble.recap summary{cursor:pointer;color:var(--think-text);font-size:12px;list-style:none}
|
|
210
|
+
/* The fold glyph every collapsible in the history draws (steps, thinking, the answer fold), not the native triangle. */
|
|
211
|
+
.bubble.recap summary::-webkit-details-marker{display:none}
|
|
212
|
+
.bubble.recap summary::before{content:"▸ ";color:var(--think-text)}
|
|
213
|
+
.bubble.recap[open] summary::before{content:"▾ "}
|
|
198
214
|
.bubble.recap .recap-body{white-space:pre-wrap;margin-top:6px}
|
|
199
215
|
.bubble.recap.stale{opacity:.7;border-left-style:dashed}
|
|
200
216
|
.activity-body{margin-top:6px}
|
|
@@ -602,7 +618,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
|
|
|
602
618
|
.bubble.user>.copy-btn:hover{background:var(--bg-raised);border-color:var(--border-strong)}
|
|
603
619
|
.code-wrap{position:relative;margin:0 0 12px}
|
|
604
620
|
.code-wrap:last-child{margin-bottom:0}
|
|
605
|
-
.bubble.output.markdown .code-wrap>pre{margin:0}
|
|
621
|
+
.bubble.output.markdown .code-wrap>pre,.bubble.user>.user-message.markdown .code-wrap>pre{margin:0}
|
|
606
622
|
.code-wrap>.copy-btn{top:5px;right:5px}
|
|
607
623
|
@media (hover:none){.copy-btn{opacity:.55}}
|
|
608
624
|
/* The dock is the composer card (composer.css .composer-card): the text field, a toolbar row, the info bar as its footer. */
|
|
@@ -1021,9 +1037,12 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
|
1021
1037
|
@media(max-width:600px){body.working-set .ts-live{padding-right:0;--ws-row-h:0px}.ts-ws{display:none}}
|
|
1022
1038
|
@media (prefers-reduced-motion:reduce){.ws-item,.ws-more,.ws-ring .fill{transition:none}.ws-item.enter,.ws-tick.fresh{animation:none}}
|
|
1023
1039
|
@media(max-width:600px){
|
|
1024
|
-
.
|
|
1025
|
-
|
|
1026
|
-
|
|
1040
|
+
/* A line that wraps: its label and badge on one row, its text full width under them. A queued line, and in the
|
|
1041
|
+
history a labelled one (a delegate report, tui, a participant's), which beside its label read a word a line.
|
|
1042
|
+
Not the stage's prompt or extras: one line there. */
|
|
1043
|
+
:is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)){flex-wrap:wrap}
|
|
1044
|
+
:is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)) :is(.origin-label,.state-badge){order:0}
|
|
1045
|
+
:is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)) .user-message{order:1;flex-basis:100%}
|
|
1027
1046
|
.ts-status{padding-left:10px}
|
|
1028
1047
|
.ts-scroll{padding:8px 10px 10px}
|
|
1029
1048
|
.ts-foot{padding:0 10px 10px}
|
|
@@ -1040,9 +1059,9 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
|
1040
1059
|
meta keeps the width. */
|
|
1041
1060
|
#infoBar .info{flex:1 1 640px;min-width:0;font-size:13px;line-height:1.4;word-break:break-word;display:flex;flex-direction:column;gap:2px}
|
|
1042
1061
|
#infoBar .meta{font-size:11px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
|
1043
|
-
#infoBar .
|
|
1044
|
-
#infoBar .meta .
|
|
1045
|
-
#infoBar .meta .
|
|
1062
|
+
/* The id is a button (a click copies the full id, app.js): its own padding and no button look, the #infoBar button rule aside. */
|
|
1063
|
+
#infoBar .meta .id{padding:2px 6px;border-radius:6px;border:1px solid transparent;background:transparent;cursor:pointer;font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
|
|
1064
|
+
#infoBar .meta .id:hover:not(:disabled){filter:none;border-color:var(--accent-border);background:var(--accent-bg)}
|
|
1046
1065
|
#infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
|
|
1047
1066
|
/* The status word takes the widest word's room (running, stopped; idle and error are narrower), so a turn's idle ↔ running doesn't move the rest of the line. */
|
|
1048
1067
|
#infoBar .meta .status{display:inline-grid;justify-items:center}
|
|
@@ -1088,12 +1107,16 @@ body.has-session.composer-folded #initRow{display:none}
|
|
|
1088
1107
|
body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
1089
1108
|
#infoBar #infoCancelBtn{background:var(--bg-raised);color:var(--danger-text);border:1px solid color-mix(in srgb,var(--danger) 55%,transparent)}
|
|
1090
1109
|
#infoBar #infoCancelBtn:hover:not(:disabled){color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
1091
|
-
/* The
|
|
1092
|
-
#infoBar .meta .
|
|
1093
|
-
#infoBar .meta .
|
|
1094
|
-
#infoBar .meta .
|
|
1095
|
-
.
|
|
1096
|
-
.
|
|
1110
|
+
/* The ⚙ chip (settings_chip.js): a model-like chip that opens the session's settings panel; accent when the session set its own (capped like the model chip in the min-width:601px rule above, the full text in its tooltip; a phone shows ⚙ with a dot). */
|
|
1111
|
+
#infoBar .meta .settings-chip{position:relative;padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
1112
|
+
#infoBar .meta .settings-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
1113
|
+
#infoBar .meta .settings-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
1114
|
+
/* A chip's icon (⚙ here, the start page's own SVG from composer.css): the glyph stays the text, the mask draws it. */
|
|
1115
|
+
#infoBar .meta :is(.settings-chip,.tools-chip){display:inline-flex;align-items:center;gap:4px}
|
|
1116
|
+
#infoBar .meta .settings-chip .sc-own{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
1117
|
+
#infoBar .meta .chip-icon{flex:none;width:12px;height:12px;font-size:0;background:currentColor;-webkit-mask:var(--icon) center/contain no-repeat;mask:var(--icon) center/contain no-repeat}
|
|
1118
|
+
#infoBar .meta .sc-gear{--icon:var(--icon-settings)}
|
|
1119
|
+
#infoBar .meta .tc-kit{--icon:var(--icon-toolkit)}
|
|
1097
1120
|
.llmctx-field{display:flex;align-items:center;gap:8px}
|
|
1098
1121
|
.llmctx-field>span{flex:0 0 58px;font-size:12px;color:var(--text-muted)}
|
|
1099
1122
|
.llmctx-field select,.llmctx-field input{flex:1;min-width:0;box-sizing:border-box;padding:5px 8px;border-radius:7px;border:1px solid var(--border-strong);background:var(--bg-inset);color:var(--text);font:inherit;font-size:13px;line-height:1.4}
|
|
@@ -1101,8 +1124,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1101
1124
|
/* A select's closed box and caret: forms.css (.select-wrap); here the wrapper takes the field's width. */
|
|
1102
1125
|
.llmctx-field>.select-wrap{flex:1}
|
|
1103
1126
|
.llmctx-field input::placeholder{color:var(--text-faint);opacity:1}
|
|
1104
|
-
.llmctx-form .llmctx-set{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border)}
|
|
1105
|
-
.llmctx-form .llmctx-set:disabled{opacity:.6;cursor:default}
|
|
1106
1127
|
/* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
|
|
1107
1128
|
#contextChips{order:5;flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
|
|
1108
1129
|
#contextChips[hidden]{display:none}
|
|
@@ -1297,13 +1318,15 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1297
1318
|
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
|
|
1298
1319
|
@media(max-width:600px){
|
|
1299
1320
|
#infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
|
|
1300
|
-
/* No terminal on a phone to attach from. */
|
|
1301
|
-
#infoBar .meta .copy-attach{display:none}
|
|
1302
1321
|
#infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
|
|
1303
1322
|
#infoBar .meta::-webkit-scrollbar{display:none}
|
|
1304
1323
|
#infoBar .meta>*{flex-shrink:0}
|
|
1305
1324
|
#infoBar .meta .id{order:-2}
|
|
1306
1325
|
#infoBar .meta .status{order:-1}
|
|
1326
|
+
/* ⚙ alone, the start page's dot when the session set its own (its values in the tooltip and accessible name). */
|
|
1327
|
+
#infoBar .meta .settings-chip .sc-own{display:none}
|
|
1328
|
+
#infoBar .meta .settings-chip.own{padding-right:10px}
|
|
1329
|
+
#infoBar .meta .settings-chip.own::after{content:"";position:absolute;top:3px;right:3px;width:5px;height:5px;border-radius:50%;background:var(--accent)}
|
|
1307
1330
|
/* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
1308
1331
|
#infoBar .meta .mem-chip,#infoBar .meta .ws-chip{order:-1}
|
|
1309
1332
|
/* A parent's children chip too: its waiting part is what to see. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// The session bar's info line (#infoText): the open session's id
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The session bar's info line (#infoText): the open session's id (a button
|
|
2
|
+
// that copies the full id), status, model, the ⚙ settings chip (thinking,
|
|
3
|
+
// reasoning history, LLM context), the 🧰 toolkit chip, chain links, parent
|
|
4
|
+
// and delegates, model notes, memories, folder, ctx, speed,
|
|
4
5
|
// session clock and the worker's version badge. A lit-html template over a
|
|
5
6
|
// plain model (info_bar_model.js, applied by app.js's updateInfoBar), so the markup lives here and
|
|
6
7
|
// runs under node --test (spec/web/public/info_bar.test.js).
|
|
@@ -13,9 +14,7 @@
|
|
|
13
14
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
14
15
|
import { memChipText, memChipTitle } from "./format.js";
|
|
15
16
|
import { ALL_SESSIONS_HASH, sessionHash } from "./route.js";
|
|
16
|
-
import { chipTemplate as
|
|
17
|
-
import { chipTemplate as thinkingChip } from "./thinking_chip.js";
|
|
18
|
-
import { chipTemplate as reasoningHistoryChip } from "./reasoning_history_chip.js";
|
|
17
|
+
import { chipTemplate as settingsChip } from "./settings_chip.js";
|
|
19
18
|
import { chipTemplate as toolkitChip } from "./toolkit_chip.js";
|
|
20
19
|
import { chipTemplate as participantsChip } from "./participants_panel.js";
|
|
21
20
|
|
|
@@ -77,12 +76,13 @@ export function workingSetChipTemplate(chip) {
|
|
|
77
76
|
|
|
78
77
|
// The info line for +bar+, nothing without a session (the start page hides
|
|
79
78
|
// the bar). +bar+:
|
|
80
|
-
// id,
|
|
79
|
+
// id, status, model ({text, title,
|
|
81
80
|
// mismatch}: format.js modelLabel), chain ({prev, next}: chainLinkTemplate's
|
|
82
81
|
// links or null), parent ({id, title} or null), kids (format.js
|
|
83
|
-
// childrenSummary),
|
|
84
|
-
// summaries; reasoningHistory null on a
|
|
85
|
-
//
|
|
82
|
+
// childrenSummary), settings ({thinking, reasoningHistory, llmContext}:
|
|
83
|
+
// the ⚙ chip's summaries, settings_chip.js; reasoningHistory null on a
|
|
84
|
+
// native host: no row), toolkit ({summary, defaultKit}: the 🧰
|
|
85
|
+
// chip's, toolkit_chip.js; null: none), notes
|
|
86
86
|
// ({text, title} or null), memories (the used names), workingSet
|
|
87
87
|
// (web.working_set on: {chip}, the working set's chip in place of the
|
|
88
88
|
// mem chip; null: off), dir, ctx ({text,
|
|
@@ -92,9 +92,8 @@ export function workingSetChipTemplate(chip) {
|
|
|
92
92
|
// participants chip: participants_panel.js chipModel, or null).
|
|
93
93
|
export function infoBarHtml(bar) {
|
|
94
94
|
if (!bar) return nothing;
|
|
95
|
-
const attach = `chi --attach ${bar.id}`;
|
|
96
95
|
const memories = bar.memories || [];
|
|
97
|
-
return html`<div class="meta"><
|
|
96
|
+
return html`<div class="meta"><button type="button" class="id" title=${`Session id ${bar.id}: click to copy it`}>${String(bar.id).slice(0, 8)}</button><span class=${bar.status ? `status ${bar.status}` : "status"}><span>${bar.status || ""}</span></span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${settingsChip(bar.settings)}${toolkitChip(bar.toolkit)}${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)}${participantsChip(bar.participants)}${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${bar.speed.empty ? " empty" : ""}`} title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}<span class=${`model session-time${bar.clock ? "" : " empty"}`} title=${bar.clock ? "This session's time: its turns, and the running one" : "This session's time: no turn yet"}>${bar.clock || CLOCK_NONE}</span>${workerBadgeTemplate(bar.worker)}</div>`;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip
|
|
@@ -65,12 +65,11 @@ export function infoButtons({ id, state, chain, archived, continuing, streaming
|
|
|
65
65
|
// @param badge workerBadgeFor's, or null
|
|
66
66
|
// @param restarting whether this session's Restart is under way
|
|
67
67
|
// @param running whether its turn runs (the session clock ticks)
|
|
68
|
-
// @param now the clock (ms)
|
|
69
68
|
// @param workingSet web.working_set on: the chip's {n, changed} (n 0 while
|
|
70
69
|
// the set is empty); undefined: off (the mem chip shows)
|
|
71
70
|
// @param participants the participants chip (participants_panel.js
|
|
72
71
|
// chipModel), null before the list is read
|
|
73
|
-
export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting, running,
|
|
72
|
+
export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting, running, workingSet = undefined, participants = null }) {
|
|
74
73
|
const tokens = state.timing.tokens;
|
|
75
74
|
const ctxTip = ctxBarTip(tokens, state.timing.memoryIndex,
|
|
76
75
|
{ tokens: state.ctxWindow, source: state.ctxWindowSource, pct: state.ctxPct,
|
|
@@ -86,7 +85,6 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
86
85
|
const parentChip = delegatedBy(state.parentId, sessions.find((x) => x.id === state.parentId) || null);
|
|
87
86
|
return {
|
|
88
87
|
id,
|
|
89
|
-
copied: now - state.attachCopiedAt < 2000,
|
|
90
88
|
status: state.status,
|
|
91
89
|
model: modelLabel(state.model, ...(state.served || [])),
|
|
92
90
|
// A link of a session chain: ← the previous link, → the next one (each
|
|
@@ -95,11 +93,11 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
95
93
|
parent: parentChip ? { id: state.parentId, title: parentChip.title } : null,
|
|
96
94
|
// A parent names its delegates; the link opens all sessions.
|
|
97
95
|
kids: childrenSummary({ id }, sessions),
|
|
98
|
-
|
|
99
|
-
thinking: state.thinking,
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
toolkit: state.toolkit,
|
|
96
|
+
// The ⚙ chip (settings_chip.js): the three settings' summaries.
|
|
97
|
+
settings: { thinking: state.thinking, reasoningHistory: state.reasoningHistory, llmContext: state.llmContext },
|
|
98
|
+
// The 🧰 chip (toolkit_chip.js): the session's kit (null on full) and
|
|
99
|
+
// config's default.
|
|
100
|
+
toolkit: { summary: state.toolkit, defaultKit: state.toolkitDefault },
|
|
103
101
|
// The model notes the session's prompt carried.
|
|
104
102
|
notes: promptNotesChip(state.promptNotes),
|
|
105
103
|
memories: state.usedMemories,
|
|
@@ -122,12 +120,12 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
122
120
|
// The whole bar for the open session +id+ (null: the start page, no line).
|
|
123
121
|
// @param sessions the page's list; +continuing+, +restarting+: Sets of ids
|
|
124
122
|
// @return {buttons, bar}
|
|
125
|
-
export function sessionBar({ id, state, sessions, versions, cardTitle, continuing, restarting, streaming, running,
|
|
123
|
+
export function sessionBar({ id, state, sessions, versions, cardTitle, continuing, restarting, streaming, running, workingSet = undefined, participants = null }) {
|
|
126
124
|
const chain = id ? chainNeighbours(sessions, id, { continues: state.continues }) : { prev: null, next: null };
|
|
127
125
|
const archived = !!sessions.find((s) => s.id === id)?.archived;
|
|
128
126
|
const buttons = infoButtons({ id, state, chain, archived, continuing: continuing.has(id), streaming });
|
|
129
127
|
if (!id) return { buttons, bar: null };
|
|
130
128
|
const badge = workerBadgeFor(id, state, versions);
|
|
131
|
-
const bar = infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting: restarting.has(id), running,
|
|
129
|
+
const bar = infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting: restarting.has(id), running, workingSet, participants });
|
|
132
130
|
return { buttons, bar };
|
|
133
131
|
}
|
|
@@ -1,107 +1,45 @@
|
|
|
1
|
-
// A level
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// A level setting (thinking, reasoning history) as the settings panels draw
|
|
2
|
+
// it: a row of segments (settings_panel.js; the start page's ⚙ panel and the
|
|
3
|
+
// session bar's, settings_chip.js) and the command a session's pick runs in
|
|
4
|
+
// its worker (the reply shows in the conversation, like a typed one). The
|
|
5
|
+
// summary is the setting's SettingExplained#summary ({level, source, own}:
|
|
6
|
+
// the worker's snapshot, a command_ran, or the server's own reading of the
|
|
7
|
+
// session file; the start page's from GET /api/models, own nil).
|
|
8
8
|
//
|
|
9
9
|
// A config names the setting (thinking_chip.js, reasoning_history_chip.js):
|
|
10
|
-
// name the
|
|
11
|
-
// prefix the chip's word ("think")
|
|
10
|
+
// name the row's label and the tooltip's word ("Thinking")
|
|
11
|
+
// prefix the session chip's word for an own value ("think low")
|
|
12
12
|
// command the command ("/thinking")
|
|
13
|
-
// levels the settable levels (the
|
|
14
|
-
//
|
|
15
|
-
// chipTemplate draws it; clicks are delegated on the same
|
|
16
|
-
// element as the other chips')
|
|
17
|
-
// formClass, popoverClass, nowClass the form's, popover's and "Now:"
|
|
18
|
-
// line's own classes
|
|
19
|
-
// when() the form's note on when it applies
|
|
20
|
-
|
|
21
|
-
import { createChipPopover } from "./chip_popover.js";
|
|
22
|
-
import { html, render } from "./vendor/lit-html.js";
|
|
13
|
+
// levels the settable levels (the segments after "default")
|
|
14
|
+
// cost the panel's note on what a change costs the prompt cache
|
|
23
15
|
|
|
24
16
|
export const DEFAULT = "default";
|
|
25
17
|
|
|
26
|
-
// The
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
export function chipModel(cfg, summary) {
|
|
30
|
-
if (!summary || !summary.level) return null;
|
|
31
|
-
const own = !!summary.own;
|
|
32
|
-
const from = own ? "this session" : summary.source || "the model's own";
|
|
33
|
-
const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for this session.`;
|
|
34
|
-
return { text: `${cfg.prefix} ${summary.level}${own ? " · session" : ""}`, title, own };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// The command line for a select value ("default" unsets the session's own),
|
|
38
|
-
// with --yes: picking a level in the form is the choice, and the form
|
|
39
|
-
// names the cost (CacheConfirm asks nothing then); null when it is what
|
|
40
|
-
// the form opened with.
|
|
18
|
+
// The command line for a level ("default" unsets the session's own), with
|
|
19
|
+
// --yes: the panel names the cost (CacheConfirm asks nothing then); null
|
|
20
|
+
// when it is the level the session has (+initial+: its own, or default).
|
|
41
21
|
export function commandLine(cfg, level, initial = null) {
|
|
42
22
|
if (initial !== null && level === initial) return null;
|
|
43
23
|
return `${cfg.command} ${level} --yes`;
|
|
44
24
|
}
|
|
45
25
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// The chip's popover (chip_popover.js' shell), as createLlmContextControl's:
|
|
69
|
-
// el is where the chip is drawn (clicks are delegated: #infoText,
|
|
70
|
-
// info_bar.js); summary(): the open session's summary; session(): its id;
|
|
71
|
-
// running(): a turn runs; run(line): sends the command. id and anchor (the
|
|
72
|
-
// chip's selector, by default the config's data attribute) keep it apart
|
|
73
|
-
// from the other controls on the same element.
|
|
74
|
-
export function createLevelControl({ cfg, el, summary, session = () => null, running = () => false, run,
|
|
75
|
-
id, anchor = cfg.anchor,
|
|
76
|
-
doc = document, win = window }) {
|
|
77
|
-
let initial = null;
|
|
78
|
-
// What the open form was drawn from: the session's summary and the form's
|
|
79
|
-
// options.
|
|
80
|
-
let shown = null;
|
|
81
|
-
const draw = (now) => render(formTemplate(cfg, shown.data, { ...shown.opts, now }), shell.pop);
|
|
82
|
-
|
|
83
|
-
// A refresh (a newer summary): an open form keeps the user's pick; only
|
|
84
|
-
// "Now:" follows (the same template again: lit writes that line alone).
|
|
85
|
-
function show(data, opening) {
|
|
86
|
-
if (opening) {
|
|
87
|
-
initial = data.own || DEFAULT;
|
|
88
|
-
shown = { data, opts: { running: running() } };
|
|
89
|
-
}
|
|
90
|
-
draw(data);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function submit(form) {
|
|
94
|
-
const level = form.elements.level.value;
|
|
95
|
-
const was = initial;
|
|
96
|
-
shell.close();
|
|
97
|
-
const line = commandLine(cfg, level, was);
|
|
98
|
-
if (line) run(line);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const shell = createChipPopover({
|
|
102
|
-
id, className: `ctx-popover ${cfg.popoverClass}`, width: 320, el, anchor, summary, session, show,
|
|
103
|
-
onClose: () => { initial = null; shown = null; }, onSubmit: submit, doc, win,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return { close: shell.close, refresh: shell.refresh, get open() { return shell.isOpen(); } };
|
|
26
|
+
// A settings panel row's first segment (settings_panel.js): follow the
|
|
27
|
+
// model, its value in brackets ("default (high)"); "default" alone for a
|
|
28
|
+
// model with no value of its own, or when +own+ (the value in force is the
|
|
29
|
+
// session's, not the model's: the summary doesn't carry the model's then).
|
|
30
|
+
export const followSegment = (value, own = false) => {
|
|
31
|
+
const plain = own || value === DEFAULT;
|
|
32
|
+
return { value: DEFAULT, label: plain ? DEFAULT : `${DEFAULT} (${value})`,
|
|
33
|
+
title: plain ? "Follow the model" : `Follow the model (${value})` };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// A config's row of segments in the settings panel: the model's own level
|
|
37
|
+
// first (followSegment; plain while the session has its own), then the
|
|
38
|
+
// config's levels; the pressed one is +choice+ (null: the follow segment).
|
|
39
|
+
// null without the summary's level (the row is hidden).
|
|
40
|
+
export function segments(cfg, summary, choice) {
|
|
41
|
+
if (!summary?.level) return null;
|
|
42
|
+
const pressed = choice || DEFAULT;
|
|
43
|
+
return [followSegment(summary.level, !!summary.own),
|
|
44
|
+
...cfg.levels.map((level) => ({ value: level, label: level }))].map((s) => ({ ...s, pressed: s.value === pressed }));
|
|
107
45
|
}
|