@gotcos/glasses-server 7.0.31 → 7.0.34

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.
@@ -50,7 +50,10 @@
50
50
 
51
51
  import express, { Router, type NextFunction, type Request, type Response } from 'express'
52
52
  import { timingSafeTokenEqual } from '../lib/token-auth.js'
53
- import { turnEndAskStats } from '../lib/session-hooks-runtime.js'
53
+ import { turnEndAskStats, turnEndAsksForQueue } from '../lib/session-hooks-runtime.js'
54
+ import { buildQuestionQueue, type QuestionQueue } from '../lib/question-queue.js'
55
+ import { sessionTitleFor } from '../lib/session-title-cache.js'
56
+ import { waitingQuestionWordsEnabled } from '../lib/full-hold.js'
54
57
  import {
55
58
  ANSWERING_CLIENTS,
56
59
  CLIENT_LIVE_WINDOW_MS,
@@ -163,6 +166,9 @@ export interface SessionQuestionsRouterDeps {
163
166
  * rules: `isWebSessionAuthorized` in production). Absent: only the token opens the list, as before.
164
167
  */
165
168
  webSessionAuthorized?: (req: Request) => boolean
169
+ /** X1 (the one queue): the turn-end asks and the session titles the queue reads. Default to the runtime's and the list's. */
170
+ turnEndQueue?: () => ReturnType<typeof turnEndAsksForQueue>
171
+ titleFor?: (sessionId: string) => string | null
166
172
  /** 7.0.18 (X1): the turn-end ask counters (lib/turn-end-ask.ts), null while COS_TURN_END_ASKS is off. Defaults to the runtime's. */
167
173
  turnEndAskStats?: () => Record<string, number> | null
168
174
  /** Where a counted poll is recorded; `noteQuestionsPoll` in production. */
@@ -274,6 +280,26 @@ export function requestFromThisMac(
274
280
  return own.some(a => normalizeRemoteIp(String(a)).toLowerCase() === ip)
275
281
  }
276
282
 
283
+ /**
284
+ * X1 (the one queue, Miles 11/12 17:31): `queue` and `queueCount`, always present (whatever the full-hold switches),
285
+ * appended after every existing key so a client that reads the 7.0.31 keys reads exactly what it read before. A
286
+ * failure to build it never costs the rest of the response: the keys are then an empty queue.
287
+ */
288
+ export function questionQueueFields(deps: SessionQuestionsRouterDeps): QuestionQueue {
289
+ try {
290
+ const turnEnd = (deps.turnEndQueue ?? turnEndAsksForQueue)()
291
+ return buildQuestionQueue({
292
+ broker: deps.broker.queueSources(),
293
+ turnEnd: turnEnd.asks,
294
+ turnEndAnswered: turnEnd.answered,
295
+ titleFor: deps.titleFor ?? sessionTitleFor,
296
+ words: waitingQuestionWordsEnabled(process.env),
297
+ })
298
+ } catch {
299
+ return { queue: [], queueCount: 0 }
300
+ }
301
+ }
302
+
277
303
  function turnEndAskCounters(deps: SessionQuestionsRouterDeps): { turnEndAsks?: Record<string, number> } {
278
304
  let counters: Record<string, number> | null = null
279
305
  try { counters = (deps.turnEndAskStats ?? turnEndAskStats)() } catch { counters = null }
@@ -341,6 +367,7 @@ export function createSessionQuestionsRouter(deps: SessionQuestionsRouterDeps):
341
367
  // 7.0.18 (X1), additive and only while COS_TURN_END_ASKS is on: how many turn-end asks armed, were shown,
342
368
  // were answered, were overtaken by a new turn, or aged out. Counts only, never words.
343
369
  ...turnEndAskCounters(deps),
370
+ ...questionQueueFields(deps),
344
371
  })
345
372
  })
346
373
  router.post('/session-questions/:id/answer', (req: Request, res: Response) => {
@@ -350,7 +377,9 @@ export function createSessionQuestionsRouter(deps: SessionQuestionsRouterDeps):
350
377
  let expected = ''
351
378
  try { expected = deps.apiToken() } catch { expected = '' }
352
379
  const by = timingSafeTokenEqual(req.headers['x-cos-token'], expected) ? 'app' : 'web'
353
- const result = deps.broker.answer(String(req.params.id ?? ''), req.body, by)
380
+ // X1 (the one queue): the answering client may name itself (glasses, phone, mac_app) so others can say where.
381
+ const client = req.get('x-cos-client') ?? (typeof req.query.client === 'string' ? req.query.client : undefined)
382
+ const result = deps.broker.answer(String(req.params.id ?? ''), req.body, by, client)
354
383
  res.status(result.status).json(result.body)
355
384
  })
356
385
  // X1: the web door's confirm step for an approval (lib/permission-broker.ts `confirm`). The /api gate let the
@@ -12,6 +12,9 @@ import { refreshAfterInstall } from '../lib/provider-observe.js'
12
12
  import { Router } from 'express'
13
13
  import { installClaudeHooks, uninstallClaudeHooks } from '../lib/claude-hooks-installer.js'
14
14
  import { deskIdleSeconds, invalidateHookStatus, sessionHooksHealthFields, sessionSignalStore } from '../lib/session-hooks-runtime.js'
15
+ import { API_AUTH_BY_LOCAL } from '../lib/api-auth.js'
16
+ import { questionHoldMode } from '../lib/full-hold.js'
17
+ import { writeQuestionHoldSetting } from '../lib/question-hold-setting.js'
15
18
  import { workspaceFromCwd } from '../lib/claude-session-registry.js'
16
19
 
17
20
  /** The registry entrypoints a person sits at; everything else (`sdk-cli`, unknown) may be a job. */
@@ -69,6 +72,26 @@ export function createSessionHooksRouter(options: { port: number; codex?: Sessio
69
72
  res.json({ ok: true, ...sessionHooksHealthFields() })
70
73
  })
71
74
 
75
+ // X1, 7.0.34: the question hold's server-side setting, for Control's Settings row (Control 0.5.394). Body
76
+ // `{"mode":"on"|"off"|"default"}`; `default` removes the setting. The Mac app only: a browser session is refused
77
+ // (Control Web shows the state read-only). `COS_QUESTION_FULL_HOLD` in the environment wins over the setting, so
78
+ // while it is set nothing is written and the answer says so (409 `set_by_env`). Turning the hold on or off here
79
+ // changes what Reinstall hooks writes; the status's `state` then says whether a Reinstall is due.
80
+ router.put('/session-hooks/question-hold', (req, res) => {
81
+ res.set('Cache-Control', 'private, no-store')
82
+ if (res.locals[API_AUTH_BY_LOCAL] !== 'token') { res.status(403).json({ ok: false, reason: 'web_read_only' }); return }
83
+ const mode = req.body && typeof req.body === 'object' ? (req.body as { mode?: unknown }).mode : undefined
84
+ if (mode !== 'on' && mode !== 'off' && mode !== 'default') { res.status(400).json({ ok: false, reason: 'bad_mode' }); return }
85
+ if (questionHoldMode(process.env).source === 'env') {
86
+ res.status(409).json({ ok: false, reason: 'set_by_env', questionWait: sessionHooksHealthFields().sessionHooks.questionWait ?? null })
87
+ return
88
+ }
89
+ try { writeQuestionHoldSetting(mode) } catch { res.status(500).json({ ok: false, reason: 'write_failed' }); return }
90
+ invalidateHookStatus()
91
+ const hooks = sessionHooksHealthFields().sessionHooks
92
+ res.json({ ok: true, questionWait: hooks.questionWait ?? null, state: hooks.state })
93
+ })
94
+
72
95
  router.post('/session-hooks/install', (req, res) => {
73
96
  const dryRun = req.query.dryRun === '1' || (req.body && typeof req.body === 'object' && (req.body as { dryRun?: unknown }).dryRun === true)
74
97
  const result = installClaudeHooks({ port: options.port, deskIdleSeconds: deskIdleSeconds(), dryRun })
@@ -309,6 +309,7 @@ sessionsRouter.get('/sessions/today/all-messages', async (req, res) => {
309
309
  query: string
310
310
  text: string
311
311
  timestamp: number
312
+ queryTimestamp?: number
312
313
  chatIndex: number
313
314
  sessionId: string
314
315
  source: 'live'
@@ -341,6 +342,8 @@ sessionsRouter.get('/sessions/today/all-messages', async (req, res) => {
341
342
  query: ex.content,
342
343
  text: next.content,
343
344
  timestamp: next.timestamp,
345
+ // 7.0.33 A2: when the question was asked, beside when COS answered (timestamp), for a time under each.
346
+ ...(Number.isFinite(ex.timestamp) ? { queryTimestamp: ex.timestamp } : {}),
344
347
  chatIndex: -1,
345
348
  sessionId: session.id,
346
349
  source: 'live',
@@ -2,14 +2,14 @@
2
2
  "v": 1,
3
3
  "algorithm": "sha256",
4
4
  "files": {
5
- "/app.css": "bc5e29be841629261889aebfd439a3b126a50041c4e079c9866eac93a3552fe1",
6
- "/app.js": "50ac6ccfcd483496a411ae6569217e5cf9574305642b628ec703aa4a6ee8b797",
5
+ "/app.css": "f2d9c657580959c8a49ce5dd947c027aded0f4e69e4367b8247f092c0ccb0c8f",
6
+ "/app.js": "c5740309a27ba9b1513be466e687ec483311045c2f0088ffb3d5507afb208db2",
7
7
  "/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
8
- "/control.css": "f5b7a2e0f800cc07072581b7e817fcd2b54bdb1d91f4662a3cdc9d80a7bc32ff",
8
+ "/control.css": "6943c7a0573ae85e1fbdbda36e1765ea4ee5d5fd9205690253570f89b49d7fd3",
9
9
  "/fonts/DMSans.ttf": "8cd08d97e89c24d0aa92edd2f0f4c8ee6195eee9b7c9f154865a58b02f0c1c0d",
10
10
  "/fonts/Fraunces.ttf": "177ff6c0f14e5550a3c624247cd1189611d4eb65d000b14944c63d967958abbb",
11
11
  "/fonts/JetBrainsMono.ttf": "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
12
- "/index.html": "c3a88479e5b9efa323129c5a425dada70dc7f0f27a0c787c1bc225c661f20a7b",
12
+ "/index.html": "d9d1cede8b6ef2a3680f5b62e24fffcbf65e2eff3c33de31a69263f53d78520c",
13
13
  "/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
14
14
  "/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
15
15
  "/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
@@ -22,14 +22,14 @@
22
22
  "/panes/meetings.css": "635470e4d5f5665b046eab7382734c9772df569069c9b1eb10cfde6151014bfa",
23
23
  "/panes/meetings.js": "e057f4dae697138111a4c07e8ec6c2a0502b5d93023cf491460bc7bf20172c85",
24
24
  "/panes/memories.css": "36731d6fd21ad1516dfeccf34fe5b2d3d267d21e40b47704055a0685334b579e",
25
- "/panes/memories.js": "66341dd5126c40275979d1c40f8db72e1f8229ac9634a4bb72b70140db277f2a",
26
- "/panes/messages.js": "7262ad82b6337a610b11ffff0677663e9317d1fe537f9e4161ae3af1ddef35ef",
25
+ "/panes/memories.js": "47b9a06b329c3ec7c21902e6d8ec2fc7fe9013fffcfe936b54037be162c82958",
26
+ "/panes/messages.js": "5c43d171cb026ff98cc316821f631992288909570ec6abbd9bcc4dc1dc046bca",
27
27
  "/panes/preview.js": "79f1aecb27f48d584ed4cf37c7f4f423f0f9b45dd6c2fb24a9d88c72625d8680",
28
28
  "/panes/questions.css": "5c5a9b32ce32386b508ce326b17d40ecacbb0f2e049df7f4fe2a0009bd3b7908",
29
- "/panes/questions.js": "228e8cd0a9651a290cbb380db487af677a2fc7e93a64b1e33c9411cd8b013478",
29
+ "/panes/questions.js": "60115b6f5d5380d6dac1ceb50e6a56bd6d10f8d49fb64429fbe73d6890270111",
30
30
  "/panes/reader-media.js": "4cf892a855aa2d0dce6978e69296cd49b8036d8fc62f12773b3fcadc210b2ef0",
31
31
  "/panes/sessions.css": "654e0509ab2b699b9ea92b6d9e2f7522146df1f0b6355915bef03caaf63addc4",
32
- "/panes/settings.js": "315458c5911f31592853800418dde6d87e9e61bbe96f90c77450c94f87d01951",
32
+ "/panes/settings.js": "cc2ec51f397383be0b6f5078e6632c35dab8bf12ed3555df20c7cf2ec8ae8f63",
33
33
  "/panes/speakers.css": "49e70dae4266151b14b0057f2e43de169de225b8f3ac30343c81cf532735a487",
34
34
  "/panes/speakers.js": "e4fbcc147c44f8bf798e0694e0c8b152d5cde82dc46823641c8e8d37714b8fba",
35
35
  "/panes/threads.js": "45fd7b56480be53225e91193cfa4c41a4ee7167890f10560e439bc7d2461747f",
@@ -37,7 +37,7 @@
37
37
  "/panes/today-actions.js": "c767b62b21a9f1debffd6e4ff4558891222b573535de1d78849b602030bb09bd",
38
38
  "/panes/work.css": "da50355dac690f712d1f6331f70ee5eadbf5982d37508648ae7786afc99ac7b1",
39
39
  "/panes/work.js": "913c56084a6df463cc83358ce360da30d529466f1c583683fd3a4fd5eafe2d30",
40
- "/shell.js": "cc7d0d771712607d4ba49eb1cfc83c50c7fcae2203f1c386778fee43d1803a81",
40
+ "/shell.js": "9bef169c42d8130cbca8c012c3667a9b92cacaa8673b4cef47d6fc7cfc0096ef",
41
41
  "/theme.js": "e3e6422eaac43979d03149da114f5b83757b7361dd7b309cdf76be36a596044b",
42
42
  "/vendor/qrcode-encoder.js": "61d87cc7445cc66b862cff913de4571634746ea62bcd50d0f592be14f6d1ce46"
43
43
  }
@@ -225,6 +225,26 @@ select.field {
225
225
  .pet-caption { color: var(--sub); font-size: 14px; }
226
226
  /* A still pet (gallery pick, Cloud Puff, single image). Small pixel art scales without blur. */
227
227
  .pet-still { height: 140px; width: auto; max-width: 220px; object-fit: contain; }
228
+ /* 7.0.33 A13, Anand's mock (slide 9): in a phone's top bar, the pet in a small round button left of Ask, keeping its own
229
+ animation; a tap opens a card under it with the pet, its line, Change pet and Hide. The card floats, so it has the
230
+ mock's soft shadow and no border. */
231
+ .topbar .today-pet.in-bar { position: relative; min-height: 0; margin: 0 6px 0 auto; flex-direction: row; }
232
+ .today-pet.in-bar > .pet-btn { width: 36px; height: 36px; border-radius: 50%; justify-content: center; overflow: hidden;
233
+ background: color-mix(in srgb, var(--accent) 16%, var(--page)); border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent); }
234
+ .today-pet.in-bar > .pet-btn .pet-still { height: 30px; max-width: 30px; }
235
+ .today-pet.in-bar > .pet-btn .pet-sprite { zoom: 0.21; }
236
+ .today-pet.in-bar > .pet-btn .pet-caption, .today-pet.in-bar > .pet-change { display: none; }
237
+ .pet-card { position: absolute; top: calc(100% + 8px); right: -40px; width: min(250px, calc(100vw - 32px)); padding: 14px; text-align: left;
238
+ background: var(--panel); border-radius: 16px; box-shadow: 0 8px 28px rgba(40, 30, 15, 0.16); z-index: 30; }
239
+ .pet-card-head { display: flex; align-items: center; gap: 12px; width: 100%; background: none; border: 0; padding: 0; color: inherit; font: inherit; text-align: left; cursor: pointer; }
240
+ .pet-card-head[aria-disabled="true"] { cursor: default; }
241
+ .pet-card-pet { flex: none; width: 44px; display: grid; place-items: center; }
242
+ .pet-card-pet .pet-still { height: 48px; max-width: 44px; }
243
+ .pet-card-pet .pet-sprite { zoom: 0.34; }
244
+ .pet-card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
245
+ .pet-card-title { color: var(--ink); font-weight: 600; font-size: 16px; }
246
+ .pet-card-sub { color: var(--sub); font-size: 14px; line-height: 1.35; }
247
+ .pet-card-acts { display: flex; gap: 8px; margin-top: 12px; }
228
248
  .pet-still.pixel { image-rendering: pixelated; }
229
249
  .pet-change { background: none; border: 0; padding: 6px 10px; margin-top: 2px; color: var(--sub); font: inherit; font-size: 13px; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
230
250
  .pet-change:focus-visible { outline: 2px solid var(--sub); outline-offset: 2px; border-radius: 6px; }
@@ -492,6 +512,9 @@ select.field {
492
512
  .pane-head.with-ask { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; }
493
513
  .pane-head.with-ask .ask-bar { max-width: 400px; margin-top: 0; }
494
514
  .ask-bar .kbd { display: inline-block; }
515
+ /* 7.0.33 A9 (Anand round 2, slide 29): ⌘K is a keyboard's shortcut. A touch screen with no mouse (the open Duo, an
516
+ iPad held alone) does not show it. */
517
+ @media (hover: none) and (pointer: coarse) { .ask-bar .kbd { display: none; } }
495
518
  .pane-title { font-size: 44px; }
496
519
  .story { font-size: 26px; }
497
520
  .run-row { grid-template-columns: 118px minmax(0, 1fr); gap: 2px 14px; align-items: start; }
@@ -704,8 +727,8 @@ html { touch-action: manipulation; }
704
727
  /* The focus ring sits inside the button: the rail clips anything past its edge. */
705
728
  html.short-screen .rail :focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; border-radius: 12px; }
706
729
  /* One way to switch panes on a screen: the rail. On a list the top bar keeps the pane's name, without the caret and
707
- its menu (app.js makes the button inert); on a reader its Back moves to the rail and the left of the top bar is
708
- empty. */
709
- html.short-screen .section-btn .caret, html.short-screen #topbar-back { display: none !important; }
730
+ its menu (app.js makes the button inert). On a reader (7.0.33 A12) the top bar shows Back with the name of where it
731
+ goes, and stays put while you read; the rail's foot keeps its own Back within the thumb's reach. */
732
+ html.short-screen .section-btn .caret { display: none !important; }
710
733
  html.short-screen .section-btn { cursor: default; }
711
734
  }