@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.
- package/.env.example +37 -0
- package/CHANGELOG.md +587 -0
- package/README.md +3 -1
- package/docs/web-key-verifier-contract.md +378 -0
- package/managed-runtime-contract.json +12 -1
- package/package.json +10 -3
- package/server/bootstrap.ts +10 -0
- package/server/index.ts +155 -30
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-recent.ts +321 -0
- package/server/lib/agent-session-store.ts +27 -14
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +43 -1
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-meetings.ts +70 -0
- package/server/lib/calendar-source.ts +536 -0
- package/server/lib/claude-hooks-installer.ts +111 -12
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/conversation.ts +56 -28
- package/server/lib/cors-reject-log.ts +51 -0
- package/server/lib/disk-io-meter.ts +202 -0
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-service.ts +47 -12
- package/server/lib/drop-store.ts +31 -5
- package/server/lib/event-loop-stall.ts +62 -0
- package/server/lib/full-hold-engine.ts +114 -0
- package/server/lib/full-hold.ts +124 -0
- package/server/lib/g2-turn-provenance.ts +128 -9
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/listener-startup.ts +18 -0
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/meeting-batch-transcribe.ts +32 -5
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/permission-broker.ts +443 -11
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +83 -0
- package/server/lib/session-cache-writer.ts +8 -1
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-hook-events.ts +23 -1
- package/server/lib/session-hooks-runtime.ts +34 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/session-log.ts +313 -4
- package/server/lib/session-state-derive.ts +36 -0
- package/server/lib/session-stream-events.ts +25 -1
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +640 -85
- package/server/lib/turn-end-ask.ts +320 -0
- package/server/lib/turn-footer.ts +3 -3
- package/server/lib/web-control-integrity.ts +160 -0
- package/server/lib/web-control-offnet-page.ts +58 -0
- package/server/lib/web-control-static.ts +53 -9
- package/server/lib/web-key-authority.ts +372 -0
- package/server/lib/web-key-envelope.ts +98 -0
- package/server/lib/web-key-handoff.ts +135 -0
- package/server/lib/web-key-origin.ts +21 -0
- package/server/lib/web-key-passkey.ts +155 -0
- package/server/lib/web-key-registry.ts +825 -0
- package/server/lib/web-key-signer-app.ts +152 -0
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/web-session-auth.ts +133 -15
- package/server/lib/whisper-local.ts +93 -7
- package/server/lib/whisper-preview.ts +5 -3
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-orchestrator/snapshot.ts +17 -3
- package/server/lib/work-review-runtime.ts +17 -4
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +29 -1
- package/server/routes/agent-session-stream.ts +23 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/drops.ts +7 -2
- package/server/routes/g2-turns.ts +35 -0
- package/server/routes/health.ts +46 -1
- package/server/routes/live-events.ts +334 -0
- package/server/routes/permission-broker.ts +95 -5
- package/server/routes/query-jobs.ts +3 -0
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/sessions.ts +15 -24
- package/server/routes/thread-turn-queue.ts +223 -12
- package/server/routes/today-brief.ts +134 -44
- package/server/routes/web-control.ts +37 -4
- package/server/routes/web-key.ts +284 -0
- package/server/routes/welcome-context.ts +28 -4
- package/server/routes/work-board.ts +9 -2
- package/server/routes/work-orchestrator.ts +12 -7
- package/server/scripts/write-web-control-manifest.mjs +43 -0
- package/server/web-control/.manifest.json +44 -0
- package/server/web-control/app.css +76 -1
- package/server/web-control/app.js +2247 -98
- package/server/web-control/apple-touch-icon.png +0 -0
- package/server/web-control/control.css +25 -0
- package/server/web-control/index.html +21 -3
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +94 -9
- package/server/web-control/panes/cos-chat.js +628 -44
- package/server/web-control/panes/meetings.css +18 -2
- package/server/web-control/panes/meetings.js +24 -8
- package/server/web-control/panes/memories.css +17 -2
- package/server/web-control/panes/memories.js +127 -42
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.js +162 -21
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +32 -3
- package/server/web-control/panes/settings.js +97 -6
- package/server/web-control/panes/today-actions.css +46 -0
- package/server/web-control/panes/today-actions.js +119 -17
- package/server/web-control/panes/work.css +13 -0
- package/server/web-control/panes/work.js +188 -37
- package/server/web-control/shell.js +17 -14
- package/server/web-signer/.manifest.json +11 -0
- package/server/web-signer/index.html +74 -0
- package/server/web-signer/signer-proof.js +348 -0
- package/server/web-signer/signer-store.js +39 -0
- package/server/web-signer/signer.css +23 -0
- package/server/web-signer/signer.js +341 -0
- package/shared/web-approval-packets.golden.json +124 -0
- package/shared/web-turn-envelope.ts +271 -0
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { Router } from 'express'
|
|
6
6
|
import { callPython, pythonBridgeAvailable } from '../lib/python-bridge.js'
|
|
7
|
+
import { calendarToday, readCalendar } from '../lib/calendar-source.js'
|
|
8
|
+
import { isCalendarHold } from '../lib/calendar-meetings.js'
|
|
7
9
|
import { parsePrepOccurrence } from '../lib/prep-store.js'
|
|
8
10
|
import type { PrepOccurrence } from '../../shared/prep.js'
|
|
9
11
|
|
|
@@ -21,7 +23,8 @@ type WeatherPayload = { temp: string; desc: string; location: string }
|
|
|
21
23
|
* provider, calendar id, event id or external id, and start. The bridge sends no event ids today, so it is absent.
|
|
22
24
|
* It lets a client find a Prep draft for this exact event; it never binds one, and nothing is inferred from the title.
|
|
23
25
|
*/
|
|
24
|
-
|
|
26
|
+
// `hold` (11/09): present, true, only when the title is a hold or status block (calendar-meetings.ts isCalendarHold).
|
|
27
|
+
type NextEventPayload = { title: string; time: string; inMinutes?: number; occurrence?: PrepOccurrence; hold?: true }
|
|
25
28
|
|
|
26
29
|
/** The bridge event's provider identity, or undefined. snake_case (calendar_context.py) or camelCase is accepted. */
|
|
27
30
|
export function bridgeOccurrence(evt: Record<string, unknown> | null | undefined): PrepOccurrence | undefined {
|
|
@@ -34,7 +37,8 @@ export function bridgeOccurrence(evt: Record<string, unknown> | null | undefined
|
|
|
34
37
|
calendarId: pick('calendar_id', 'calendarId'),
|
|
35
38
|
eventId: pick('event_id', 'eventId') ?? null,
|
|
36
39
|
externalId: pick('external_id', 'externalId') ?? null,
|
|
37
|
-
|
|
40
|
+
// occurrence_start (EventKit's original occurrence date) first, so a moved occurrence keeps its draft.
|
|
41
|
+
startsAt: pick('occurrence_start', 'start_time', 'start', 'startsAt'),
|
|
38
42
|
endsAt: pick('end_time', 'end', 'endsAt') ?? null,
|
|
39
43
|
}) ?? undefined
|
|
40
44
|
} catch { return undefined }
|
|
@@ -327,7 +331,8 @@ export async function fetchNextEvent(): Promise<{ event: NextEventPayload | null
|
|
|
327
331
|
}
|
|
328
332
|
|
|
329
333
|
try {
|
|
330
|
-
const
|
|
334
|
+
const calRead = await readCalendar()
|
|
335
|
+
const cal = calRead.data as {
|
|
331
336
|
today_remaining?: Array<{
|
|
332
337
|
title?: string
|
|
333
338
|
start_time?: string
|
|
@@ -338,10 +343,14 @@ export async function fetchNextEvent(): Promise<{ event: NextEventPayload | null
|
|
|
338
343
|
minutes_until_next?: number
|
|
339
344
|
} | null
|
|
340
345
|
|
|
341
|
-
|
|
346
|
+
// The EventKit snapshot is a real read without the bridge; 'none' (denied or expired) never is.
|
|
347
|
+
const sourceReady = calRead.source === 'bridge' ? pythonBridgeAvailable() : calRead.source !== 'none'
|
|
348
|
+
const read = calendarAnswerWasRead(sourceReady, cal)
|
|
342
349
|
const remaining = cal?.today_remaining ?? []
|
|
343
350
|
const meaningful = remaining.find((evt) => {
|
|
344
351
|
if (evt.is_all_day) return false
|
|
352
|
+
// Snapshot rows say whether they may become next/prep (declined, shared-calendar travel, holidays).
|
|
353
|
+
if ((evt as { prep_worthy?: unknown }).prep_worthy === false) return false
|
|
345
354
|
return !isGenericNoiseEvent(evt.title || '')
|
|
346
355
|
})
|
|
347
356
|
|
|
@@ -357,6 +366,7 @@ export async function fetchNextEvent(): Promise<{ event: NextEventPayload | null
|
|
|
357
366
|
time: meaningful.start_time || meaningful.start || '',
|
|
358
367
|
...(minsUntil != null ? { inMinutes: minsUntil } : {}),
|
|
359
368
|
...(occurrence ? { occurrence } : {}),
|
|
369
|
+
...(isCalendarHold(meaningful.title) ? { hold: true as const } : {}),
|
|
360
370
|
}
|
|
361
371
|
} else if (cal?.next_event && !isGenericNoiseEvent(cal.next_event.title || '')) {
|
|
362
372
|
const occurrence = bridgeOccurrence(cal.next_event as Record<string, unknown>)
|
|
@@ -365,6 +375,7 @@ export async function fetchNextEvent(): Promise<{ event: NextEventPayload | null
|
|
|
365
375
|
time: cal.next_event.start_time || '',
|
|
366
376
|
...(cal.minutes_until_next != null ? { inMinutes: cal.minutes_until_next } : {}),
|
|
367
377
|
...(occurrence ? { occurrence } : {}),
|
|
378
|
+
...(isCalendarHold(cal.next_event.title) ? { hold: true as const } : {}),
|
|
368
379
|
}
|
|
369
380
|
} else {
|
|
370
381
|
cachedNextEvent = null
|
|
@@ -394,6 +405,19 @@ export function _resetWelcomeContextCachesForTests(): void {
|
|
|
394
405
|
lastFailureLogAt.clear()
|
|
395
406
|
}
|
|
396
407
|
|
|
408
|
+
/**
|
|
409
|
+
* PH plan P1: Today's calendar for Control Web and desktop. Behind the /api token gate. Selected calendars only, the
|
|
410
|
+
* next 5 events (Today round 2, was 3), plus `endedRecently` (ended within 2 h). `connected: false` means "Calendar not connected", never an empty day.
|
|
411
|
+
*/
|
|
412
|
+
welcomeContextRouter.get('/calendar/today', async (_req, res) => {
|
|
413
|
+
try {
|
|
414
|
+
res.set('Cache-Control', 'no-store')
|
|
415
|
+
res.json(await calendarToday())
|
|
416
|
+
} catch {
|
|
417
|
+
res.status(503).json({ error: 'calendar_unavailable' })
|
|
418
|
+
}
|
|
419
|
+
})
|
|
420
|
+
|
|
397
421
|
welcomeContextRouter.get('/welcome-context', async (req, res) => {
|
|
398
422
|
const lat = parseCoord(req.query.lat, 'lat')
|
|
399
423
|
const lon = parseCoord(req.query.lon, 'lon')
|
|
@@ -6,6 +6,7 @@ import { listBoard, listBoardWithDates, workBoardCapabilities, setTaskWorkStage,
|
|
|
6
6
|
import { createWorkBoardReader, WORK_BOARD_READ_LIMITS, WorkBoardTimeoutError, type WorkBoardReader } from '../lib/work-board-reader.js'
|
|
7
7
|
import type { MeetingDescriptor } from '../lib/work-review-store.js'
|
|
8
8
|
import { isSafeDomainName } from '../lib/domains.js'
|
|
9
|
+
import { workSessionActivity, waitingSessionIds } from '../lib/work-session-activity.js'
|
|
9
10
|
import { resolveSavedMeetingDetail } from './meetings.js'
|
|
10
11
|
import type { MeetingDetail } from '../lib/meeting-store.js'
|
|
11
12
|
|
|
@@ -28,11 +29,13 @@ export interface WorkBoardDependencies {
|
|
|
28
29
|
requests: () => Pick<WorkActivityCapabilities, 'requests' | 'requestsInbox' | 'requestsConsumerSeenAt'>
|
|
29
30
|
/** 6.59.0: the bounded, shared board read (5 s). Defaults to one over `list`. */
|
|
30
31
|
readBoard?: WorkBoardReader<TaskBoardRow>
|
|
32
|
+
/** 7.0.1: each linked session's newest activity (lib/work-session-activity.ts), for the open list's supersede rule. */
|
|
33
|
+
sessionActivity: (sessionIds: readonly string[]) => Promise<ReadonlyMap<string, number>>
|
|
31
34
|
now: () => number
|
|
32
35
|
}
|
|
33
36
|
const defaults: WorkBoardDependencies = { journal: () => readWorkJournal(), list: listBoard, capabilities: workBoardCapabilities,
|
|
34
37
|
stage: setTaskWorkStage, link: linkTaskMeeting, edit: editWorkTask, capture: captureWorkItem, captureCapabilities: workIntakeCapabilities, resolveMeeting: resolveSavedMeetingDetail,
|
|
35
|
-
requests: () => ({ requests: 0, requestsInbox: 0, requestsConsumerSeenAt: null }), now: () => Date.now() }
|
|
38
|
+
requests: () => ({ requests: 0, requestsInbox: 0, requestsConsumerSeenAt: null }), sessionActivity: ids => workSessionActivity(ids), now: () => Date.now() }
|
|
36
39
|
/** How old a board read the activity and open-work glances accept (the reader shares one read between them). */
|
|
37
40
|
export const WORK_GLANCE_BOARD_AGE_MS = 10_000
|
|
38
41
|
|
|
@@ -82,7 +85,11 @@ export function createWorkBoardRouter(overrides: Partial<WorkBoardDependencies>
|
|
|
82
85
|
if (!journal.available) return res.json({ version: 1, available: false, reason: journal.reason, capabilities, items: [], total: 0, truncated: false })
|
|
83
86
|
const rows = await glanceBoard('open work')
|
|
84
87
|
if (!rows) return res.json({ version: 1, available: false, reason: 'Work board is unavailable. Refresh before trying again.', capabilities, items: [], total: 0, truncated: false })
|
|
85
|
-
|
|
88
|
+
// 7.0.1: a waiting item whose session moved on since its report is not waiting. Only those sessions are read; a
|
|
89
|
+
// read that fails keeps the list as it was.
|
|
90
|
+
const now = deps.now(), first = projectOpenWork(journal, rows, now), waiting = waitingSessionIds(first.items)
|
|
91
|
+
const sessionActivityMs = waiting.length ? await deps.sessionActivity(waiting).catch(() => undefined) : undefined
|
|
92
|
+
return res.json({ version: 1, available: true, capabilities, ...(sessionActivityMs?.size ? projectOpenWork(journal, rows, now, { sessionActivityMs }) : first) })
|
|
86
93
|
})
|
|
87
94
|
router.get('/work-board', async (_req, res) => {
|
|
88
95
|
try {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 10 minutes (`refresh=1`: 2 minutes). No timer reads usage unless COS_WORK_USAGE_POLL=1.
|
|
14
14
|
*/
|
|
15
15
|
import { Router } from 'express'
|
|
16
|
-
import {
|
|
16
|
+
import { parseSnapshotReport, type ControlSnapshot } from '../lib/work-orchestrator/snapshot.js'
|
|
17
17
|
import type { OrchestratorInput } from '../lib/work-orchestrator/input.js'
|
|
18
18
|
import type { UsageMonitor, UsageView } from '../lib/work-orchestrator/usage.js'
|
|
19
19
|
import { orchestratorInputFor, orchestratorSnapshots, orchestratorUsage } from '../lib/work-orchestrator/runtime.js'
|
|
@@ -67,8 +67,8 @@ export function createWorkOrchestratorRouter(given: Partial<WorkOrchestratorDeps
|
|
|
67
67
|
return res.status(400).json({ error: 'invalid_snapshot_binding' })
|
|
68
68
|
}
|
|
69
69
|
if (Buffer.byteLength(JSON.stringify(body)) > ORCHESTRATOR_BODY_BYTES) return res.status(413).json({ error: 'snapshot_too_large' })
|
|
70
|
-
let snapshot: ControlSnapshot
|
|
71
|
-
try { snapshot =
|
|
70
|
+
let snapshot: ControlSnapshot, droppedLiveRows: number
|
|
71
|
+
try { ({ snapshot, droppedLiveRows } = parseSnapshotReport(body.snapshot, deps.now())) }
|
|
72
72
|
catch { return res.status(400).json({ error: 'invalid_snapshot' }) }
|
|
73
73
|
const binding = () => {
|
|
74
74
|
const id = deps.serverInstanceId()
|
|
@@ -90,7 +90,9 @@ export function createWorkOrchestratorRouter(given: Partial<WorkOrchestratorDeps
|
|
|
90
90
|
// This is the same paired-client evidence cache as /input, not execution authority. parseSnapshot
|
|
91
91
|
// retains only its known fields; arbitrary automatic/forkable/capability fields never enter the cache.
|
|
92
92
|
deps.putSnapshot(snapshot)
|
|
93
|
-
|
|
93
|
+
if (droppedLiveRows) console.warn(`[work-orchestrator] snapshot work=${body.workID} droppedLiveRows=${droppedLiveRows}`)
|
|
94
|
+
return res.json({ protocolVersion: 1, accepted: true, workID: body.workID, revision: body.revision, serverInstanceId,
|
|
95
|
+
...(droppedLiveRows ? { droppedLiveRows } : {}) })
|
|
94
96
|
} catch (error) {
|
|
95
97
|
if (res.destroyed) return
|
|
96
98
|
const known = error instanceof AskWorkPlanError
|
|
@@ -105,16 +107,19 @@ export function createWorkOrchestratorRouter(given: Partial<WorkOrchestratorDeps
|
|
|
105
107
|
|| !Array.isArray(meetings) || meetings.length > 20 || !meetings.every(m => typeof m === 'string' && MEETING_ID.test(m))) {
|
|
106
108
|
return res.status(400).json({ error: 'invalid_orchestrator_input' })
|
|
107
109
|
}
|
|
110
|
+
// 7.0.13: live rows the snapshot left out (a short or malformed session id, or text over a bound). Reported in the
|
|
111
|
+
// answer and the log line only when a snapshot came with this request and something was left out.
|
|
112
|
+
let droppedLiveRows = 0
|
|
108
113
|
if ('snapshot' in body) {
|
|
109
114
|
if (Buffer.byteLength(JSON.stringify(body.snapshot)) > ORCHESTRATOR_BODY_BYTES) return res.status(413).json({ error: 'snapshot_too_large' })
|
|
110
115
|
let snapshot: ControlSnapshot
|
|
111
|
-
try { snapshot =
|
|
116
|
+
try { ({ snapshot, droppedLiveRows } = parseSnapshotReport(body.snapshot, deps.now())) } catch (e) { return res.status(400).json({ error: 'invalid_snapshot', reason: (e as Error).message }) }
|
|
112
117
|
deps.putSnapshot(snapshot)
|
|
113
118
|
}
|
|
114
119
|
try {
|
|
115
120
|
const input = await deps.input(body.workID, meetings as string[])
|
|
116
|
-
console.log(`[work-orchestrator] input work=${body.workID} rows=${input.rows.length} discovered=${input.discovered} leftOut=${input.leftOut.length} snapshotAge=${input.snapshotAgeSec ?? '-'}s liveAge=${input.liveListAgeSec ?? '-'}s scan=${input.logScanComplete ? 'complete' : 'partial'}`)
|
|
117
|
-
return res.json(input)
|
|
121
|
+
console.log(`[work-orchestrator] input work=${body.workID} rows=${input.rows.length} discovered=${input.discovered} leftOut=${input.leftOut.length} snapshotAge=${input.snapshotAgeSec ?? '-'}s liveAge=${input.liveListAgeSec ?? '-'}s scan=${input.logScanComplete ? 'complete' : 'partial'}${droppedLiveRows ? ` droppedLiveRows=${droppedLiveRows}` : ''}`)
|
|
122
|
+
return res.json(droppedLiveRows ? { ...input, droppedLiveRows } : input)
|
|
118
123
|
} catch (e) {
|
|
119
124
|
console.warn(`[work-orchestrator] input failed for ${body.workID}: ${(e as Error).message}`)
|
|
120
125
|
return res.status(503).json({ error: 'orchestrator_input_unavailable' })
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 6.99.31 (Phase 6): the pack step. Writes server/web-control/.manifest.json, the sha256 of every
|
|
3
|
+
// file COS Control Web ships (every regular non-dot file except *.test.ts, which never ship). With
|
|
4
|
+
// COS_WEB_CONTROL_INTEGRITY on, the server serves /control only while the files match it
|
|
5
|
+
// (server/lib/web-control-integrity.ts, whose computeWebControlManifest must agree with this byte
|
|
6
|
+
// for byte; web-control-integrity.test.ts pins that). Run by `npm pack` / `npm publish` (prepack).
|
|
7
|
+
// Writes one file, prints its path and the file count. Never reads anything outside web-control/.
|
|
8
|
+
import { createHash } from 'node:crypto'
|
|
9
|
+
import { readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
export function manifestFor(dir) {
|
|
14
|
+
const files = {}
|
|
15
|
+
const walk = (d, base) => {
|
|
16
|
+
let names
|
|
17
|
+
try { names = readdirSync(d) } catch { return }
|
|
18
|
+
for (const name of names.sort()) {
|
|
19
|
+
if (name.startsWith('.')) continue
|
|
20
|
+
const file = join(d, name)
|
|
21
|
+
let st
|
|
22
|
+
try { st = statSync(file) } catch { continue }
|
|
23
|
+
const urlPath = `${base}/${name}`
|
|
24
|
+
if (st.isDirectory()) walk(file, urlPath)
|
|
25
|
+
else if (st.isFile() && !name.endsWith('.test.ts')) files[urlPath] = createHash('sha256').update(readFileSync(file)).digest('hex')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
walk(dir, '')
|
|
29
|
+
return { v: 1, algorithm: 'sha256', files }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]
|
|
33
|
+
if (isMain) {
|
|
34
|
+
const dir = process.argv[2] ?? join(dirname(fileURLToPath(import.meta.url)), '..', 'web-control')
|
|
35
|
+
const manifest = manifestFor(dir)
|
|
36
|
+
const out = join(dir, '.manifest.json')
|
|
37
|
+
const tmp = `${out}.tmp`
|
|
38
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o644 })
|
|
39
|
+
renameSync(tmp, out)
|
|
40
|
+
// stderr, never stdout: it runs as `prepack`, and `npm pack --json` callers (the release packager, package-contents and
|
|
41
|
+
// launcher-contract tests) parse stdout as JSON (7.0.9 full suite).
|
|
42
|
+
console.error(`[web-control-manifest] ${out} (${Object.keys(manifest.files).length} files)`)
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"v": 1,
|
|
3
|
+
"algorithm": "sha256",
|
|
4
|
+
"files": {
|
|
5
|
+
"/app.css": "0d3ebbf2ca167bc9ef6ab8efef41618f514cc4f505ae588642d36935249697af",
|
|
6
|
+
"/app.js": "3c489b154588908178de5b70b18ee8184ec9256e99f774177b9e35ceb27eb362",
|
|
7
|
+
"/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
|
|
8
|
+
"/control.css": "e7b287aa4acc1b3895a9ce61512fe449c555b6335a7145684fd78108ae008c75",
|
|
9
|
+
"/fonts/DMSans.ttf": "8cd08d97e89c24d0aa92edd2f0f4c8ee6195eee9b7c9f154865a58b02f0c1c0d",
|
|
10
|
+
"/fonts/Fraunces.ttf": "177ff6c0f14e5550a3c624247cd1189611d4eb65d000b14944c63d967958abbb",
|
|
11
|
+
"/fonts/JetBrainsMono.ttf": "48715a42ec242c21e9f02692891e147d022299a52e48d5e413e1a942193ffeda",
|
|
12
|
+
"/index.html": "26ee9cd1f68829ae5f47dcc7a0c2e45a40d28c4ee65d0537f0677b13ea727267",
|
|
13
|
+
"/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
|
|
14
|
+
"/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
|
|
15
|
+
"/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
|
|
16
|
+
"/memories/graph-explorer.js": "98bde3df25f42022b098d047d2c8e7a22b972c1d817a18a24a0ab075d77871d3",
|
|
17
|
+
"/memories/memory-workspace.js": "013f6c8c72c3474768f8c374edccde0e3656fc243e56528e258c3c29acca64f5",
|
|
18
|
+
"/memory-review.js": "f27fe519f7ad7eccc3b44124fae724aa51e2eb3cb060d2f1795600e0e41b074f",
|
|
19
|
+
"/panes/cos-chat.css": "29a5d22d0f02d0707e65711d151ad7c8ac0ab4bfeb30ab04b1f9d9fcf7d457b8",
|
|
20
|
+
"/panes/cos-chat.js": "0005562525cab6353ef8fc002d7960b30a8bf9fc9a9a1059c0123c6cf343366e",
|
|
21
|
+
"/panes/dictation.js": "e9434b353a1a3ab3f21d4cef5b2d5f927a733f6af5f58285fda6a78472761f91",
|
|
22
|
+
"/panes/meetings.css": "3ad4d0c5d59bd9f21c5ac9e234437cc0c81b381bd98ca4634b147efba5105a53",
|
|
23
|
+
"/panes/meetings.js": "e3839cd7d52c98510ca232e41d3a336806d740b4a1381eab1a367d56a765f925",
|
|
24
|
+
"/panes/memories.css": "36731d6fd21ad1516dfeccf34fe5b2d3d267d21e40b47704055a0685334b579e",
|
|
25
|
+
"/panes/memories.js": "28461f953ad0bf9d13287039594f2ce7ea29a46366e249240fa50f4b0f3b8c31",
|
|
26
|
+
"/panes/messages.js": "be7a7b294f67a43fcb4cc518da987d2da2990484cb677ada810afa93af25a8a5",
|
|
27
|
+
"/panes/preview.js": "79f1aecb27f48d584ed4cf37c7f4f423f0f9b45dd6c2fb24a9d88c72625d8680",
|
|
28
|
+
"/panes/questions.css": "5c5a9b32ce32386b508ce326b17d40ecacbb0f2e049df7f4fe2a0009bd3b7908",
|
|
29
|
+
"/panes/questions.js": "228e8cd0a9651a290cbb380db487af677a2fc7e93a64b1e33c9411cd8b013478",
|
|
30
|
+
"/panes/reader-media.js": "4cf892a855aa2d0dce6978e69296cd49b8036d8fc62f12773b3fcadc210b2ef0",
|
|
31
|
+
"/panes/sessions.css": "fa2e714e7b03459e12407f7125ab0e1eb7d12a4cc9341a2442dc73900f5a3081",
|
|
32
|
+
"/panes/settings.js": "5c46c951fdcc2fc942486ec4b468b7e0bb0350a0a56a38d429948fc5147ab2f4",
|
|
33
|
+
"/panes/speakers.css": "49e70dae4266151b14b0057f2e43de169de225b8f3ac30343c81cf532735a487",
|
|
34
|
+
"/panes/speakers.js": "8132c9244fb3965de802680840d2985978571988b9b4462b347d079330251bc5",
|
|
35
|
+
"/panes/threads.js": "45fd7b56480be53225e91193cfa4c41a4ee7167890f10560e439bc7d2461747f",
|
|
36
|
+
"/panes/today-actions.css": "c82f95b2aaf09802b15d47b24eccdb5c974afcab38ff5f861b3a8d8630b60869",
|
|
37
|
+
"/panes/today-actions.js": "4fbc51dc624f9e4cdc85db89905f019a8a5961e4984e8733e0525c71cf0e8657",
|
|
38
|
+
"/panes/work.css": "da50355dac690f712d1f6331f70ee5eadbf5982d37508648ae7786afc99ac7b1",
|
|
39
|
+
"/panes/work.js": "0425f53fba98ca34496486d914c3b03f95de3c2a651dc0c88a2a20bc4f6407cd",
|
|
40
|
+
"/shell.js": "f1cc6a52c78e01ddba89ff27c6bd0e8f8bd8f29bdfcbb5150ee4ed66a8128d9d",
|
|
41
|
+
"/theme.js": "700453683539c9efcde014ef1eb088c39478dad54f1ef53fa5e2dee9b36f0a3f",
|
|
42
|
+
"/vendor/qrcode-encoder.js": "61d87cc7445cc66b862cff913de4571634746ea62bcd50d0f592be14f6d1ce46"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -186,6 +186,11 @@ select.field {
|
|
|
186
186
|
padding: 4px calc(var(--gutter) + var(--safe-right)) calc(var(--safe-bottom) + 96px) calc(var(--gutter) + var(--safe-left));
|
|
187
187
|
}
|
|
188
188
|
.pane { max-width: 880px; }
|
|
189
|
+
/* 7.0.2 (A2, the Duo audit): the COS chat route ends at its composer (panes/messages.js sets ws-chat). The 96 px above
|
|
190
|
+
(and the 40 px from 600 px up) held room for a bottom bar that is gone; under the sticky composer it was blank page
|
|
191
|
+
that a focused box scrolled to and the closed keyboard left on screen. Two columns from 860 px keep a small margin. */
|
|
192
|
+
.workspace.ws-chat { padding-bottom: 0; }
|
|
193
|
+
@media (min-width: 860px) { .workspace.ws-chat { padding-bottom: calc(var(--safe-bottom) + 16px); } }
|
|
189
194
|
.pane-head { margin: 8px 0 18px; }
|
|
190
195
|
.pane-title { font: 500 40px/1.02 var(--display); letter-spacing: -0.015em; margin: 0; }
|
|
191
196
|
.pane-sub { color: var(--sub); margin: 6px 0 0; font-size: 15px; }
|
|
@@ -249,6 +254,8 @@ select.field {
|
|
|
249
254
|
.story { font: 400 24px/1.28 var(--display); letter-spacing: -0.005em; margin: 26px 2px; text-wrap: pretty; }
|
|
250
255
|
/* The Today summary's quiet age line (app.js briefWhen). */
|
|
251
256
|
.story-when { display: block; margin-top: 10px; font: 400 14px/1.35 var(--body); letter-spacing: 0; color: var(--sub); }
|
|
257
|
+
/* Today round 2: "Ask about this" under the summary, the COS chat composer (panes/cos-chat.css) laid flat in the page. */
|
|
258
|
+
.today-ask.cc-composer { position: static; background: transparent; border-top: 0; padding: 0 0 14px; }
|
|
252
259
|
.story-skel { display: grid; gap: 12px; margin: 30px 0; }
|
|
253
260
|
.story-skel .skeleton { height: 22px; }
|
|
254
261
|
|
|
@@ -288,6 +295,12 @@ select.field {
|
|
|
288
295
|
.next-up .when { font: 500 24px/1 var(--display); white-space: nowrap; }
|
|
289
296
|
.next-up .what { font-weight: 600; }
|
|
290
297
|
.next-up .sub { color: var(--sub); font-size: 14px; }
|
|
298
|
+
.next-later { padding: 0 16px 14px; font-size: 14px; }
|
|
299
|
+
.next-later .later-row { display: flex; gap: 12px; padding: 3px 0; min-width: 0; }
|
|
300
|
+
.next-later .later-when { color: var(--sub); font-variant-numeric: tabular-nums; white-space: nowrap; min-width: 4.5em; }
|
|
301
|
+
.next-later .later-what { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
302
|
+
.next-later .later-asof { color: var(--sub); margin-top: 4px; }
|
|
303
|
+
.next-later .later-more { margin-top: 4px; padding: 3px 0; }
|
|
291
304
|
|
|
292
305
|
/* ── Lists (sessions, meetings, messages) ────────────────── */
|
|
293
306
|
.list { background: var(--panel); border: 1px solid var(--panel-border); border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--shadow); }
|
|
@@ -405,6 +418,11 @@ select.field {
|
|
|
405
418
|
.menu-host { display: flex; align-items: center; gap: 8px; color: var(--sub); font-size: 14px; margin: 6px 4px 12px; }
|
|
406
419
|
.sheet-head { display: flex; gap: 10px; align-items: center; }
|
|
407
420
|
.sheet-head .field { flex: 1; }
|
|
421
|
+
/* W1 (Miles 2026-11-09 21:24, closed Duo): the Ask field sat beside the camera, the clip and Cancel and its placeholder
|
|
422
|
+
was cut off. It needs at least 15rem; when the buttons do not fit beside that, they go to the row below, at its right. */
|
|
423
|
+
#ask-form { flex-wrap: wrap; }
|
|
424
|
+
#ask-form > .field { flex: 1 1 15rem; min-width: 0; }
|
|
425
|
+
#ask-form > .ask-clip { margin-left: auto; }
|
|
408
426
|
@keyframes fade { from { opacity: 0; } }
|
|
409
427
|
@keyframes drop { from { transform: translateY(-24px); opacity: 0.5; } }
|
|
410
428
|
|
|
@@ -507,6 +525,28 @@ select.field {
|
|
|
507
525
|
.pane.reading .pane-head.with-ask { display: flex; }
|
|
508
526
|
}
|
|
509
527
|
|
|
528
|
+
/* Duo tables: the divider between a list and its reader (app.js splitKit, W.splitKit), for Memories and Messages.
|
|
529
|
+
Hidden everywhere; each pane shows it only inside its own two-column breakpoint (panes/memories.css 760 px,
|
|
530
|
+
panes/cos-chat.css 860 px), so a phone never sees it. Sessions keeps its own copy of these rules (panes/sessions.css). */
|
|
531
|
+
.split-handle, .split-full-btn { display: none; }
|
|
532
|
+
.split-handle {
|
|
533
|
+
justify-content: center; align-items: center; position: sticky; top: calc(var(--safe-top) + 12px); align-self: start;
|
|
534
|
+
height: calc(100dvh - var(--safe-top) - 24px); cursor: col-resize; touch-action: none; user-select: none; -webkit-user-select: none;
|
|
535
|
+
-webkit-touch-callout: none; outline: none; z-index: 2;
|
|
536
|
+
}
|
|
537
|
+
/* The line is the split's own gap; the grip shows where to take it. */
|
|
538
|
+
.split-handle::after { content: ''; width: 4px; height: 36px; border-radius: 2px; background: var(--panel-border); transition: background 120ms ease, height 120ms ease; }
|
|
539
|
+
.split-handle:hover::after, .split-handle:focus-visible::after, .split-dragging > .split-handle::after { background: var(--accent); height: 52px; }
|
|
540
|
+
.split-handle:focus-visible { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent); border-radius: 6px; }
|
|
541
|
+
/* A finger gets at least a 44 px target, 13 px over each pane's edge; a mouse keeps the divider's own column. */
|
|
542
|
+
@media (pointer: coarse) { .split-handle::before { content: ''; position: absolute; top: 0; bottom: 0; left: -13px; right: -13px; } }
|
|
543
|
+
/* While dragging, nothing in the panes selects or takes the pointer. */
|
|
544
|
+
.split-dragging { user-select: none; -webkit-user-select: none; cursor: col-resize; }
|
|
545
|
+
.split-dragging > :not(.split-handle) { pointer-events: none; }
|
|
546
|
+
.split-full-btn { margin-left: auto; flex: none; }
|
|
547
|
+
@media (pointer: coarse) { .split-full-btn { min-height: 44px; } }
|
|
548
|
+
@media (prefers-reduced-motion: reduce) { .split-handle::after { transition: none; } }
|
|
549
|
+
|
|
510
550
|
/* ── Desktop ─────────────────────────────────────────────── */
|
|
511
551
|
@media (min-width: 1100px) and (min-height: 721px) {
|
|
512
552
|
:root { --gutter: 40px; }
|
|
@@ -550,11 +590,20 @@ select.field {
|
|
|
550
590
|
@media (any-pointer: coarse) {
|
|
551
591
|
input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']):not([type='file']):not([type='button']):not([type='submit']):not([type='reset']):not([type='image']):not([type='hidden']),
|
|
552
592
|
select, textarea { font-size: 16px !important; }
|
|
593
|
+
/* 7.0.17 (Miles 2026-11-10, Duo web app zooming): the same floor for any element that types without being a field.
|
|
594
|
+
None exists in Control Web today (work.js only names the selector); this keeps a future editor from zooming. */
|
|
595
|
+
[contenteditable]:not([contenteditable='false']) { font-size: 16px !important; }
|
|
553
596
|
}
|
|
597
|
+
/* 7.0.17 (Miles 2026-11-10: "a lot of zooming in and out as I'm opening and closing keyboard"; the Duo's own breadcrumb
|
|
598
|
+
showed vvs=2.12 with the keyboard down, a double-tap zoom fitting a column). manipulation keeps pan and pinch and
|
|
599
|
+
turns off double-tap zoom. On the root only: touch-action is the intersection down the tree, so the splitter's
|
|
600
|
+
none and the charts' pan-y stay exactly as strict as they were. */
|
|
601
|
+
html { touch-action: manipulation; }
|
|
554
602
|
/* Folded phones: every button and field is at least 44 px tall (design QA: .cx-btn came in six heights from 30 px). A
|
|
555
603
|
minimum only, so it grows the compact ones and leaves the taller ones; the per-pane phone heights that fought it are
|
|
556
604
|
gone. */
|
|
557
|
-
|
|
605
|
+
/* 7.0.2 (A8): a short touch screen at any width too (the unfolded Duo, 951x539 in Comet, is touch first). */
|
|
606
|
+
@media (max-width: 599px), (pointer: coarse) and (max-height: 720px) {
|
|
558
607
|
.cx-btn, .field { min-height: 44px !important; }
|
|
559
608
|
}
|
|
560
609
|
/* Touch screens: the small controls the canary measured under 44 px (nav 30, switches 30, a chip's remove and Retry 36,
|
|
@@ -566,6 +615,32 @@ select.field {
|
|
|
566
615
|
.cc-chip-x, .cc-attach, .cc-camera { min-width: 44px !important; min-height: 44px !important; }
|
|
567
616
|
.cx-switch button::before { content: ''; position: absolute; inset: -7px -4px; }
|
|
568
617
|
}
|
|
618
|
+
/* 7.0.2 (A8): a short touch screen 600 px wide or more (the unfolded Duo, 951x539) keeps the phone Ask sheet: full width
|
|
619
|
+
from the top, no centred desktop card with 8 % of a 539 px screen spent above it (app.js isPhone says phone too). */
|
|
620
|
+
@media (min-width: 600px) and (pointer: coarse) and (max-height: 720px) {
|
|
621
|
+
.sheet-backdrop { padding-top: 0; }
|
|
622
|
+
.sheet { max-width: none; border-radius: 0 0 20px 20px; padding: calc(var(--safe-top) + 10px) var(--gutter) 14px; max-height: calc(100dvh - 90px); box-shadow: none; }
|
|
623
|
+
}
|
|
569
624
|
/* 6.99.30 (QA): an element with the hidden attribute is hidden, whatever display its classes give it ("Retry image"
|
|
570
625
|
showed under loaded images: .link-btn's inline-flex beat [hidden]). */
|
|
571
626
|
[hidden] { display: none !important; }
|
|
627
|
+
|
|
628
|
+
/* W1: the Mac not answering, and the sign-in ending soon (app.js "Mac reachability and sign-in lifetime"). */
|
|
629
|
+
.conn-notice { display: grid; gap: 8px; margin: 0 0 12px; }
|
|
630
|
+
.conn-notice[hidden] { display: none; }
|
|
631
|
+
.conn-line { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 12px 14px; border-radius: var(--radius-sm); border: 1px solid var(--panel-border); background: var(--panel); color: var(--ink); font-size: 15px; line-height: 1.4; }
|
|
632
|
+
.conn-line strong { font-weight: 600; }
|
|
633
|
+
.conn-line .conn-sub { color: var(--sub); flex: 1 1 220px; min-width: 0; }
|
|
634
|
+
.conn-line .btn { margin: 0; }
|
|
635
|
+
.auth-offline p { color: var(--sub); }
|
|
636
|
+
.meaning-consent .row-actions { padding: 0 16px 14px; }
|
|
637
|
+
/* W1 A5: a question waiting on the Mac's own prompt, at the top of the session reader. */
|
|
638
|
+
.sx-mac-q { margin: 8px 0 12px; padding: 12px 14px; border-radius: var(--radius-sm); border: 1px solid var(--panel-border); background: var(--panel); }
|
|
639
|
+
.sx-mac-q[hidden] { display: none; }
|
|
640
|
+
.sx-mac-q .sx-mac-q-title { margin: 0 0 6px; }
|
|
641
|
+
/* 7.0.18: a turn that ended asking him: the choices as buttons in a row that wraps under its own width (at most two,
|
|
642
|
+
each label at most 40 characters), so the closed Duo stacks them and the open Duo keeps them side by side. Labels wrap
|
|
643
|
+
inside a button rather than cut. */
|
|
644
|
+
.sx-te-choices, .ta-te-choices { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 6px; }
|
|
645
|
+
.sx-te-choices .cx-btn, .ta-te-choices .cx-btn { flex: 1 1 12rem; min-height: 44px; white-space: normal; text-align: center; line-height: 1.25; }
|
|
646
|
+
.sx-mac-q .sx-note { margin: 6px 0 0; }
|