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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -1
  3. data/docs/cli.md +4 -4
  4. data/docs/configuration.md +10 -7
  5. data/docs/internals/prompt-caching.md +1 -1
  6. data/docs/internals/web-frontend.md +9 -8
  7. data/docs/internals/working-set.md +2 -2
  8. data/docs/sessions.md +3 -3
  9. data/docs/testing.md +11 -6
  10. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  11. data/lib/samagotchi/version.rb +1 -1
  12. data/lib/samagotchi/web/app.rb +4 -4
  13. data/lib/samagotchi/web/message_display.rb +16 -0
  14. data/lib/samagotchi/web/public/answer_fold.js +98 -29
  15. data/lib/samagotchi/web/public/app.js +17 -40
  16. data/lib/samagotchi/web/public/bubbles.js +27 -7
  17. data/lib/samagotchi/web/public/chip_popover.js +2 -2
  18. data/lib/samagotchi/web/public/history_reveal.js +2 -2
  19. data/lib/samagotchi/web/public/index.html +67 -44
  20. data/lib/samagotchi/web/public/info_bar.js +11 -12
  21. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  22. data/lib/samagotchi/web/public/level_chip.js +33 -95
  23. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  24. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  25. data/lib/samagotchi/web/public/revise_stack.js +9 -3
  26. data/lib/samagotchi/web/public/session_fields.js +4 -4
  27. data/lib/samagotchi/web/public/session_state.js +7 -5
  28. data/lib/samagotchi/web/public/session_store.js +1 -1
  29. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  30. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  31. data/lib/samagotchi/web/public/shell_command.js +26 -0
  32. data/lib/samagotchi/web/public/start_settings.js +25 -88
  33. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  34. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  35. data/lib/samagotchi/web/public/timing.js +13 -3
  36. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  37. data/lib/samagotchi/web/public/turn_block.js +6 -1
  38. data/lib/samagotchi/web/public/turn_events.js +1 -1
  39. data/lib/samagotchi/web/public/turn_html.js +16 -14
  40. data/lib/samagotchi/web/session_create.rb +1 -1
  41. data/lib/samagotchi/web/session_show.rb +6 -3
  42. 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; folded, the bubble is hidden and the row reads its first line and size; unfolded, a slim "▾ N lines" header close above it. */
61
- .bubble.output.folded{display:none}
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
- .answer-fold:not(.folded){margin-bottom:-8px;padding-top:1px;padding-bottom:1px;border-left-color:transparent}
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
- .bubble.user.queued[data-delivery="queue"]{flex-wrap:wrap}
1025
- .bubble.user.queued[data-delivery="queue"] .origin-label,.bubble.user.queued[data-delivery="queue"] .state-badge{order:0}
1026
- .bubble.user.queued[data-delivery="queue"] .user-message{order:1;flex-basis:100%}
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 .meta .id{font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
1044
- #infoBar .meta .copy-attach{padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong)}
1045
- #infoBar .meta .copy-attach:hover{color:var(--text-hover);border-color:var(--border-strong)}
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 LLM context and thinking chips (llm_context_chip.js, thinking_chip.js): model-like chips that open their popovers; accent when the session set its own. */
1092
- #infoBar .meta .llmctx-chip,#infoBar .meta .think-chip,#infoBar .meta .rh-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
1093
- #infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover,#infoBar .meta .rh-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
1094
- #infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own,#infoBar .meta .rh-chip.own{color:var(--accent);border-color:var(--accent-border)}
1095
- .llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
1096
- .llmctx-form .ctx-pop-head strong{font-family:inherit}
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, its copy
2
- // chi --attach button, status, model, chain links, parent and delegates,
3
- // thinking, reasoning history, llm ctx and tools chips, model notes, memories, folder, ctx, speed,
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 llmContextChip } from "./llm_context_chip.js";
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, copied (the copy button just copied), status, model ({text, title,
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), llmContext, thinking and reasoningHistory (the chips'
84
- // summaries; reasoningHistory null on a native host: no chip), toolkit
85
- // (the tools chip's summary, null on full: no chip), notes
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"><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"}><span>${bar.status || ""}</span></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)}${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>`;
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, now, workingSet = undefined, participants = null }) {
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
- llmContext: state.llmContext,
99
- thinking: state.thinking,
100
- reasoningHistory: state.reasoningHistory,
101
- // The tools chip (toolkit_chip.js): null on full.
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, now, workingSet = undefined, participants = null }) {
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, now, workingSet, participants });
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 chip in the session bar and its popover: the level the next turn
2
- // runs at ("think low", "reasoning all"; "· session" when the session set
3
- // its own) and a form with a level select that runs the setting's command
4
- // in the session's worker (the reply shows in the conversation, like a
5
- // typed one). The summary is the setting's SettingExplained#summary
6
- // ({level, source, own}: the worker's snapshot, a command_ran, or the
7
- // server's own reading of the session file).
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 popover's heading ("Thinking")
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 select's options after "follow")
14
- // anchor the chip's selector (its data attribute: each config's
15
- // chipTemplate draws it; clicks are delegated on the same
16
- // element as the other chips')
17
- // formClass, popoverClass, nowClass the form's, popover's and "Now:"
18
- // line's own classes
19
- // when() 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 chip: its text, tooltip and whether the session set its own; null
27
- // without a summary (an older worker, or a host the setting means nothing
28
- // on).
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
- // "Now: low (session)", "Now: default (the model's own)".
47
- export function nowText(summary) {
48
- return `Now: ${summary.level} (${summary.source || "the model's own"})`;
49
- }
50
-
51
- const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
52
-
53
- // The popover's form (a lit-html template); +now+ is the summary its "Now:"
54
- // line reads (refresh passes a newer one, the rest stays as the form opened).
55
- export function formTemplate(cfg, summary, { running = false, now = summary } = {}) {
56
- const chosen = summary.own || DEFAULT;
57
- const follow = summary.own ? "follow the model" : `follow the model (${summary.level})`;
58
- return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
59
- ><span class="ctx-pop-kind">this session</span
60
- ><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
61
- ><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
62
- ><label class="llmctx-field"><span>Level</span><span class="select-wrap"><select name="level">${option(DEFAULT, follow, chosen)}${
63
- cfg.levels.map((level) => option(level, level, chosen))}</select></span></label
64
- ><div class="ctx-pop-meta">${cfg.when()}${running ? " A turn is running: it applies after this turn." : ""}</div
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
  }