samagotchi 0.135.0 → 0.136.1

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/docs/cli.md +7 -6
  4. data/docs/configuration.md +10 -7
  5. data/docs/internals/prompt-caching.md +1 -1
  6. data/docs/internals/web-frontend.md +10 -9
  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 +110 -30
  15. data/lib/samagotchi/web/public/app.js +22 -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/details_motion.js +74 -0
  19. data/lib/samagotchi/web/public/history_reveal.js +2 -2
  20. data/lib/samagotchi/web/public/index.html +75 -54
  21. data/lib/samagotchi/web/public/info_bar.js +11 -12
  22. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  23. data/lib/samagotchi/web/public/level_chip.js +33 -95
  24. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  25. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  26. data/lib/samagotchi/web/public/revise_stack.js +9 -3
  27. data/lib/samagotchi/web/public/session_fields.js +4 -4
  28. data/lib/samagotchi/web/public/session_state.js +7 -5
  29. data/lib/samagotchi/web/public/session_store.js +1 -1
  30. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  31. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  32. data/lib/samagotchi/web/public/shell_command.js +26 -0
  33. data/lib/samagotchi/web/public/start_settings.js +25 -88
  34. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  35. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  36. data/lib/samagotchi/web/public/timing.js +13 -3
  37. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  38. data/lib/samagotchi/web/public/turn_block.js +6 -1
  39. data/lib/samagotchi/web/public/turn_events.js +1 -1
  40. data/lib/samagotchi/web/public/turn_html.js +16 -14
  41. data/lib/samagotchi/web/session_create.rb +1 -1
  42. data/lib/samagotchi/web/session_show.rb +6 -3
  43. metadata +5 -1
@@ -57,17 +57,25 @@ 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}
62
- .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
- .answer-fold:hover{background:var(--bg-panel)}
60
+ /* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, in the bubble's own shape. Folded, the row is the answer's box closed ("▸ first line · N lines") and the bubble is hidden; unfolded, the row is that box's top edge ("▾ N lines") and the bubble its rest, the two one block. The shape never changes; a click changes the height (details_motion.js). */
61
+ /* Folded: the bubble hidden, an answer or a delegate report (#history's: outranks .bubble.user's display). */
62
+ #history>.bubble.folded{display:none}
63
+ .answer-fold{display:flex;align-items:baseline;gap:0;align-self:stretch;max-width:100%;min-width:0;margin:0;padding:12px 14px;font:inherit;font-size:13px;font-weight:600;line-height:1.5;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent-strong);border-radius:12px}
64
+ .answer-fold:hover .answer-fold-size{color:var(--text-2)}
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 box's top edge (-12px: #history's gap), the bubble under it its rest. */
69
+ .answer-fold:not(.folded){margin-bottom:-12px;padding-bottom:0;border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}
70
+ .answer-fold:not(.folded)+.bubble{border-top:0;border-top-left-radius:0;border-top-right-radius:0;padding-top:8px}
71
+ /* A delegate report's row: the report's box (right, its tint, the user accent; dashed when merged mid-turn), closed or
72
+ its top edge. */
73
+ .answer-fold.report{align-self:flex-end;width:85%;background:var(--accent-bg);border-left-color:var(--accent)}
74
+ .answer-fold.report.steered{border-left-style:dashed}
75
+ .answer-fold.report:not(.folded)+.bubble.user{width:85%}
68
76
  .answer-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);font-size:13px}
69
77
  .answer-fold-label:empty{display:none}
70
- .answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums}
78
+ .answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums;transition:color .12s ease}
71
79
  /* Revise stacks (revise_stack.js): a superseded version folds to its stub; the Revise row under a final answer
72
80
  (zero height: its button sits on the bubble's bottom edge, clear of the copy button, shown on hover) and a stack
73
81
  top's version chip. */
@@ -103,11 +111,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
103
111
  .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
112
  .turn-timing{margin-top:8px;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
105
113
  .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)}
114
+ .bubble.output.markdown,.bubble.user>.user-message.markdown{white-space:normal}
115
+ .bubble.output.markdown pre,.bubble.user>.user-message.markdown pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
116
+ .bubble.output.markdown code,.bubble.user>.user-message.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
117
+ .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)}
118
+ .bubble.output.markdown pre,.bubble.user>.user-message.markdown pre{font-size:13px;line-height:1.45;border:1px solid var(--border)}
111
119
  /* 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
120
  .syntax-highlighting{color:var(--code-text)}
113
121
  .syntax-highlighting .hl-keyword,.syntax-highlighting .hl-storage{color:var(--code-keyword)}
@@ -120,24 +128,24 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
120
128
  .syntax-highlighting .hl-markup.hl-inserted{color:var(--diff-add-text)}
121
129
  .syntax-highlighting .hl-markup.hl-deleted{color:var(--diff-del-text)}
122
130
  .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}
131
+ .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)}
132
+ .bubble.output.markdown h1,.bubble.user>.user-message.markdown h1{font-size:19px}
133
+ .bubble.output.markdown h2,.bubble.user>.user-message.markdown h2{font-size:17px}
134
+ .bubble.output.markdown h3,.bubble.user>.user-message.markdown h3{font-size:15.5px}
135
+ .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)}
136
+ .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}
137
+ .bubble.output.markdown strong,.bubble.user>.user-message.markdown strong{color:var(--text-bright)}
138
+ .bubble.output.markdown hr,.bubble.user>.user-message.markdown hr{border:0;border-top:1px solid var(--border);margin:14px 0}
139
+ .bubble.output.markdown img,.bubble.user>.user-message.markdown img{max-width:100%}
140
+ .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}
141
+ .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}
142
+ .bubble.output.markdown ul,.bubble.output.markdown ol,.bubble.user>.user-message.markdown ul,.bubble.user>.user-message.markdown ol{padding-left:1.5em}
143
+ .bubble.output.markdown li+li,.bubble.user>.user-message.markdown li+li{margin-top:4px}
144
+ .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}
145
+ .bubble.output.markdown blockquote,.bubble.user>.user-message.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-muted)}
146
+ .bubble.output.markdown table,.bubble.user>.user-message.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
147
+ .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}
148
+ .bubble.output.markdown th,.bubble.user>.user-message.markdown th{background:var(--bg-inset);font-weight:600}
141
149
  /* Plugins' slow setup (chi.init, init_row.js): a quiet line per task over the composer. */
142
150
  #initRow{display:flex;flex-direction:column;gap:2px;padding:6px 12px 2px;font-size:12px;line-height:1.4;color:var(--text-3)}
143
151
  #initRow[hidden]{display:none}
@@ -187,6 +195,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
187
195
  .review-apply-note{border-left:0;padding-left:0;text-align:center}
188
196
  }
189
197
  .bubble.user .user-message{flex:1;min-width:0}
198
+ /* A delegate report in the history (long, markdown): its label and badge on a row of their own above it at any width,
199
+ not centred beside a tall body. */
200
+ #history>.bubble.user[data-source="delegate_report"]{flex-wrap:wrap}
201
+ #history>.bubble.user[data-source="delegate_report"] :is(.origin-label,.state-badge){order:0}
202
+ #history>.bubble.user[data-source="delegate_report"] .user-message{order:1;flex-basis:100%}
190
203
  .bubble.user blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-3);margin:2px 0 6px}
191
204
  .bubble.user blockquote:last-child{margin-bottom:0}
192
205
  .bubble.tool{border-left:3px solid var(--ok);background:var(--bg-panel);font-size:13px;color:var(--text-2)}
@@ -194,7 +207,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
194
207
  /* A stop someone chose (a cancel, the session's stop): the ■ STOPPED rows' colours, not an error's. */
195
208
  .bubble.cancel.stopped{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,var(--bg-panel));color:var(--warn-text)}
196
209
  .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}
210
+ .bubble.recap summary{cursor:pointer;color:var(--think-text);font-size:12px;list-style:none}
211
+ /* The fold glyph every collapsible in the history draws (steps, thinking, the answer fold), not the native triangle. */
212
+ .bubble.recap summary::-webkit-details-marker{display:none}
213
+ .bubble.recap summary::before{content:"▸ ";color:var(--think-text)}
214
+ .bubble.recap[open] summary::before{content:"▾ "}
198
215
  .bubble.recap .recap-body{white-space:pre-wrap;margin-top:6px}
199
216
  .bubble.recap.stale{opacity:.7;border-left-style:dashed}
200
217
  .activity-body{margin-top:6px}
@@ -304,11 +321,10 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
304
321
  .activity-row>.activity-command{margin-left:0}
305
322
  }
306
323
  .bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
307
- .bubble.thinking:not([open]),.bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
308
- .bubble.thinking:not([open]){border-left-color:var(--think)}
324
+ .bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
309
325
  .bubble.activity:not([open]){border-left-color:var(--accent)}
310
- .bubble.thinking:not([open]):hover,.bubble.activity:not([open]):hover{background:var(--bg-panel)}
311
- .bubble.thinking:not([open])>summary,.bubble.activity:not([open])>summary{font-size:12px;font-weight:500}
326
+ .bubble.activity:not([open]):hover{background:var(--bg-panel)}
327
+ .bubble.activity:not([open])>summary{font-size:12px;font-weight:500}
312
328
  .bubble.thinking>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;font-style:italic;user-select:none;list-style:none}
313
329
  .bubble.thinking>summary::-webkit-details-marker{display:none}
314
330
  .bubble.thinking>summary::before{content:"▸ ";color:var(--think)}
@@ -320,9 +336,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
320
336
  .turn-work>summary::-webkit-details-marker{display:none}
321
337
  .turn-work>summary::before{content:"▸ ";color:var(--accent)}
322
338
  .turn-work[open]>summary::before{content:"▾ "}
323
- .turn-work:not([open]){padding:3px 10px;background:transparent;border-color:transparent;border-left-color:var(--accent);align-self:flex-start;border-radius:6px;transition:background .12s ease}
324
- .turn-work:not([open]):hover{background:var(--bg-panel)}
325
- .turn-work:not([open])>summary{font-size:12px;font-weight:500}
339
+ /* Closed, a block keeps its box (the bar, the border, the width): a click changes its height only (details_motion.js); thinking and the recap the same. */
326
340
  /* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
327
341
  chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
328
342
  negative margin: a child's own margin shorthand can't undo the padding. */
@@ -602,7 +616,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
602
616
  .bubble.user>.copy-btn:hover{background:var(--bg-raised);border-color:var(--border-strong)}
603
617
  .code-wrap{position:relative;margin:0 0 12px}
604
618
  .code-wrap:last-child{margin-bottom:0}
605
- .bubble.output.markdown .code-wrap>pre{margin:0}
619
+ .bubble.output.markdown .code-wrap>pre,.bubble.user>.user-message.markdown .code-wrap>pre{margin:0}
606
620
  .code-wrap>.copy-btn{top:5px;right:5px}
607
621
  @media (hover:none){.copy-btn{opacity:.55}}
608
622
  /* The dock is the composer card (composer.css .composer-card): the text field, a toolbar row, the info bar as its footer. */
@@ -1021,9 +1035,12 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
1021
1035
  @media(max-width:600px){body.working-set .ts-live{padding-right:0;--ws-row-h:0px}.ts-ws{display:none}}
1022
1036
  @media (prefers-reduced-motion:reduce){.ws-item,.ws-more,.ws-ring .fill{transition:none}.ws-item.enter,.ws-tick.fresh{animation:none}}
1023
1037
  @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%}
1038
+ /* A line that wraps: its label and badge on one row, its text full width under them. A queued line, and in the
1039
+ history a labelled one (a delegate report, tui, a participant's), which beside its label read a word a line.
1040
+ Not the stage's prompt or extras: one line there. */
1041
+ :is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)){flex-wrap:wrap}
1042
+ :is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)) :is(.origin-label,.state-badge){order:0}
1043
+ :is(.bubble.user.queued[data-delivery="queue"],#history>.bubble.user:has(>.origin-label)) .user-message{order:1;flex-basis:100%}
1027
1044
  .ts-status{padding-left:10px}
1028
1045
  .ts-scroll{padding:8px 10px 10px}
1029
1046
  .ts-foot{padding:0 10px 10px}
@@ -1040,9 +1057,9 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
1040
1057
  meta keeps the width. */
1041
1058
  #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
1059
  #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)}
1060
+ /* 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. */
1061
+ #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)}
1062
+ #infoBar .meta .id:hover:not(:disabled){filter:none;border-color:var(--accent-border);background:var(--accent-bg)}
1046
1063
  #infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
1047
1064
  /* 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
1065
  #infoBar .meta .status{display:inline-grid;justify-items:center}
@@ -1088,12 +1105,16 @@ body.has-session.composer-folded #initRow{display:none}
1088
1105
  body.has-session.composer-folded #infoBar{border-top-color:transparent}
1089
1106
  #infoBar #infoCancelBtn{background:var(--bg-raised);color:var(--danger-text);border:1px solid color-mix(in srgb,var(--danger) 55%,transparent)}
1090
1107
  #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}
1108
+ /* 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). */
1109
+ #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}
1110
+ #infoBar .meta .settings-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
1111
+ #infoBar .meta .settings-chip.own{color:var(--accent);border-color:var(--accent-border)}
1112
+ /* A chip's icon (⚙ here, the start page's own SVG from composer.css): the glyph stays the text, the mask draws it. */
1113
+ #infoBar .meta :is(.settings-chip,.tools-chip){display:inline-flex;align-items:center;gap:4px}
1114
+ #infoBar .meta .settings-chip .sc-own{min-width:0;overflow:hidden;text-overflow:ellipsis}
1115
+ #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}
1116
+ #infoBar .meta .sc-gear{--icon:var(--icon-settings)}
1117
+ #infoBar .meta .tc-kit{--icon:var(--icon-toolkit)}
1097
1118
  .llmctx-field{display:flex;align-items:center;gap:8px}
1098
1119
  .llmctx-field>span{flex:0 0 58px;font-size:12px;color:var(--text-muted)}
1099
1120
  .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 +1122,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
1101
1122
  /* A select's closed box and caret: forms.css (.select-wrap); here the wrapper takes the field's width. */
1102
1123
  .llmctx-field>.select-wrap{flex:1}
1103
1124
  .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
1125
  /* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
1107
1126
  #contextChips{order:5;flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
1108
1127
  #contextChips[hidden]{display:none}
@@ -1297,13 +1316,15 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
1297
1316
  /* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
1298
1317
  @media(max-width:600px){
1299
1318
  #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
1319
  #infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
1303
1320
  #infoBar .meta::-webkit-scrollbar{display:none}
1304
1321
  #infoBar .meta>*{flex-shrink:0}
1305
1322
  #infoBar .meta .id{order:-2}
1306
1323
  #infoBar .meta .status{order:-1}
1324
+ /* ⚙ alone, the start page's dot when the session set its own (its values in the tooltip and accessible name). */
1325
+ #infoBar .meta .settings-chip .sc-own{display:none}
1326
+ #infoBar .meta .settings-chip.own{padding-right:10px}
1327
+ #infoBar .meta .settings-chip.own::after{content:"";position:absolute;top:3px;right:3px;width:5px;height:5px;border-radius:50%;background:var(--accent)}
1307
1328
  /* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
1308
1329
  #infoBar .meta .mem-chip,#infoBar .meta .ws-chip{order:-1}
1309
1330
  /* 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
  }