@gotcos/glasses-server 6.99.33 → 7.0.18

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 (133) hide show
  1. package/.env.example +37 -0
  2. package/CHANGELOG.md +587 -0
  3. package/README.md +3 -1
  4. package/docs/web-key-verifier-contract.md +378 -0
  5. package/managed-runtime-contract.json +12 -1
  6. package/package.json +10 -3
  7. package/server/bootstrap.ts +10 -0
  8. package/server/index.ts +155 -30
  9. package/server/lib/agent-session-output-images.ts +627 -49
  10. package/server/lib/agent-session-recent.ts +321 -0
  11. package/server/lib/agent-session-store.ts +27 -14
  12. package/server/lib/agent-session-tool-images.ts +224 -0
  13. package/server/lib/agent-session-turns.ts +19 -2
  14. package/server/lib/api-auth.ts +43 -1
  15. package/server/lib/ask-work-plan.ts +3 -1
  16. package/server/lib/calendar-meetings.ts +70 -0
  17. package/server/lib/calendar-source.ts +536 -0
  18. package/server/lib/claude-hooks-installer.ts +111 -12
  19. package/server/lib/codex-bridge.ts +4 -1
  20. package/server/lib/codex-run-ledger.ts +29 -0
  21. package/server/lib/context-builder.ts +2 -1
  22. package/server/lib/conversation.ts +56 -28
  23. package/server/lib/cors-reject-log.ts +51 -0
  24. package/server/lib/disk-io-meter.ts +202 -0
  25. package/server/lib/drop-p2-runtime.ts +1 -1
  26. package/server/lib/drop-service.ts +47 -12
  27. package/server/lib/drop-store.ts +31 -5
  28. package/server/lib/event-loop-stall.ts +62 -0
  29. package/server/lib/full-hold-engine.ts +114 -0
  30. package/server/lib/full-hold.ts +124 -0
  31. package/server/lib/g2-turn-provenance.ts +128 -9
  32. package/server/lib/learning-needs-you.ts +24 -7
  33. package/server/lib/listener-startup.ts +18 -0
  34. package/server/lib/live-events.ts +533 -0
  35. package/server/lib/local-path-redact.ts +272 -0
  36. package/server/lib/meeting-ask-orientation.ts +40 -6
  37. package/server/lib/meeting-ask-runner.ts +30 -10
  38. package/server/lib/meeting-batch-transcribe.ts +32 -5
  39. package/server/lib/morning-brief-runtime.ts +4 -2
  40. package/server/lib/native-mac-caller.ts +29 -0
  41. package/server/lib/permission-broker.ts +443 -11
  42. package/server/lib/ping-engine.ts +1 -1
  43. package/server/lib/ping-sources.ts +11 -6
  44. package/server/lib/preview-content.ts +172 -0
  45. package/server/lib/preview-error-code.ts +10 -0
  46. package/server/lib/preview-folders-setting.ts +133 -0
  47. package/server/lib/preview-log-throttle.ts +18 -0
  48. package/server/lib/preview-thumbs.ts +139 -0
  49. package/server/lib/secret-path-guard.ts +83 -0
  50. package/server/lib/session-cache-writer.ts +8 -1
  51. package/server/lib/session-files-setting.ts +88 -0
  52. package/server/lib/session-hook-events.ts +23 -1
  53. package/server/lib/session-hooks-runtime.ts +34 -0
  54. package/server/lib/session-links.ts +280 -0
  55. package/server/lib/session-log.ts +313 -4
  56. package/server/lib/session-state-derive.ts +36 -0
  57. package/server/lib/session-stream-events.ts +25 -1
  58. package/server/lib/thread-turn-queue.ts +10 -0
  59. package/server/lib/today-brief.ts +640 -85
  60. package/server/lib/turn-end-ask.ts +320 -0
  61. package/server/lib/turn-footer.ts +3 -3
  62. package/server/lib/web-control-integrity.ts +160 -0
  63. package/server/lib/web-control-offnet-page.ts +58 -0
  64. package/server/lib/web-control-static.ts +53 -9
  65. package/server/lib/web-key-authority.ts +372 -0
  66. package/server/lib/web-key-envelope.ts +98 -0
  67. package/server/lib/web-key-handoff.ts +135 -0
  68. package/server/lib/web-key-origin.ts +21 -0
  69. package/server/lib/web-key-passkey.ts +155 -0
  70. package/server/lib/web-key-registry.ts +825 -0
  71. package/server/lib/web-key-signer-app.ts +152 -0
  72. package/server/lib/web-memories-bridge.ts +3 -1
  73. package/server/lib/web-session-auth.ts +133 -15
  74. package/server/lib/whisper-local.ts +93 -7
  75. package/server/lib/whisper-preview.ts +5 -3
  76. package/server/lib/work-activity.ts +56 -6
  77. package/server/lib/work-orchestrator/snapshot.ts +17 -3
  78. package/server/lib/work-review-runtime.ts +17 -4
  79. package/server/lib/work-session-activity.ts +260 -0
  80. package/server/lib/your-move-count.ts +9 -3
  81. package/server/routes/agent-session-bindings.ts +29 -1
  82. package/server/routes/agent-session-stream.ts +23 -0
  83. package/server/routes/agent-sessions.ts +146 -11
  84. package/server/routes/ask-work-plan.ts +4 -1
  85. package/server/routes/claude-sessions.ts +5 -1
  86. package/server/routes/drops.ts +7 -2
  87. package/server/routes/g2-turns.ts +35 -0
  88. package/server/routes/health.ts +46 -1
  89. package/server/routes/live-events.ts +334 -0
  90. package/server/routes/permission-broker.ts +95 -5
  91. package/server/routes/query-jobs.ts +3 -0
  92. package/server/routes/session-preview.ts +200 -0
  93. package/server/routes/sessions.ts +15 -24
  94. package/server/routes/thread-turn-queue.ts +223 -12
  95. package/server/routes/today-brief.ts +134 -44
  96. package/server/routes/web-control.ts +37 -4
  97. package/server/routes/web-key.ts +284 -0
  98. package/server/routes/welcome-context.ts +28 -4
  99. package/server/routes/work-board.ts +9 -2
  100. package/server/routes/work-orchestrator.ts +12 -7
  101. package/server/scripts/write-web-control-manifest.mjs +43 -0
  102. package/server/web-control/.manifest.json +44 -0
  103. package/server/web-control/app.css +76 -1
  104. package/server/web-control/app.js +2247 -98
  105. package/server/web-control/apple-touch-icon.png +0 -0
  106. package/server/web-control/control.css +25 -0
  107. package/server/web-control/index.html +21 -3
  108. package/server/web-control/memory-review.js +100 -0
  109. package/server/web-control/panes/cos-chat.css +94 -9
  110. package/server/web-control/panes/cos-chat.js +628 -44
  111. package/server/web-control/panes/meetings.css +18 -2
  112. package/server/web-control/panes/meetings.js +24 -8
  113. package/server/web-control/panes/memories.css +17 -2
  114. package/server/web-control/panes/memories.js +127 -42
  115. package/server/web-control/panes/messages.js +21 -2
  116. package/server/web-control/panes/preview.js +318 -0
  117. package/server/web-control/panes/questions.js +162 -21
  118. package/server/web-control/panes/reader-media.js +409 -23
  119. package/server/web-control/panes/sessions.css +32 -3
  120. package/server/web-control/panes/settings.js +97 -6
  121. package/server/web-control/panes/today-actions.css +46 -0
  122. package/server/web-control/panes/today-actions.js +119 -17
  123. package/server/web-control/panes/work.css +13 -0
  124. package/server/web-control/panes/work.js +188 -37
  125. package/server/web-control/shell.js +17 -14
  126. package/server/web-signer/.manifest.json +11 -0
  127. package/server/web-signer/index.html +74 -0
  128. package/server/web-signer/signer-proof.js +348 -0
  129. package/server/web-signer/signer-store.js +39 -0
  130. package/server/web-signer/signer.css +23 -0
  131. package/server/web-signer/signer.js +341 -0
  132. package/shared/web-approval-packets.golden.json +124 -0
  133. package/shared/web-turn-envelope.ts +271 -0
@@ -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)); }
@@ -11,6 +11,8 @@
11
11
  <meta name="apple-mobile-web-app-title" content="COS">
12
12
  <title>COS Control</title>
13
13
  <link rel="icon" href="/control/mark.svg" type="image/svg+xml">
14
+ <!-- iOS "Add to Home Screen" ignores SVG icons: without this, a saved Control Web shortcut is a grey letter. -->
15
+ <link rel="apple-touch-icon" href="/control/apple-touch-icon.png">
14
16
  <link rel="stylesheet" href="/control/app.css">
15
17
  <link rel="stylesheet" href="/control/control.css">
16
18
  <link rel="stylesheet" href="/control/panes/speakers.css">
@@ -55,7 +57,11 @@
55
57
  <div class="auth-card">
56
58
  <svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg>
57
59
  <h1 id="auth-title">Connect this browser</h1>
58
- <p>COS runs on your Mac. Sign in once and this browser stays connected for 7 days.</p>
60
+ <p id="auth-intro">COS runs on your Mac. Sign in once. You stay signed in while you use COS here, for up to 30 days.</p>
61
+ <div id="auth-offline" class="auth-offline hidden" role="status">
62
+ <p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.</p>
63
+ <button type="button" class="btn primary block" id="auth-retry">Try again</button>
64
+ </div>
59
65
  <form id="auth-form">
60
66
  <div id="auth-code-block">
61
67
  <label for="auth-code">Sign-in code</label>
@@ -114,6 +120,8 @@
114
120
  </header>
115
121
 
116
122
  <main class="workspace" id="workspace">
123
+ <!-- W1: the Mac not answering, and the sign-in ending soon (app.js "Mac reachability and sign-in lifetime") -->
124
+ <div id="conn-notice" class="conn-notice" hidden></div>
117
125
  <!-- SESSION QUESTIONS (panes/questions.js, 6.99.12): over every pane while one waits -->
118
126
  <div id="sq-host" class="sq-host" aria-live="polite" hidden></div>
119
127
  <!-- TODAY -->
@@ -123,6 +131,7 @@
123
131
  <h1 class="pane-title">Today</h1>
124
132
  <p id="today-date" class="today-date"></p>
125
133
  <p id="today-weather" class="today-weather hidden"></p>
134
+ <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
135
  </div>
127
136
  <button type="button" class="ask-bar" data-open-ask>
128
137
  <svg class="icon"><use href="#i-ask"/></svg>
@@ -135,6 +144,10 @@
135
144
  <div class="today-cols">
136
145
  <div class="today-main">
137
146
  <div id="today-story" class="story-skel" aria-live="polite"><div class="skeleton"></div><div class="skeleton" style="width:82%"></div><div class="skeleton" style="width:58%"></div></div>
147
+ <form id="today-ask" class="cc-composer today-ask hidden" autocomplete="off" aria-label="Ask about this summary">
148
+ <textarea class="field cc-input" id="today-ask-input" rows="1" maxlength="2000" placeholder="Ask about this or steer a session" aria-label="Ask about this summary"></textarea>
149
+ <div class="cc-buttons"><button type="submit" class="cx-btn primary cc-ask">Send</button></div>
150
+ </form>
138
151
  <section class="run-panel" aria-labelledby="run-title">
139
152
  <div class="panel-head">
140
153
  <h2 id="run-title" class="panel-title">Running<span id="run-count" class="count"></span></h2>
@@ -161,6 +174,8 @@
161
174
  <div class="panel-head"><h2 id="next-title" class="panel-title">Next up</h2></div>
162
175
  <div id="next-body" class="next-up"></div>
163
176
  <div id="next-actions" class="next-actions"></div>
177
+ <div id="next-first-prep" class="next-first-prep hidden"></div>
178
+ <div id="next-later" class="next-later hidden" aria-label="Later today"></div>
164
179
  </section>
165
180
  </div>
166
181
  </div>
@@ -181,7 +196,7 @@
181
196
  </header>
182
197
  <div class="cx-controls sx-views" id="sessions-views"></div>
183
198
  <div class="split sx-split" id="sessions-split">
184
- <div class="split-list">
199
+ <div class="split-list" id="sessions-list-col">
185
200
  <div class="sx-tools">
186
201
  <form class="cx-search sx-search" id="sessions-search-form" role="search">
187
202
  <input id="sessions-q" class="field" type="search" placeholder="Search titles, transcripts…" enterkeyhint="search" autocomplete="off" aria-label="Search titles, transcripts">
@@ -197,6 +212,7 @@
197
212
  <div id="sessions-hooks"></div>
198
213
  <div id="sessions-list"></div>
199
214
  </div>
215
+ <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
216
  <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
217
  </div>
202
218
  <div class="sx-usage hidden" id="sessions-usage"></div>
@@ -266,6 +282,7 @@
266
282
  <div id="toast" class="toast hidden" role="status"></div>
267
283
 
268
284
  <script src="/control/vendor/qrcode-encoder.js"></script>
285
+ <script src="/control/memory-review.js"></script>
269
286
  <script src="/control/app.js"></script>
270
287
  <script src="/control/shell.js"></script>
271
288
  <script src="/control/panes/cos-chat.js"></script>
@@ -281,7 +298,8 @@
281
298
  "settings": "/control/panes/settings.js",
282
299
  "memories": "/control/panes/memories.js",
283
300
  "todayActions": "/control/panes/today-actions.js",
284
- "readerMedia": "/control/panes/reader-media.js"
301
+ "readerMedia": "/control/panes/reader-media.js",
302
+ "preview": "/control/panes/preview.js"
285
303
  }</script>
286
304
  <!-- The Knowledge graph explorer, loaded by panes/memories.js when the graph first opens. -->
287
305
  <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 ───────────────────────────────────── */
@@ -35,10 +56,12 @@
35
56
  /* ── the chat ─────────────────────────────────────────── */
36
57
  .cc { --cc-you: color-mix(in srgb, var(--gold) 22%, var(--panel)); }
37
58
  .cc-head { display: flex; align-items: center; gap: 12px; padding: 14px 0 12px; border-bottom: 1px solid var(--hairline); flex-wrap: wrap; }
38
- .cc-head-text { min-width: 0; flex: 1; }
59
+ .cc-head-text { min-width: 0; flex: 1 1 16rem; }
60
+ /* W1 (open Duo, narrow chat column): the description was squeezed to four short lines beside All messages, Mark all read
61
+ and New thread. Below 16rem for the words, the links wrap beneath them. */
39
62
  .cc-title { font: 500 24px/1.1 var(--display); color: var(--ink); margin: 0; }
40
63
  .cc-sub { margin: 3px 0 0; font-size: 13px; line-height: 1.35; color: var(--sub); }
41
- .cc-head-acts { display: flex; gap: 14px; }
64
+ .cc-head-acts { display: flex; flex-wrap: wrap; gap: 4px 14px; }
42
65
  .cc-head-acts .link-btn { min-height: 36px; font-size: 14px; font-weight: 500; }
43
66
  .cc-avatar { flex: none; width: 30px; height: 30px; border-radius: 8px; background: #2b211a; color: var(--gold); display: grid; place-items: center; }
44
67
  .cc-avatar .icon { width: 17px; height: 17px; }
@@ -62,6 +85,9 @@
62
85
  .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
86
  font-size: 13px; color: var(--sub); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
87
  .cc-answer { margin-top: 6px; font-size: 15px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
88
+ /* 7.0.5: md() keeps an answer's lines as newlines, as the Messages reader shows them (.cx-card-body.md). Without this
89
+ every line and list of a finished answer ran together into one paragraph. The streaming partial is <p> and <br>. */
90
+ .cc-answer.md { white-space: pre-wrap; }
65
91
  .cc-answer p { margin: 0 0 0.6em; }
66
92
  .cc-answer p:last-child { margin-bottom: 0; }
67
93
  .cc-wait { color: var(--sub); font-size: 14px; }
@@ -75,12 +101,45 @@
75
101
  .cc-move .cc-choice { font-weight: 500; }
76
102
  .cc-fine { margin: 8px 0 0; font-size: 12.5px; color: var(--tertiary); }
77
103
  .cc-fine.warn { color: var(--hot); }
104
+ /* 7.0.9: a running answer's steps (its trail), newest last; once it ends, a quiet disclosure. A relayed message names the
105
+ session it went to. Every line wraps inside the card: the Duo folded is 382 px wide. */
106
+ .cc-steps-slot:empty { display: none; }
107
+ .cc-steps { list-style: none; margin: 8px 0 0; padding: 0 0 0 10px; border-left: 2px solid var(--hairline); display: grid; gap: 3px; min-width: 0; }
108
+ .cc-step { font-size: 13px; line-height: 1.4; color: var(--sub); overflow-wrap: anywhere; min-width: 0; }
109
+ .cc-step.prose { color: var(--ink); }
110
+ .cc-step.more { color: var(--tertiary); }
111
+ .cc-steps.live .cc-step:last-child { color: var(--ink); }
112
+ .cc-steps-done { margin-top: 8px; min-width: 0; }
113
+ .cc-steps-done > summary { cursor: pointer; min-height: 32px; display: flex; align-items: center; font-size: 13px; color: var(--tertiary); }
114
+ .cc-steps-done > summary::-webkit-details-marker { display: none; }
115
+ .cc-steps-done > summary::before { content: ''; width: 0; height: 0; margin-right: 8px; border-style: solid; border-width: 4px 0 4px 6px; border-color: transparent transparent transparent currentColor; transition: transform 160ms ease; }
116
+ .cc-steps-done[open] > summary::before { transform: rotate(90deg); }
117
+ @media (prefers-reduced-motion: reduce) { .cc-steps-done > summary::before { transition: none; } }
118
+ .cc-relay { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--hairline); min-width: 0; }
119
+ .cc-relay-line { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink); overflow-wrap: anywhere; }
120
+ .cc-relay-reply { margin: 6px 0 0; font-size: 14px; line-height: 1.45; color: var(--ink); overflow-wrap: anywhere; white-space: pre-wrap; }
121
+ /* 7.0.9: older questions, closed until opened, above the chat. */
122
+ .cc-archive { margin: 0 0 14px; padding: 0 0 10px; border-bottom: 1px solid var(--hairline); min-width: 0; }
123
+ .cc-archive > summary { cursor: pointer; min-height: 36px; display: flex; align-items: center; font-size: 13.5px; color: var(--sub); }
124
+ .cc-archive > summary::-webkit-details-marker { display: none; }
125
+ .cc-archive > summary::before { content: ''; width: 0; height: 0; margin-right: 8px; border-style: solid; border-width: 4px 0 4px 6px; border-color: transparent transparent transparent currentColor; transition: transform 160ms ease; }
126
+ .cc-archive[open] > summary::before { transform: rotate(90deg); }
127
+ @media (prefers-reduced-motion: reduce) { .cc-archive > summary::before { transition: none; } }
128
+ .cc-archive > .cc-fine { margin: 0 0 10px; }
129
+ .cc-relay-who { display: block; font-size: 12.5px; color: var(--tertiary); margin-bottom: 2px; }
78
130
  .cc-steer { display: flex; gap: 8px; margin-top: 10px; }
79
131
  .cc-steer .field { flex: 1; min-width: 0; height: 36px; }
80
132
 
81
133
  /* ── the composer: two visible buttons, no key needed ─── */
82
134
  .cc-composer { position: sticky; bottom: 0; z-index: 5; background: var(--page); border-top: 1px solid var(--hairline); padding: 10px 0 calc(10px + var(--safe-bottom, env(safe-area-inset-bottom, 0px)));
83
- display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 10px; align-items: end; }
135
+ display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: flex-end; }
136
+ /* W1 (Miles 2026-11-09, open Duo: "Ask COS, or drop a thought, a task, a ..." wrapped and clipped beside the camera,
137
+ clip, Ask and Drop in the three-column split). The row follows the composer's own width, not the screen: the words need
138
+ 19rem (every composer placeholder is 36 characters or fewer), and when the buttons do not fit beside that they move to
139
+ the row below, at its right. Panes are resizable, so no breakpoint can know this. Same rule as the reader's box. */
140
+ .cc-composer > .cc-input { flex: 1 1 19rem; min-width: 0; }
141
+ .cc-composer > .cc-buttons { flex: none; margin-left: auto; }
142
+ .cc-composer > .cc-chips, .cc-composer > .cc-foot, .cc-composer > .cc-note { flex: 1 1 100%; min-width: 0; }
84
143
  .cc-input { min-height: 44px; max-height: 140px; padding: 11px 12px; font-size: 16px; border-radius: 12px; }
85
144
  .cc-buttons { display: flex; gap: 8px; }
86
145
  .cc-buttons .cx-btn { height: 44px; padding: 0 16px; font-size: 15px; border-radius: 12px; }
@@ -88,15 +147,30 @@
88
147
  .cc-foot { grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; font-size: 12px; color: var(--tertiary); }
89
148
  .cc-model { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--sub); }
90
149
  .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; }
150
+ /* 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). */
151
+ .cc-menus, .cc-model { min-width: 0; max-width: 100%; }
152
+ .cc-model .field { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
91
153
  .cc-hint { flex: 1; min-width: 180px; }
92
154
  .cc-note { grid-column: 1 / -1; margin: 0; font-size: 13px; color: var(--hot); }
93
155
  /* A phone at 390 px: the box on its own row, the buttons under it, the menus under those. */
94
156
  @media (max-width: 480px) {
95
- .cc-composer { grid-template-columns: minmax(0, 1fr); }
96
- .cc-buttons { justify-content: flex-end; }
157
+ .cc-composer > .cc-input { flex-basis: 100%; }
158
+ .cc-composer > .cc-buttons { flex: 1 1 100%; justify-content: flex-end; }
97
159
  .cc-buttons .cx-btn { flex: 1; justify-content: center; }
98
160
  .cc-buttons .cc-attach { flex: none; }
99
161
  }
162
+ /* 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
163
+ left folded, 200 unfolded): the box (3 lines at most), then two rows, clip with Ask and Drop, then Model and Effort
164
+ side by side, their names read from aria-label. Measured in WebKit at 382 px with an empty box: 171 px (was 252). The
165
+ camera (the clip's own menu offers Take Photo) and the fine print wait for the keyboard to close. */
166
+ html.kb-up .cc-composer.cc-kb { gap: 6px 8px; padding-top: 6px; }
167
+ html.kb-up .cc-kb .cc-input { max-height: 88px; }
168
+ html.kb-up .cc-composer.cc-kb .cc-hint, html.kb-up .cc-composer.cc-kb .cc-camera { display: none; }
169
+ html.kb-up .cc-kb .cc-foot { flex-wrap: nowrap; }
170
+ html.kb-up .cc-kb .cc-menus { display: flex; flex-wrap: nowrap; width: 100%; gap: 8px; }
171
+ html.kb-up .cc-kb .cc-model { flex: 1 1 0; }
172
+ 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; }
173
+ html.kb-up .cc-kb .cc-model .field { width: 100%; }
100
174
 
101
175
  /* ── 6.99.12: the menus, the clip and the chips ───────── */
102
176
  .cc-menus { display: inline-flex; flex-wrap: wrap; gap: 6px 12px; }
@@ -210,13 +284,24 @@
210
284
  their own above the words, Take photo and the clip beside Fork and Send, and the limits in plain words under the chips. */
211
285
  .att-composer { flex-wrap: wrap; }
212
286
  .att-composer .att-files { flex: 1 1 100%; padding: 2px 0 4px; }
287
+ /* W1 (Miles 2026-11-09 21:24, closed Duo screenshot: "Continue this session" wrapped onto two lines, the second
288
+ clipped). The phone's real width did not trip the 430 px rule below, so the row now follows the space the box itself
289
+ has: the words need at least 19rem (the longest placeholder, "Message this session (not an answer)", is about 277 px
290
+ in the kit font), and when the buttons do not fit beside that they move to the row below, at its right. Wide panes
291
+ keep the one-row layout. Measured in WebKit at 382, 440, 520 and 951 px: lanes/W1_evidence/duo_layout_*. */
292
+ .att-composer > textarea { flex: 1 1 19rem; min-width: 0; }
293
+ .att-composer > .composer-actions { margin-left: auto; }
213
294
  /* 6.99.30 (canary): the folded Sessions reader squeezed the box to about 101 px beside Fork, the clip and Send. Under
214
295
  430 px the words get their own full row and the buttons sit on the row below, at its right. */
215
296
  @media (max-width: 429px) {
216
297
  .att-composer > textarea { flex: 1 1 100%; }
217
- .att-composer > .composer-actions { margin-left: auto; }
218
298
  }
219
299
  .att-composer .att-files.hidden { display: none; }
300
+ .phone-approval-packet { flex: 1 1 100%; min-width: 0; }
301
+ .phone-approval-packet label { display: block; font-size: 13px; margin-bottom: 4px; }
302
+ .phone-approval-packet select { width: 100%; max-width: 100%; font: inherit; color: inherit; background: var(--card, transparent); }
303
+ .phone-approval-packet pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.4 ui-monospace, monospace; max-height: 11rem; overflow-y: auto; margin: 6px 0; }
304
+ .composer-actions:has([data-phone-offer]) { flex-wrap: wrap; }
220
305
  .att-clip, .ta-clip { display: inline-flex; gap: 6px; }
221
306
  .att-clip:empty { display: none; }
222
307
  .att-clip .cc-attach { height: 38px; width: 42px; border-radius: var(--radius-sm); }