samagotchi 0.134.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/bin/chi +2 -3
  4. data/docs/architecture.md +6 -3
  5. data/docs/cli.md +43 -38
  6. data/docs/configuration.md +11 -9
  7. data/docs/guardrails.md +53 -9
  8. data/docs/internals/engine.md +6 -3
  9. data/docs/internals/participants.md +1 -1
  10. data/docs/internals/prompt-caching.md +1 -1
  11. data/docs/internals/web-frontend.md +263 -0
  12. data/docs/internals/working-set.md +2 -2
  13. data/docs/sessions.md +5 -3
  14. data/docs/testing.md +36 -6
  15. data/lib/samagotchi/bridge.rb +11 -6
  16. data/lib/samagotchi/bridge_client.rb +8 -5
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/config.rb +0 -2
  19. data/lib/samagotchi/engine.rb +69 -12
  20. data/lib/samagotchi/guardrail_wiring.rb +8 -6
  21. data/lib/samagotchi/guardrails/approval.rb +41 -5
  22. data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
  23. data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
  24. data/lib/samagotchi/guardrails/pair_command.rb +387 -78
  25. data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
  26. data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
  27. data/lib/samagotchi/guardrails/shared_git.rb +1 -1
  28. data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
  29. data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
  30. data/lib/samagotchi/guardrails_command.rb +13 -10
  31. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  32. data/lib/samagotchi/message_parts.rb +1 -1
  33. data/lib/samagotchi/revise_anchor.rb +33 -0
  34. data/lib/samagotchi/session_inbox.rb +20 -12
  35. data/lib/samagotchi/session_manager.rb +13 -11
  36. data/lib/samagotchi/system_one/approval_question.rb +21 -16
  37. data/lib/samagotchi/system_one/self_protect.rb +13 -15
  38. data/lib/samagotchi/tools/delegate_relay.rb +6 -5
  39. data/lib/samagotchi/tools/delegate_wait.rb +5 -5
  40. data/lib/samagotchi/tools/execute.rb +6 -6
  41. data/lib/samagotchi/tools/output_guardrails.rb +9 -2
  42. data/lib/samagotchi/tools/read.rb +5 -5
  43. data/lib/samagotchi/turn_anchors.rb +49 -0
  44. data/lib/samagotchi/verify_anchor.rb +5 -5
  45. data/lib/samagotchi/version.rb +1 -1
  46. data/lib/samagotchi/web/app.rb +6 -18
  47. data/lib/samagotchi/web/message_display.rb +33 -1
  48. data/lib/samagotchi/web/participant_access.rb +2 -2
  49. data/lib/samagotchi/web/participant_view.rb +8 -0
  50. data/lib/samagotchi/web/public/activity.js +1 -1
  51. data/lib/samagotchi/web/public/annotations.js +1 -1
  52. data/lib/samagotchi/web/public/answer_fold.js +100 -31
  53. data/lib/samagotchi/web/public/app.js +451 -367
  54. data/lib/samagotchi/web/public/bubbles.js +30 -10
  55. data/lib/samagotchi/web/public/cards_place.js +4 -2
  56. data/lib/samagotchi/web/public/chip_popover.js +2 -2
  57. data/lib/samagotchi/web/public/composer.css +8 -0
  58. data/lib/samagotchi/web/public/context_chips.js +9 -6
  59. data/lib/samagotchi/web/public/data.js +4 -2
  60. data/lib/samagotchi/web/public/dock_reserve.js +13 -15
  61. data/lib/samagotchi/web/public/feature_gates.js +33 -0
  62. data/lib/samagotchi/web/public/history_reveal.js +10 -6
  63. data/lib/samagotchi/web/public/index.html +105 -53
  64. data/lib/samagotchi/web/public/info_bar.js +11 -12
  65. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  66. data/lib/samagotchi/web/public/latest.js +46 -0
  67. data/lib/samagotchi/web/public/level_chip.js +33 -95
  68. data/lib/samagotchi/web/public/lists_view.js +15 -13
  69. data/lib/samagotchi/web/public/live_timing.js +2 -0
  70. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  71. data/lib/samagotchi/web/public/markdown_place.js +1 -1
  72. data/lib/samagotchi/web/public/method_api.js +14 -0
  73. data/lib/samagotchi/web/public/notifications.js +4 -11
  74. data/lib/samagotchi/web/public/participant.js +11 -6
  75. data/lib/samagotchi/web/public/participant_names.js +1 -1
  76. data/lib/samagotchi/web/public/participants_panel.js +4 -2
  77. data/lib/samagotchi/web/public/project_picker.js +18 -22
  78. data/lib/samagotchi/web/public/question_cards.js +2 -2
  79. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  80. data/lib/samagotchi/web/public/restore.js +2 -0
  81. data/lib/samagotchi/web/public/revise_stack.js +378 -0
  82. data/lib/samagotchi/web/public/scope.js +12 -20
  83. data/lib/samagotchi/web/public/send_turn.js +29 -16
  84. data/lib/samagotchi/web/public/session_actions.js +15 -14
  85. data/lib/samagotchi/web/public/session_fields.js +152 -0
  86. data/lib/samagotchi/web/public/session_payload.js +35 -0
  87. data/lib/samagotchi/web/public/session_state.js +9 -5
  88. data/lib/samagotchi/web/public/session_store.js +191 -0
  89. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  90. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  91. data/lib/samagotchi/web/public/shell_command.js +26 -0
  92. data/lib/samagotchi/web/public/stage_model.js +4 -4
  93. data/lib/samagotchi/web/public/stage_view.js +11 -16
  94. data/lib/samagotchi/web/public/start_settings.js +25 -88
  95. data/lib/samagotchi/web/public/stream_handlers.js +88 -90
  96. data/lib/samagotchi/web/public/task_stop.js +1 -1
  97. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  98. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  99. data/lib/samagotchi/web/public/timing.js +15 -5
  100. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  101. data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
  102. data/lib/samagotchi/web/public/turn_events.js +3 -3
  103. data/lib/samagotchi/web/public/turn_html.js +20 -16
  104. data/lib/samagotchi/web/public/turn_model.js +6 -6
  105. data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
  106. data/lib/samagotchi/web/public/working_set_view.js +3 -1
  107. data/lib/samagotchi/web/server.rb +4 -4
  108. data/lib/samagotchi/web/session_control.rb +2 -2
  109. data/lib/samagotchi/web/session_create.rb +1 -1
  110. data/lib/samagotchi/web/session_show.rb +6 -3
  111. data/lib/samagotchi/worker.rb +9 -9
  112. data/lib/samagotchi/worker_inbound.rb +6 -5
  113. metadata +15 -3
  114. data/lib/samagotchi/web/public/hold.js +0 -78
@@ -57,29 +57,64 @@ 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
- .bubble.output.boxed{overflow:hidden;transition:max-height 360ms ease}
61
- .bubble.output.revealing>:not(.copy-btn){animation:fadeIn 240ms ease}
62
- @media (prefers-reduced-motion:reduce){.bubble.output.boxed{transition:none}.bubble.output.revealing>:not(.copy-btn){animation:none}}
63
- /* 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. */
64
- .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}
65
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}
66
64
  .answer-fold:hover{background:var(--bg-panel)}
67
65
  .answer-fold:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
68
66
  .answer-fold::before{content:"▾\00a0";color:var(--accent);flex:none}
69
67
  .answer-fold.folded::before{content:"▸\00a0"}
70
- .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%}
71
75
  .answer-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);font-size:13px}
72
76
  .answer-fold-label:empty{display:none}
73
77
  .answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums}
78
+ /* Revise stacks (revise_stack.js): a superseded version folds to its stub; the Revise row under a final answer
79
+ (zero height: its button sits on the bubble's bottom edge, clear of the copy button, shown on hover) and a stack
80
+ top's version chip. */
81
+ .stack-hidden,.time-gap.stack-gap{display:none!important}
82
+ .stack-stub{display:flex;align-items:baseline;gap:8px;align-self:stretch;max-width:100%;min-width:0;margin:0;padding:4px 10px;font:inherit;font-size:12px;line-height:1.4;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:transparent;border:1px dashed var(--border-strong);border-radius:8px;transition:background .12s ease}
83
+ .stack-stub:hover{background:var(--bg-panel)}
84
+ .stack-stub:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
85
+ .stack-stub::before{content:"▸";color:var(--accent);flex:none}
86
+ .stack-stub.open::before{content:"▾"}
87
+ .stack-stub.open{border-style:solid;border-color:transparent;border-left:3px solid var(--border-strong);border-radius:6px}
88
+ .stack-stub-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
89
+ .stack-stub-gap{flex:none;margin-left:auto;color:var(--time-gap);white-space:nowrap;font-variant-numeric:tabular-nums}
90
+ .revise-row{position:relative;height:0;margin-top:-12px;display:flex;justify-content:flex-end;align-items:center;gap:8px}
91
+ .revise-row.stacked{height:auto;margin-top:-6px;justify-content:flex-start}
92
+ .revise-btn{min-height:0;height:22px;padding:0 8px;border-radius:6px;font-size:11px;line-height:20px;background:var(--bg-raised);border:1px solid var(--border-strong);color:var(--text-muted);cursor:pointer;opacity:0;transition:opacity .12s ease,color .12s ease}
93
+ .revise-row:not(.stacked) .revise-btn{position:absolute;right:14px;top:-10px;z-index:1;height:20px;line-height:18px}
94
+ .revise-btn:hover:not(:disabled){color:var(--text-bright);filter:none}
95
+ .revise-btn:disabled{cursor:default}
96
+ .revise-btn.armed,.revise-btn.armed:hover:not(:disabled){opacity:1;color:var(--on-solid);background:var(--accent-strong);border-color:var(--accent-strong)}
97
+ .bubble.output:hover+.revise-row .revise-btn,.revise-row:hover .revise-btn,.revise-row.stacked .revise-btn,.revise-btn:focus-visible{opacity:1}
98
+ @media (hover:none){.revise-btn{opacity:.55}}
99
+ .stack-versions{position:relative;font-size:12px;color:var(--text-muted)}
100
+ .stack-versions>summary{list-style:none;cursor:pointer;padding:1px 8px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);white-space:nowrap;font-variant-numeric:tabular-nums}
101
+ .stack-versions>summary::-webkit-details-marker{display:none}
102
+ .stack-versions>summary::after{content:"\00a0▾";color:var(--accent)}
103
+ .stack-versions[open]>summary::after{content:"\00a0▴"}
104
+ .stack-versions-list{position:absolute;z-index:5;top:calc(100% + 4px);left:0;display:flex;flex-direction:column;min-width:240px;max-width:min(520px,calc(100vw - 32px));padding:4px;border-radius:8px;border:1px solid var(--border-strong);background:var(--bg-raised);box-shadow:var(--toast-shadow)}
105
+ .stack-version{display:block;min-height:0;padding:4px 8px;border:0;border-radius:6px;background:transparent;color:var(--text-2);font:inherit;font-size:12px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
106
+ .stack-version:hover:not(:disabled){background:var(--bg-hover);filter:none}
107
+ .stack-version.current{color:var(--text-muted);cursor:default}
108
+ .stack-version-n{font-weight:600;color:var(--accent);font-variant-numeric:tabular-nums}
74
109
  /* A gap divider (time_gaps.js): one centred dim line where a quiet stretch passed, never wrapped (a phone at 360 px keeps it on one line). Not a .bubble. */
75
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}
76
111
  .turn-timing{margin-top:8px;color:var(--text-muted);font-size:11px;font-variant-numeric:tabular-nums}
77
112
  .turn-timing.live{color:var(--accent)}
78
- .bubble.output.markdown{white-space:normal}
79
- .bubble.output.markdown pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
80
- .bubble.output.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
81
- .bubble.output.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:1px 5px;color:var(--text)}
82
- .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)}
83
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"). */
84
119
  .syntax-highlighting{color:var(--code-text)}
85
120
  .syntax-highlighting .hl-keyword,.syntax-highlighting .hl-storage{color:var(--code-keyword)}
@@ -92,24 +127,24 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
92
127
  .syntax-highlighting .hl-markup.hl-inserted{color:var(--diff-add-text)}
93
128
  .syntax-highlighting .hl-markup.hl-deleted{color:var(--diff-del-text)}
94
129
  .syntax-highlighting .hl-invalid{color:var(--danger-text)}
95
- .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)}
96
- .bubble.output.markdown h1{font-size:19px}
97
- .bubble.output.markdown h2{font-size:17px}
98
- .bubble.output.markdown h3{font-size:15.5px}
99
- .bubble.output.markdown h4,.bubble.output.markdown h5,.bubble.output.markdown h6{font-size:15px;color:var(--text-2)}
100
- .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}
101
- .bubble.output.markdown strong{color:var(--text-bright)}
102
- .bubble.output.markdown hr{border:0;border-top:1px solid var(--border);margin:14px 0}
103
- .bubble.output.markdown img{max-width:100%}
104
- .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}
105
- .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}
106
- .bubble.output.markdown ul,.bubble.output.markdown ol{padding-left:1.5em}
107
- .bubble.output.markdown li+li{margin-top:4px}
108
- .bubble.output.markdown li>ul,.bubble.output.markdown li>ol{margin:4px 0 0}
109
- .bubble.output.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-muted)}
110
- .bubble.output.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
111
- .bubble.output.markdown th,.bubble.output.markdown td{border:1px solid var(--border-strong);padding:5px 8px;font-size:14px}
112
- .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}
113
148
  /* Plugins' slow setup (chi.init, init_row.js): a quiet line per task over the composer. */
114
149
  #initRow{display:flex;flex-direction:column;gap:2px;padding:6px 12px 2px;font-size:12px;line-height:1.4;color:var(--text-3)}
115
150
  #initRow[hidden]{display:none}
@@ -159,6 +194,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
159
194
  .review-apply-note{border-left:0;padding-left:0;text-align:center}
160
195
  }
161
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%}
162
202
  .bubble.user blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-3);margin:2px 0 6px}
163
203
  .bubble.user blockquote:last-child{margin-bottom:0}
164
204
  .bubble.tool{border-left:3px solid var(--ok);background:var(--bg-panel);font-size:13px;color:var(--text-2)}
@@ -166,7 +206,11 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
166
206
  /* A stop someone chose (a cancel, the session's stop): the ■ STOPPED rows' colours, not an error's. */
167
207
  .bubble.cancel.stopped{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,var(--bg-panel));color:var(--warn-text)}
168
208
  .bubble.recap{border-left:3px solid var(--think-text);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));font-size:13px}
169
- .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:"▾ "}
170
214
  .bubble.recap .recap-body{white-space:pre-wrap;margin-top:6px}
171
215
  .bubble.recap.stale{opacity:.7;border-left-style:dashed}
172
216
  .activity-body{margin-top:6px}
@@ -286,7 +330,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
286
330
  .bubble.thinking>summary::before{content:"▸ ";color:var(--think)}
287
331
  .bubble.thinking[open]>summary::before{content:"▾ "}
288
332
  .thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
289
- /* Turn view (web.view: turn, and inside the stage): one block per turn, its steps as collapsed rows, the live one open at the bottom. */
333
+ /* The turn block (the stage's cloud, the history): one block per turn, its steps as collapsed rows, the live one open on top. */
290
334
  .turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
291
335
  .turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
292
336
  .turn-work>summary::-webkit-details-marker{display:none}
@@ -303,7 +347,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
303
347
  .gen>summary::-webkit-details-marker{display:none}
304
348
  .gen>summary::before{content:"▸ ";color:var(--text-faint)}
305
349
  .gen[open]>summary::before{content:"▾ "}
306
- /* Open, a step whose label starts with its body's first words (turn_view.js labelParts) says only its calls. */
350
+ /* Open, a step whose label starts with its body's first words (turn_block.js labelParts) says only its calls. */
307
351
  .gen[open]>summary.repeats>:is(.gen-head,.gen-sep){display:none}
308
352
  /* A step with a failed call (status error): its chevron in the danger colour. */
309
353
  .gen.has-failed>summary::before{color:var(--danger)}
@@ -327,7 +371,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
327
371
  .gen details.thinking summary.thinking-fade{animation:thinking-fade 120ms ease-out}
328
372
  @keyframes thinking-fade{from{opacity:.4}to{opacity:1}}
329
373
  @media (prefers-reduced-motion:reduce){.gen details.thinking summary.thinking-fade{animation:none}}
330
- /* A finished block's outline (turn_view.js outlineBlock): open on its narrated steps' rows; a run of steps that said
374
+ /* A finished block's outline (turn_block.js outlineBlock): open on its narrated steps' rows; a run of steps that said
331
375
  nothing is one "· N steps" fold, a row like a step's, that shows them. */
332
376
  .turn-work.outline{--step-indent:14px}
333
377
  .turn-work.outline>.gen.folded:not(.unfolded){display:none}
@@ -574,7 +618,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
574
618
  .bubble.user>.copy-btn:hover{background:var(--bg-raised);border-color:var(--border-strong)}
575
619
  .code-wrap{position:relative;margin:0 0 12px}
576
620
  .code-wrap:last-child{margin-bottom:0}
577
- .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}
578
622
  .code-wrap>.copy-btn{top:5px;right:5px}
579
623
  @media (hover:none){.copy-btn{opacity:.55}}
580
624
  /* The dock is the composer card (composer.css .composer-card): the text field, a toolbar row, the info bar as its footer. */
@@ -676,9 +720,9 @@ body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){dis
676
720
  #commandList .command-desc{flex:1;min-width:0;font-size:13px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
677
721
  #commandList .command-note{font-size:11px;color:var(--text-faint);white-space:nowrap}
678
722
  @media(max-width:600px){#commandList{left:6px;width:calc(100% - 12px)}#commandList .command-option{flex-wrap:wrap;row-gap:0;padding:8px 10px}#commandList .command-desc{flex-basis:100%;order:3}}
679
- /* The stage view (web.view: stage, stage_view.js): the running turn as the dock card's top section, pinned above the composer.
723
+ /* The stage (stage_view.js): the running turn as the dock card's top section, pinned above the composer.
680
724
  Fixed slots (status row, prompt line, a two-line headline, the tool row, a one-line trail) never change height while the
681
- turn runs; cards and rows that arrive meanwhile go in their own capped scroller (extras); the cloud is the turn view's
725
+ turn runs; cards and rows that arrive meanwhile go in their own capped scroller (extras); the cloud is the turn
682
726
  block behind a chip of ticks. --stage-max (stage_view.js fitCap) keeps it inside the session panel. */
683
727
  /* D2: its own glass card above the composer card (as a section of one card the composer's focus ring wrapped the turn too):
684
728
  in the stage view #dock is a clear column holding the stage and .dock-card (the composer and its footer, stage_view.js). */
@@ -791,7 +835,7 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
791
835
  .ts-answer{display:flex;flex-direction:column;gap:8px}
792
836
  .ts-answer>.bubble.output{border-left:0;background:transparent;border:0;padding:0}
793
837
  .ts-answer>.bubble.output>.copy-btn{top:0;right:0}
794
- /* The cloud: one chip (steps, calls, a tick per call), the turn view's block behind it. */
838
+ /* The cloud: one chip (steps, calls, a tick per call), the turn block behind it. */
795
839
  /* The footer: the chip at the stage's bottom edge (the cloud's steps open above it, the scroller's last slot). */
796
840
  .ts-foot{position:relative;flex:none;padding:0 14px 10px}
797
841
  /* The question room's jump pill (stage_view.js drawPill): at the scroller's bottom edge, above the chip, while the waiting
@@ -993,9 +1037,12 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
993
1037
  @media(max-width:600px){body.working-set .ts-live{padding-right:0;--ws-row-h:0px}.ts-ws{display:none}}
994
1038
  @media (prefers-reduced-motion:reduce){.ws-item,.ws-more,.ws-ring .fill{transition:none}.ws-item.enter,.ws-tick.fresh{animation:none}}
995
1039
  @media(max-width:600px){
996
- .bubble.user.queued[data-delivery="queue"]{flex-wrap:wrap}
997
- .bubble.user.queued[data-delivery="queue"] .origin-label,.bubble.user.queued[data-delivery="queue"] .state-badge{order:0}
998
- .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%}
999
1046
  .ts-status{padding-left:10px}
1000
1047
  .ts-scroll{padding:8px 10px 10px}
1001
1048
  .ts-foot{padding:0 10px 10px}
@@ -1012,9 +1059,9 @@ button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
1012
1059
  meta keeps the width. */
1013
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}
1014
1061
  #infoBar .meta{font-size:11px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
1015
- #infoBar .meta .id{font-family:ui-monospace,monospace;font-weight:700;font-size:12px;color:var(--accent)}
1016
- #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)}
1017
- #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)}
1018
1065
  #infoBar .meta .status{font-size:11px;padding:2px 6px;border-radius:999px}
1019
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. */
1020
1067
  #infoBar .meta .status{display:inline-grid;justify-items:center}
@@ -1060,12 +1107,16 @@ body.has-session.composer-folded #initRow{display:none}
1060
1107
  body.has-session.composer-folded #infoBar{border-top-color:transparent}
1061
1108
  #infoBar #infoCancelBtn{background:var(--bg-raised);color:var(--danger-text);border:1px solid color-mix(in srgb,var(--danger) 55%,transparent)}
1062
1109
  #infoBar #infoCancelBtn:hover:not(:disabled){color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
1063
- /* 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. */
1064
- #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}
1065
- #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)}
1066
- #infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own,#infoBar .meta .rh-chip.own{color:var(--accent);border-color:var(--accent-border)}
1067
- .llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
1068
- .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)}
1069
1120
  .llmctx-field{display:flex;align-items:center;gap:8px}
1070
1121
  .llmctx-field>span{flex:0 0 58px;font-size:12px;color:var(--text-muted)}
1071
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}
@@ -1073,8 +1124,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
1073
1124
  /* A select's closed box and caret: forms.css (.select-wrap); here the wrapper takes the field's width. */
1074
1125
  .llmctx-field>.select-wrap{flex:1}
1075
1126
  .llmctx-field input::placeholder{color:var(--text-faint);opacity:1}
1076
- .llmctx-form .llmctx-set{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border)}
1077
- .llmctx-form .llmctx-set:disabled{opacity:.6;cursor:default}
1078
1127
  /* Attached context (context_chips.js): a row of its own under the meta; not the composer's #chips/.chip. */
1079
1128
  #contextChips{order:5;flex-basis:100%;display:flex;flex-wrap:wrap;gap:6px;align-items:center;min-width:0}
1080
1129
  #contextChips[hidden]{display:none}
@@ -1269,13 +1318,15 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
1269
1318
  /* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
1270
1319
  @media(max-width:600px){
1271
1320
  #infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
1272
- /* No terminal on a phone to attach from. */
1273
- #infoBar .meta .copy-attach{display:none}
1274
1321
  #infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
1275
1322
  #infoBar .meta::-webkit-scrollbar{display:none}
1276
1323
  #infoBar .meta>*{flex-shrink:0}
1277
1324
  #infoBar .meta .id{order:-2}
1278
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)}
1279
1330
  /* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
1280
1331
  #infoBar .meta .mem-chip,#infoBar .meta .ws-chip{order:-1}
1281
1332
  /* A parent's children chip too: its waiting part is what to see. */
@@ -1654,6 +1705,7 @@ a{color:var(--accent)}
1654
1705
  </div>
1655
1706
  <div id="deliveryLine" class="composer-line" aria-live="off" hidden></div>
1656
1707
  <div class="compose-col">
1708
+ <div id="reviseChip" hidden></div>
1657
1709
  <div id="chips" hidden></div>
1658
1710
  <textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
1659
1711
  </div>
@@ -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
  }
@@ -0,0 +1,46 @@
1
+ // Stale-answer guards: an answer that comes back after an await is applied
2
+ // only while what it was asked for is still what the page shows. The page
3
+ // switches sessions, reopens a popover or asks again while a fetch is out;
4
+ // an answer for the old ask must be dropped, not drawn over the new one.
5
+ //
6
+ // Two shapes (docs/internals/web-frontend.md, "Stale answers"):
7
+ // - stillOn(get): the thing asked about (the open session's id) hasn't
8
+ // changed. For a one-off read: the same session may ask twice and both
9
+ // answers are good.
10
+ // - createLatest(get): also only the newest ask counts. For a list that is
11
+ // re-read (a refresh while a refresh is out): an older answer landing
12
+ // last would draw the older list.
13
+ //
14
+ // const latest = createLatest(() => sessionId);
15
+ // async function refresh() {
16
+ // const fresh = latest.begin();
17
+ // const data = await api.list(sessionId);
18
+ // if (!fresh()) return;
19
+ // …
20
+ // }
21
+
22
+ // @param get () => the key the answer is for (the open session's id)
23
+ // @param at the key asked for (default: get() now)
24
+ // @return () => true while get() is still +at+
25
+ export function stillOn(get, at = get()) {
26
+ return () => get() === at;
27
+ }
28
+
29
+ // @param get () => the key the asks are for (default: none, the newest ask
30
+ // alone decides)
31
+ // @return {begin(), drop()}: begin() starts an ask and gives its guard,
32
+ // () => true while it is the newest ask and get() is unchanged; drop()
33
+ // makes every ask out stale (a reset)
34
+ export function createLatest(get = () => null) {
35
+ let seq = 0;
36
+ return {
37
+ begin() {
38
+ const mine = ++seq;
39
+ const same = stillOn(get);
40
+ return () => mine === seq && same();
41
+ },
42
+ drop() {
43
+ seq += 1;
44
+ },
45
+ };
46
+ }
@@ -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
  }