@gotcos/glasses-server 6.99.29 → 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 (80) hide show
  1. package/.env.example +18 -0
  2. package/CHANGELOG.md +475 -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 +79 -9
  56. package/server/web-control/app.js +1543 -65
  57. package/server/web-control/control.css +37 -6
  58. package/server/web-control/index.html +8 -2
  59. package/server/web-control/memory-review.js +100 -0
  60. package/server/web-control/panes/cos-chat.css +51 -6
  61. package/server/web-control/panes/cos-chat.js +268 -16
  62. package/server/web-control/panes/meetings.css +23 -4
  63. package/server/web-control/panes/meetings.js +25 -9
  64. package/server/web-control/panes/memories.css +18 -3
  65. package/server/web-control/panes/memories.js +130 -50
  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.css +20 -4
  69. package/server/web-control/panes/questions.js +127 -26
  70. package/server/web-control/panes/reader-media.js +409 -23
  71. package/server/web-control/panes/sessions.css +50 -10
  72. package/server/web-control/panes/settings.js +96 -5
  73. package/server/web-control/panes/speakers.css +3 -5
  74. package/server/web-control/panes/speakers.js +7 -4
  75. package/server/web-control/panes/threads.js +3 -2
  76. package/server/web-control/panes/today-actions.css +49 -18
  77. package/server/web-control/panes/today-actions.js +217 -50
  78. package/server/web-control/panes/work.css +11 -1
  79. package/server/web-control/panes/work.js +10 -2
  80. package/server/web-control/shell.js +20 -14
@@ -6,7 +6,8 @@
6
6
 
7
7
  :root {
8
8
  --tint: var(--accent);
9
- --tertiary: #8b8378;
9
+ /* 6.99.30 (design QA): #8b8378 measured 3.29:1 on the light page; this reads at 4.5:1 or better. Dark is unchanged. */
10
+ --tertiary: #6e675d;
10
11
  --gold: #b98a3e;
11
12
  --green: #2f7d4f;
12
13
  --amber: #a8641c;
@@ -64,8 +65,9 @@
64
65
  .rail-settings { min-height: 40px; }
65
66
  }
66
67
 
67
- /* Full rail (Control's at window width 1100 and up): name and story beside the glyph. */
68
- @media (min-width: 1100px) {
68
+ /* Full rail (Control's at window width 1100 and up, on a screen tall enough for it: the same clause as app.css's desktop
69
+ step, so a wide short window keeps the icon rail and its short-screen sizes). */
70
+ @media (min-width: 1100px) and (min-height: 721px) {
69
71
  .app-shell { grid-template-columns: 248px minmax(0, 1fr); }
70
72
  .rail { align-items: stretch; padding: calc(var(--safe-top) + 18px) 12px 14px; gap: 2px; }
71
73
  .rail-head { flex-direction: row; align-items: center; gap: 8px; padding: 0 8px 14px; }
@@ -145,7 +147,7 @@
145
147
  .cx-btn:disabled { opacity: 0.45; }
146
148
  .cx-btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
147
149
  .cx-btn.quiet { border-color: var(--panel-border); }
148
- @media (max-width: 599px) { .cx-btn { height: 36px; font-size: 14.5px; } }
150
+ @media (max-width: 599px) { .cx-btn { font-size: 14.5px; } }
149
151
 
150
152
  /* ── text switch (COSBrand: words with a 2pt gold rule, never pills) ── */
151
153
  .cx-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin: 12px 0 10px; }
@@ -161,7 +163,6 @@
161
163
  .cx-check input { width: 16px; height: 16px; accent-color: var(--gold); }
162
164
  @media (max-width: 599px) {
163
165
  .cx-search { max-width: none; flex-basis: 100%; order: 3; }
164
- .cx-search .field { min-height: 40px; font-size: 16px; }
165
166
  .cx-switch button { font-size: 15px; padding: 8px 0 9px; }
166
167
  }
167
168
 
@@ -280,7 +281,7 @@
280
281
  .cx-chip { display: inline-flex; align-items: center; gap: 4px; height: 28px; padding: 0 4px 0 10px; border-radius: 14px; background: var(--panel); border: 1px solid var(--panel-border); font-size: 13px; color: var(--ink); }
281
282
  .cx-chip button, .cx-x { width: 24px; height: 24px; border-radius: 12px; color: var(--sub); font-size: 15px; line-height: 1; display: grid; place-items: center; }
282
283
  .cx-chip.more { padding-right: 10px; color: var(--sub); }
283
- @media (max-width: 599px) { .cx-inline-form .field { min-height: 40px; font-size: 16px; } .cx-chip { height: 32px; } }
284
+ @media (max-width: 599px) { .cx-chip { height: 32px; } }
284
285
 
285
286
  /* ── Settings (Control's hub categories) ──────────────── */
286
287
  .cx-set-cat { margin-top: 8px; }
@@ -302,6 +303,14 @@ button.cx-set-row:hover { background: var(--qopen); }
302
303
  .cx-set-row .cx-set-time { width: 148px; }
303
304
  .cx-set-row .cx-set-day { width: 150px; padding-right: 36px; background-position: calc(100% - 20px) 17px, calc(100% - 15px) 17px; }
304
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; }
305
314
  @media (max-width: 599px) {
306
315
  .cx-set-row:has(.cx-set-steer) { flex-wrap: wrap; }
307
316
  .cx-set-row .cx-set-steer { width: 100%; min-width: 0; }
@@ -323,3 +332,25 @@ html, body { overscroll-behavior-y: none; }
323
332
  .intake-meeting { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
324
333
  .intake-n { font: 600 12px/1.6 var(--mono); color: var(--sub); }
325
334
  .intake-foot { margin: 6px 0 0; font-size: 12.5px; color: var(--tertiary); }
335
+
336
+ /* 6.99.30 (design QA, the unfolded Duo at 1430 px): Settings and Dictation are forms and short reads, not boards, so
337
+ their content stops at 720 px instead of running lines to 137 characters; prose lines stop at about 68 characters. */
338
+ #pane-settings, #pane-dictation { max-width: 720px; }
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)); }
@@ -122,6 +122,8 @@
122
122
  <div>
123
123
  <h1 class="pane-title">Today</h1>
124
124
  <p id="today-date" class="today-date"></p>
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>
125
127
  </div>
126
128
  <button type="button" class="ask-bar" data-open-ask>
127
129
  <svg class="icon"><use href="#i-ask"/></svg>
@@ -160,6 +162,7 @@
160
162
  <div class="panel-head"><h2 id="next-title" class="panel-title">Next up</h2></div>
161
163
  <div id="next-body" class="next-up"></div>
162
164
  <div id="next-actions" class="next-actions"></div>
165
+ <div id="next-later" class="next-later hidden" aria-label="Later today"></div>
163
166
  </section>
164
167
  </div>
165
168
  </div>
@@ -180,7 +183,7 @@
180
183
  </header>
181
184
  <div class="cx-controls sx-views" id="sessions-views"></div>
182
185
  <div class="split sx-split" id="sessions-split">
183
- <div class="split-list">
186
+ <div class="split-list" id="sessions-list-col">
184
187
  <div class="sx-tools">
185
188
  <form class="cx-search sx-search" id="sessions-search-form" role="search">
186
189
  <input id="sessions-q" class="field" type="search" placeholder="Search titles, transcripts…" enterkeyhint="search" autocomplete="off" aria-label="Search titles, transcripts">
@@ -196,6 +199,7 @@
196
199
  <div id="sessions-hooks"></div>
197
200
  <div id="sessions-list"></div>
198
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>
199
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>
200
204
  </div>
201
205
  <div class="sx-usage hidden" id="sessions-usage"></div>
@@ -265,6 +269,7 @@
265
269
  <div id="toast" class="toast hidden" role="status"></div>
266
270
 
267
271
  <script src="/control/vendor/qrcode-encoder.js"></script>
272
+ <script src="/control/memory-review.js"></script>
268
273
  <script src="/control/app.js"></script>
269
274
  <script src="/control/shell.js"></script>
270
275
  <script src="/control/panes/cos-chat.js"></script>
@@ -280,7 +285,8 @@
280
285
  "settings": "/control/panes/settings.js",
281
286
  "memories": "/control/panes/memories.js",
282
287
  "todayActions": "/control/panes/today-actions.js",
283
- "readerMedia": "/control/panes/reader-media.js"
288
+ "readerMedia": "/control/panes/reader-media.js",
289
+ "preview": "/control/panes/preview.js"
284
290
  }</script>
285
291
  <!-- The Knowledge graph explorer, loaded by panes/memories.js when the graph first opens. -->
286
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)); 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
- .msg-split[data-show='chat'] .cc { min-height: calc(100vh - 64px - var(--safe-top, 0px)); display: flex; flex-direction: column; }
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; }
@@ -190,7 +229,7 @@
190
229
  .cc-prep-add:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
191
230
  .cc-prep-acts .cx-btn { height: 40px; }
192
231
  @media (max-width: 480px) {
193
- .cc-prep-acts .cx-btn { height: 44px; min-width: 44px; }
232
+ .cc-prep-acts .cx-btn { min-width: 44px; }
194
233
  .cc-prep-head-acts button { min-height: 44px; }
195
234
  }
196
235
  .cc-prep-note { margin: 0; font-size: 13px; }
@@ -210,9 +249,15 @@
210
249
  their own above the words, Take photo and the clip beside Fork and Send, and the limits in plain words under the chips. */
211
250
  .att-composer { flex-wrap: wrap; }
212
251
  .att-composer .att-files { flex: 1 1 100%; padding: 2px 0 4px; }
252
+ /* 6.99.30 (canary): the folded Sessions reader squeezed the box to about 101 px beside Fork, the clip and Send. Under
253
+ 430 px the words get their own full row and the buttons sit on the row below, at its right. */
254
+ @media (max-width: 429px) {
255
+ .att-composer > textarea { flex: 1 1 100%; }
256
+ .att-composer > .composer-actions { margin-left: auto; }
257
+ }
213
258
  .att-composer .att-files.hidden { display: none; }
214
259
  .att-clip, .ta-clip { display: inline-flex; gap: 6px; }
215
260
  .att-clip:empty { display: none; }
216
261
  .att-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }
217
262
  .att-composer.dragging { outline: 2px dashed var(--accent); outline-offset: 4px; }
218
- .cc-chips .att-limit { flex-basis: 100%; margin: 0 2px; font-size: 12.5px; line-height: 1.35; color: var(--sub); }
263
+ .cc-chips .att-limit { flex-basis: 100%; min-width: 0; max-width: 100%; margin: 0 2px; font-size: 12.5px; line-height: 1.35; color: var(--sub); white-space: normal; overflow-wrap: anywhere; }