@gotcos/glasses-server 6.99.33 → 7.0.7

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 (76) hide show
  1. package/.env.example +18 -0
  2. package/CHANGELOG.md +417 -0
  3. package/README.md +3 -1
  4. package/managed-runtime-contract.json +3 -0
  5. package/package.json +1 -1
  6. package/server/index.ts +29 -7
  7. package/server/lib/agent-session-output-images.ts +627 -49
  8. package/server/lib/agent-session-store.ts +24 -11
  9. package/server/lib/agent-session-tool-images.ts +224 -0
  10. package/server/lib/agent-session-turns.ts +19 -2
  11. package/server/lib/api-auth.ts +6 -0
  12. package/server/lib/ask-work-plan.ts +3 -1
  13. package/server/lib/calendar-source.ts +491 -0
  14. package/server/lib/codex-bridge.ts +4 -1
  15. package/server/lib/codex-run-ledger.ts +29 -0
  16. package/server/lib/context-builder.ts +2 -1
  17. package/server/lib/drop-p2-runtime.ts +1 -1
  18. package/server/lib/drop-store.ts +10 -4
  19. package/server/lib/learning-needs-you.ts +24 -7
  20. package/server/lib/live-events.ts +533 -0
  21. package/server/lib/local-path-redact.ts +272 -0
  22. package/server/lib/meeting-ask-orientation.ts +40 -6
  23. package/server/lib/meeting-ask-runner.ts +30 -10
  24. package/server/lib/morning-brief-runtime.ts +4 -2
  25. package/server/lib/native-mac-caller.ts +29 -0
  26. package/server/lib/ping-engine.ts +1 -1
  27. package/server/lib/ping-sources.ts +11 -6
  28. package/server/lib/preview-content.ts +172 -0
  29. package/server/lib/preview-error-code.ts +10 -0
  30. package/server/lib/preview-folders-setting.ts +133 -0
  31. package/server/lib/preview-log-throttle.ts +18 -0
  32. package/server/lib/preview-thumbs.ts +139 -0
  33. package/server/lib/secret-path-guard.ts +80 -0
  34. package/server/lib/session-files-setting.ts +88 -0
  35. package/server/lib/session-links.ts +280 -0
  36. package/server/lib/thread-turn-queue.ts +10 -0
  37. package/server/lib/today-brief.ts +119 -28
  38. package/server/lib/web-memories-bridge.ts +3 -1
  39. package/server/lib/work-activity.ts +56 -6
  40. package/server/lib/work-session-activity.ts +260 -0
  41. package/server/lib/your-move-count.ts +9 -3
  42. package/server/routes/agent-session-bindings.ts +13 -0
  43. package/server/routes/agent-session-stream.ts +16 -0
  44. package/server/routes/agent-sessions.ts +146 -11
  45. package/server/routes/ask-work-plan.ts +4 -1
  46. package/server/routes/claude-sessions.ts +5 -1
  47. package/server/routes/health.ts +35 -0
  48. package/server/routes/live-events.ts +329 -0
  49. package/server/routes/permission-broker.ts +69 -5
  50. package/server/routes/session-preview.ts +200 -0
  51. package/server/routes/thread-turn-queue.ts +207 -3
  52. package/server/routes/today-brief.ts +44 -9
  53. package/server/routes/welcome-context.ts +23 -3
  54. package/server/routes/work-board.ts +9 -2
  55. package/server/web-control/app.css +40 -1
  56. package/server/web-control/app.js +1309 -56
  57. package/server/web-control/control.css +25 -0
  58. package/server/web-control/index.html +7 -2
  59. package/server/web-control/memory-review.js +100 -0
  60. package/server/web-control/panes/cos-chat.css +42 -3
  61. package/server/web-control/panes/cos-chat.js +267 -15
  62. package/server/web-control/panes/meetings.css +18 -2
  63. package/server/web-control/panes/meetings.js +24 -8
  64. package/server/web-control/panes/memories.css +17 -2
  65. package/server/web-control/panes/memories.js +127 -42
  66. package/server/web-control/panes/messages.js +21 -2
  67. package/server/web-control/panes/preview.js +318 -0
  68. package/server/web-control/panes/questions.js +28 -8
  69. package/server/web-control/panes/reader-media.js +409 -23
  70. package/server/web-control/panes/sessions.css +32 -3
  71. package/server/web-control/panes/settings.js +96 -5
  72. package/server/web-control/panes/today-actions.css +11 -0
  73. package/server/web-control/panes/today-actions.js +45 -12
  74. package/server/web-control/panes/work.css +10 -0
  75. package/server/web-control/panes/work.js +10 -2
  76. package/server/web-control/shell.js +17 -14
@@ -303,6 +303,14 @@ button.cx-set-row:hover { background: var(--qopen); }
303
303
  .cx-set-row .cx-set-time { width: 148px; }
304
304
  .cx-set-row .cx-set-day { width: 150px; padding-right: 36px; background-position: calc(100% - 20px) 17px, calc(100% - 15px) 17px; }
305
305
  .cx-set-row .cx-set-steer { flex: none; width: 46%; min-width: 180px; min-height: 64px; }
306
+ /* 7.0.5 extra folders for previews: a folder name wraps, never pushes the row sideways; 16px input keeps iOS from zooming. */
307
+ .cx-set-folder .pf-name { font: 400 14px/1.35 var(--mono); overflow-wrap: anywhere; }
308
+ .cx-set-folder .cx-btn { flex: none; min-height: 44px; }
309
+ .pf-add { flex-wrap: wrap; }
310
+ .pf-add .cx-set-text { flex-basis: 100%; }
311
+ .pf-add-line { display: flex; gap: 8px; width: 100%; min-width: 0; }
312
+ .pf-add-line input { flex: 1; min-width: 0; min-height: 44px; padding: 0 12px; font-size: 16px; border: 1px solid var(--hairline); border-radius: 10px; background: var(--panel, transparent); color: var(--ink); box-sizing: border-box; }
313
+ .pf-add-line .cx-btn { flex: none; min-height: 44px; }
306
314
  @media (max-width: 599px) {
307
315
  .cx-set-row:has(.cx-set-steer) { flex-wrap: wrap; }
308
316
  .cx-set-row .cx-set-steer { width: 100%; min-width: 0; }
@@ -329,3 +337,20 @@ html, body { overscroll-behavior-y: none; }
329
337
  their content stops at 720 px instead of running lines to 137 characters; prose lines stop at about 68 characters. */
330
338
  #pane-settings, #pane-dictation { max-width: 720px; }
331
339
  .cx-set-desc, .cx-set-blurb, .wk-fine { max-width: 68ch; }
340
+
341
+ /* Duo tables: every markdown pipe table (Sessions reader, Today's session sheet, Meetings notes and Ask, Memories
342
+ answers). app.js tableFit sets each column's floor; this keeps words whole. The panes' own text sets
343
+ overflow-wrap:anywhere, which lets a table squeeze a column to one letter, so cells go back to break-word: a column is
344
+ never narrower than its longest word, and only a .tf-long token (a URL, a hash) breaks inside. A table wider than its
345
+ box scrolls inside it, never the page, with the first column held at the left edge. */
346
+ .tf-wrap { overflow-x: auto; max-width: 100%; overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; }
347
+ table.tf th, table.tf td { overflow-wrap: break-word; word-break: normal; hyphens: manual; }
348
+ table.tf .tf-min { display: block; }
349
+ table.tf .tf-long { overflow-wrap: anywhere; }
350
+ table.tf.tf-key tr > :first-child { white-space: nowrap; }
351
+ table.tf tr > :first-child { position: sticky; left: 0; z-index: 1; background: var(--tf-bg, var(--panel)); }
352
+ /* A held cell keeps its own borders only in the separated model; in a collapsed table the lines stay behind. */
353
+ table.tf-grid { border-collapse: separate; border-spacing: 0; }
354
+ table.tf-grid th, table.tf-grid td { border: 0; border-right: 1px solid var(--tf-line, var(--hairline)); border-bottom: 1px solid var(--tf-line, var(--hairline)); }
355
+ table.tf-grid tr > :first-child { border-left: 1px solid var(--tf-line, var(--hairline)); }
356
+ table.tf-grid > :first-child > tr:first-child > * { border-top: 1px solid var(--tf-line, var(--hairline)); }
@@ -123,6 +123,7 @@
123
123
  <h1 class="pane-title">Today</h1>
124
124
  <p id="today-date" class="today-date"></p>
125
125
  <p id="today-weather" class="today-weather hidden"></p>
126
+ <div class="today-live"><button type="button" id="today-refresh" class="link-btn today-refresh" data-today-refresh>Refresh</button><span id="today-live-note" class="today-live-note" aria-hidden="true"></span><span id="today-live-status" class="sr-only" role="status" aria-live="polite"></span></div>
126
127
  </div>
127
128
  <button type="button" class="ask-bar" data-open-ask>
128
129
  <svg class="icon"><use href="#i-ask"/></svg>
@@ -161,6 +162,7 @@
161
162
  <div class="panel-head"><h2 id="next-title" class="panel-title">Next up</h2></div>
162
163
  <div id="next-body" class="next-up"></div>
163
164
  <div id="next-actions" class="next-actions"></div>
165
+ <div id="next-later" class="next-later hidden" aria-label="Later today"></div>
164
166
  </section>
165
167
  </div>
166
168
  </div>
@@ -181,7 +183,7 @@
181
183
  </header>
182
184
  <div class="cx-controls sx-views" id="sessions-views"></div>
183
185
  <div class="split sx-split" id="sessions-split">
184
- <div class="split-list">
186
+ <div class="split-list" id="sessions-list-col">
185
187
  <div class="sx-tools">
186
188
  <form class="cx-search sx-search" id="sessions-search-form" role="search">
187
189
  <input id="sessions-q" class="field" type="search" placeholder="Search titles, transcripts…" enterkeyhint="search" autocomplete="off" aria-label="Search titles, transcripts">
@@ -197,6 +199,7 @@
197
199
  <div id="sessions-hooks"></div>
198
200
  <div id="sessions-list"></div>
199
201
  </div>
202
+ <div class="sx-handle" id="sessions-handle" role="separator" aria-orientation="vertical" aria-controls="sessions-list-col" aria-label="Session list width" tabindex="0" title="Drag to resize. Double-tap or double-click to reset."></div>
200
203
  <div class="reader sx-reader" id="session-reader"><p class="reader-placeholder">Pick a session to read it and send it a turn.</p></div>
201
204
  </div>
202
205
  <div class="sx-usage hidden" id="sessions-usage"></div>
@@ -266,6 +269,7 @@
266
269
  <div id="toast" class="toast hidden" role="status"></div>
267
270
 
268
271
  <script src="/control/vendor/qrcode-encoder.js"></script>
272
+ <script src="/control/memory-review.js"></script>
269
273
  <script src="/control/app.js"></script>
270
274
  <script src="/control/shell.js"></script>
271
275
  <script src="/control/panes/cos-chat.js"></script>
@@ -281,7 +285,8 @@
281
285
  "settings": "/control/panes/settings.js",
282
286
  "memories": "/control/panes/memories.js",
283
287
  "todayActions": "/control/panes/today-actions.js",
284
- "readerMedia": "/control/panes/reader-media.js"
288
+ "readerMedia": "/control/panes/reader-media.js",
289
+ "preview": "/control/panes/preview.js"
285
290
  }</script>
286
291
  <!-- The Knowledge graph explorer, loaded by panes/memories.js when the graph first opens. -->
287
292
  <script type="application/json" id="memories-graph-assets">{
@@ -0,0 +1,100 @@
1
+ /* COS Control Web: what counts as "Needs review" in Memories. The one predicate (Miles 2026-11-08: "We
2
+ should only tag memories that need actual review with that status").
3
+
4
+ Loaded by index.html before app.js, so the Memories pane reads it as window.COSMemoryReview, and imported by the
5
+ server (server/lib/learning-needs-you.ts) for the count it hands Today, the rail story and the To review tab. The
6
+ producer is MU's `learning_events.py needs-you` (review_state there); the case table REVIEW_CASES in
7
+ memory-review.test.ts mirrors tests/test_memory_review_state.py in MU, case for case. Change both together.
8
+
9
+ An item needs review only when it holds a decision only the owner can make, with a real next step:
10
+ rule_ready a suggested rule AT or OVER its promotion bar (0.8): approve, edit or dismiss. Below the bar it
11
+ cannot be promoted, so Dismiss is the only action: it is Learning, shown with its confidence and
12
+ never counted, until its confidence reaches the bar. `low_confidence` is the older producer's name
13
+ for the same suggestion and is judged by its confidence the same way.
14
+ attribution a decision that reports what a named person said, with no attribution: whose words were they.
15
+ scope a decision or correction with no domain, so recall cannot route it.
16
+ contradiction two memories that disagree (not produced yet).
17
+ candidate a governed memory awaiting acceptance, or a quarantined one awaiting release or delete.
18
+ prune_ready a memory the nightly retention pass would quarantine; it waits for Accept or Quarantine.
19
+ A decision of accepted, dismissed or pruned moves an item to history. Any gate this file does not name is Learning,
20
+ never counted. */
21
+ (function (root) {
22
+ 'use strict';
23
+ // The bar is the producer's: learning_events.py RULE_PROMOTION_BAR, carried as `bar` on the needs-you block and on
24
+ // each rule item. This copy is only the fallback for a block that does not carry one (an older producer); the parity
25
+ // test in memory-review.test.ts reads the Python constant and fails if the two ever differ.
26
+ var RULE_BAR = 0.8;
27
+ var RULE_GATES = { rule_ready: 1, low_confidence: 1 };
28
+ var DECISION_GATES = { attribution: 1, scope: 1, contradiction: 1, candidate: 1, prune_ready: 1 };
29
+ var CLOSING = { accepted: 1, dismissed: 1, pruned: 1 };
30
+ var NEEDS = 'needs_review', LEARNING = 'learning', HISTORY = 'history';
31
+ function own(map, key) { return typeof key === 'string' && Object.prototype.hasOwnProperty.call(map, key); }
32
+ function isNum(v) { return typeof v === 'number' && isFinite(v); }
33
+
34
+ /** needs_review, learning or history for one item, from its gate, confidence and latest decision. */
35
+ function reviewState(gate, confidence, decision, bar) {
36
+ if (own(CLOSING, decision)) return HISTORY;
37
+ if (own(RULE_GATES, gate)) return isNum(confidence) && confidence >= (isNum(bar) ? bar : RULE_BAR) ? NEEDS : LEARNING;
38
+ if (own(DECISION_GATES, gate)) return NEEDS;
39
+ return LEARNING;
40
+ }
41
+ /** The bar a block was judged with: its own `bar`, else the fallback. */
42
+ function barOf(block) { return block && typeof block === 'object' && isNum(block.bar) ? block.bar : RULE_BAR; }
43
+ /** The same, for a needs-you item; `decision` (a decision made on this page) wins over one on the item. The item's own
44
+ * `bar` wins, then `blockBar` (the block it came in), then the fallback. */
45
+ function itemState(item, decision, blockBar) {
46
+ if (!item || typeof item !== 'object') return LEARNING;
47
+ return reviewState(item.gate, item.confidence, decision || item.decision, isNum(item.bar) ? item.bar : blockBar);
48
+ }
49
+ function needsReview(item, decision, blockBar) { return itemState(item, decision, blockBar) === NEEDS; }
50
+
51
+ /** A needs-you block whose items are only what needs review. A below-bar suggestion from an older producer moves to
52
+ * `learning` and leaves `count`; the count keeps every needs-review item past the shown cap. Never mutates. */
53
+ function normalizeBlock(block) {
54
+ if (!block || typeof block !== 'object' || !Array.isArray(block.items)) return block || null;
55
+ var keep = [], moved = [], bar = barOf(block);
56
+ block.items.forEach(function (it) { (needsReview(it, null, bar) ? keep : moved).push(it); });
57
+ var out = {};
58
+ Object.keys(block).forEach(function (k) { out[k] = block[k]; });
59
+ out.items = keep;
60
+ if (isNum(block.count)) out.count = Math.max(keep.length, block.count - moved.length);
61
+ if (moved.length && block.gates && typeof block.gates === 'object') {
62
+ var gates = {};
63
+ Object.keys(block.gates).forEach(function (g) { gates[g] = block.gates[g]; });
64
+ moved.forEach(function (it) { var g = it && it.gate; if (own(gates, g) && isNum(gates[g])) { gates[g] -= 1; if (gates[g] <= 0) delete gates[g]; } });
65
+ out.gates = gates;
66
+ }
67
+ var learnt = moved.filter(function (it) { return itemState(it, null, bar) === LEARNING && it && typeof it === 'object' && own(RULE_GATES, it.gate); });
68
+ if (learnt.length || Array.isArray(block.learning)) {
69
+ out.learning = learnt.concat(Array.isArray(block.learning) ? block.learning : []);
70
+ out.learning_count = (isNum(block.learning_count) ? block.learning_count : (Array.isArray(block.learning) ? block.learning.length : 0)) + learnt.length;
71
+ }
72
+ return out;
73
+ }
74
+ /** The shown items that still need review, leaving out any decided on this page (`decisions`: lesson_id to decision). */
75
+ function openItems(block, decisions) {
76
+ var d = decisions || {}, bar = barOf(block);
77
+ return block && Array.isArray(block.items) ? block.items.filter(function (it) { return needsReview(it, it && d[it.lesson_id], bar); }) : [];
78
+ }
79
+ /** The count every surface shows (Today, the rail, the To review tab): open shown items, else count capped by limit. */
80
+ function count(block, decisions) {
81
+ if (!block || typeof block !== 'object') return null;
82
+ if (Array.isArray(block.items)) return openItems(block, decisions).length;
83
+ if (isNum(block.count)) return isNum(block.limit) ? Math.min(block.count, block.limit) : block.count;
84
+ return null;
85
+ }
86
+ /** Every item that needs review, past the shown cap (the "of N" in "Next 7 of N"). */
87
+ function total(block) {
88
+ if (!block || typeof block !== 'object') return null;
89
+ if (isNum(block.count)) return block.count;
90
+ return Array.isArray(block.items) ? openItems(block).length : null;
91
+ }
92
+ /** Below-bar suggestions to show under Learning, leaving out any decided on this page. */
93
+ function learningItems(block, decisions) {
94
+ var d = decisions || {}, bar = barOf(block);
95
+ return block && Array.isArray(block.learning) ? block.learning.filter(function (it) { return itemState(it, it && d[it.lesson_id], bar) === LEARNING; }) : [];
96
+ }
97
+
98
+ root.COSMemoryReview = { RULE_BAR: RULE_BAR, barOf: barOf, NEEDS: NEEDS, LEARNING: LEARNING, HISTORY: HISTORY, reviewState: reviewState, itemState: itemState,
99
+ needsReview: needsReview, normalizeBlock: normalizeBlock, openItems: openItems, count: count, total: total, learningItems: learningItems };
100
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -6,21 +6,42 @@
6
6
  .msg-split[data-show='chat'] > .msg-left { display: none; }
7
7
  @media (min-width: 860px) {
8
8
  /* Two columns from 860 px (the unfolded Duo reads like the Mac): the list left, the chat right. */
9
- .msg-split { display: grid; grid-template-columns: minmax(300px, 40%) minmax(0, 1fr); column-gap: 24px; align-items: start; }
9
+ .msg-split { display: grid; grid-template-columns: minmax(300px, 40%) 24px minmax(0, 1fr); column-gap: 0; align-items: start; }
10
10
  .msg-split[data-show] > .msg-left, .msg-split[data-show] > .msg-right { display: block; }
11
+ /* Duo tables: the divider (app.js splitKit, styled in app.css) is the 24 px column that was the gap; a dragged width
12
+ is a share of the split, clamped so the list keeps 240 px and the chat 440 px, room for its composer (messages.js SPLIT). */
13
+ .msg-split.split-sized { grid-template-columns: clamp(240px, var(--split-list-share, 40%), calc(100% - 464px)) 24px minmax(0, 1fr); }
14
+ .msg-split > .split-handle { display: flex; top: calc(56px + var(--safe-top, 0px)); height: calc(100dvh - 72px - var(--safe-top, 0px)); }
11
15
  /* The list column is narrower than the pane: its header's buttons go under the title instead of over it. */
12
16
  .msg-left .cx-hero .cx-actions { grid-column: 1 / -1; justify-content: flex-start; }
13
- .msg-right { position: sticky; top: calc(56px + var(--safe-top, 0px)); height: calc(100vh - 72px - var(--safe-top, 0px)); height: calc(100dvh - 72px - var(--safe-top, 0px)); min-height: 420px; }
17
+ /* 7.0.2 (A4): the floor never asks for more than the screen has (420 px on a 539 px unfolded Duo with the keyboard up
18
+ pushed the composer under it). */
19
+ .msg-right { position: sticky; top: calc(56px + var(--safe-top, 0px)); height: calc(100vh - 72px - var(--safe-top, 0px)); height: calc(100dvh - 72px - var(--safe-top, 0px)); min-height: 420px; min-height: min(420px, calc(100dvh - 72px - var(--safe-top, 0px))); }
14
20
  .msg-right .cc { height: 100%; display: flex; flex-direction: column; background: var(--panel); border: 1px solid var(--panel-border); border-radius: 16px; overflow: hidden; }
15
21
  .msg-right .cc-head { padding: 14px 16px 12px; }
16
22
  .msg-right .cc-feed { flex: 1; overflow-y: auto; padding: 12px 16px 8px; overscroll-behavior: contain; }
17
- .msg-right .cc-composer { position: static; padding: 10px 16px 12px; background: var(--panel); }
23
+ .msg-right .cc-composer { position: static; padding: 10px 16px 12px; background: var(--panel); flex: none; }
24
+ /* 7.0.2 (canary WARNING 1): the feed gives way, never the composer. With the 44 px floor on short touch screens (A8),
25
+ Model and Effort are 44 px and the composer about 235 px; the feed's 160 px floor then pushed the composer 21 px
26
+ out of the 467 px card at 951x539 and clipped its fine print. */
27
+ .msg-right .cc-feed { min-height: 0; }
28
+ /* 7.0.2 (A4): with an on-screen keyboard up for a field in the chat (cos-chat.js sets html.kb-chat; app.js publishes
29
+ the visible box, --vv-top and --vv-h), the chat column is the visible box, so its composer stays above the keyboard;
30
+ the header drops to one row. The Messages search box on the left never resizes it. */
31
+ html.kb-chat .msg-right { top: calc(var(--vv-top, 0px) + 8px); height: calc(var(--vv-h, 100dvh) - 16px); min-height: 0; }
32
+ html.kb-chat .msg-right .cc-head { padding: 6px 16px; }
33
+ html.kb-chat .msg-right .cc-sub, html.kb-chat .msg-right .cc-thread, html.kb-chat .msg-right .cc-head-acts, html.kb-chat .msg-right .cc-avatar.big { display: none; }
34
+ html.kb-chat .msg-right .cc-title { font-size: 18px; }
35
+ html.kb-chat .msg-right .cc-prep { max-height: 35%; }
18
36
  }
19
37
 
20
38
  /* Below 860 px the chat fills the screen, so the composer sits at the bottom even when the chat is short. */
21
39
  @media (max-width: 859px) {
22
40
  .msg-split[data-show='chat'] .cc { min-height: calc(100vh - 64px - var(--safe-top, 0px)); min-height: calc(100dvh - 64px - var(--safe-top, 0px)); display: flex; flex-direction: column; }
23
41
  .msg-split[data-show='chat'] .cc-feed { flex: 1; }
42
+ /* 7.0.2 (A4): with the keyboard up the composer sticks above it: --vv-below is the layout viewport under the visible
43
+ box (0 where the web view itself shrank). The home indicator is under the keyboard, so its inset goes. */
44
+ html.kb-chat .msg-split[data-show='chat'] .cc-composer { bottom: var(--vv-below, 0px); padding-bottom: 8px; }
24
45
  }
25
46
 
26
47
  /* ── the pinned row ───────────────────────────────────── */
@@ -62,6 +83,9 @@
62
83
  .cc-quote { display: block; width: 100%; text-align: left; margin: 0 0 8px; padding: 6px 10px; border-left: 3px solid var(--gold); background: var(--qopen); border-radius: 6px;
63
84
  font-size: 13px; color: var(--sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
85
  .cc-answer { margin-top: 6px; font-size: 15px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
86
+ /* 7.0.5: md() keeps an answer's lines as newlines, as the Messages reader shows them (.cx-card-body.md). Without this
87
+ every line and list of a finished answer ran together into one paragraph. The streaming partial is <p> and <br>. */
88
+ .cc-answer.md { white-space: pre-wrap; }
65
89
  .cc-answer p { margin: 0 0 0.6em; }
66
90
  .cc-answer p:last-child { margin-bottom: 0; }
67
91
  .cc-wait { color: var(--sub); font-size: 14px; }
@@ -88,6 +112,9 @@
88
112
  .cc-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; font-size: 12px; color: var(--tertiary); }
89
113
  .cc-model { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--sub); }
90
114
  .cc-model .field { width: auto; height: 34px; min-height: 34px; padding: 0 30px 0 10px; font-size: 13px; background-position: calc(100% - 17px) 14px, calc(100% - 12px) 14px; }
115
+ /* 7.0.2 (A5): a long model name never widens the page (16 px on touch makes the select as wide as its longest option). */
116
+ .cc-menus, .cc-model { min-width: 0; max-width: 100%; }
117
+ .cc-model .field { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
91
118
  .cc-hint { flex: 1; min-width: 180px; }
92
119
  .cc-note { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--hot); }
93
120
  /* A phone at 390 px: the box on its own row, the buttons under it, the menus under those. */
@@ -97,6 +124,18 @@
97
124
  .cc-buttons .cx-btn { flex: 1; justify-content: center; }
98
125
  .cc-buttons .cc-attach { flex: none; }
99
126
  }
127
+ /* 7.0.2 (A5): the composer the keyboard is for (cos-chat.js marks it .cc-kb) is compact while it is up (about 220 px
128
+ left folded, 200 unfolded): the box (3 lines at most), then two rows, clip with Ask and Drop, then Model and Effort
129
+ side by side, their names read from aria-label. Measured in WebKit at 382 px with an empty box: 171 px (was 252). The
130
+ camera (the clip's own menu offers Take Photo) and the fine print wait for the keyboard to close. */
131
+ html.kb-up .cc-composer.cc-kb { gap: 6px 8px; padding-top: 6px; }
132
+ html.kb-up .cc-kb .cc-input { max-height: 88px; }
133
+ html.kb-up .cc-composer.cc-kb .cc-hint, html.kb-up .cc-composer.cc-kb .cc-camera { display: none; }
134
+ html.kb-up .cc-kb .cc-foot { flex-wrap: nowrap; }
135
+ html.kb-up .cc-kb .cc-menus { display: flex; flex-wrap: nowrap; width: 100%; gap: 8px; }
136
+ html.kb-up .cc-kb .cc-model { flex: 1 1 0; }
137
+ html.kb-up .cc-kb .cc-model > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
138
+ html.kb-up .cc-kb .cc-model .field { width: 100%; }
100
139
 
101
140
  /* ── 6.99.12: the menus, the clip and the chips ───────── */
102
141
  .cc-menus { display: inline-flex; flex-wrap: wrap; gap: 6px 12px; }