@gotcos/glasses-server 7.0.30 → 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.
- package/.env.example +10 -8
- package/CHANGELOG.md +207 -0
- package/bin/hooks/cos-session-hook +3 -2
- package/docs/pairing-fixture.json +1 -1
- package/package.json +3 -1
- package/server/index.ts +4 -2
- package/server/lib/api-auth.ts +3 -2
- package/server/lib/archive.ts +4 -2
- package/server/lib/claude-hooks-installer.ts +101 -15
- package/server/lib/codex-hooks-installer.ts +4 -1
- package/server/lib/full-hold.ts +61 -5
- package/server/lib/glasses-pairing.ts +2 -1
- package/server/lib/permission-broker.ts +130 -14
- package/server/lib/question-hold-setting.ts +38 -0
- package/server/lib/question-queue.ts +111 -0
- package/server/lib/session-hooks-runtime.ts +28 -3
- package/server/lib/session-title-cache.ts +40 -0
- package/server/lib/turn-end-ask.ts +51 -1
- package/server/lib/web-session-auth.ts +17 -0
- package/server/routes/agent-sessions.ts +3 -0
- package/server/routes/permission-broker.ts +31 -2
- package/server/routes/session-hooks.ts +23 -0
- package/server/routes/sessions.ts +3 -0
- package/server/web-control/.manifest.json +10 -10
- package/server/web-control/app.css +26 -3
- package/server/web-control/app.js +381 -36
- package/server/web-control/control.css +5 -1
- package/server/web-control/index.html +7 -5
- package/server/web-control/panes/cos-chat.css +21 -0
- package/server/web-control/panes/memories.js +18 -9
- package/server/web-control/panes/messages.js +15 -3
- package/server/web-control/panes/questions.js +6 -1
- package/server/web-control/panes/settings.js +18 -10
- package/server/web-control/shell.js +8 -13
|
@@ -107,6 +107,28 @@ export function projectedTurnAsk(value: unknown): TurnAskCapture | null {
|
|
|
107
107
|
/** What a row shows (`deriveSessionState` input). */
|
|
108
108
|
export interface VisibleTurnAsk { ask: string; choices: string[]; stoppedAt: number }
|
|
109
109
|
|
|
110
|
+
/** X1 (the one queue): a turn-end ask the queue lists, with its session. */
|
|
111
|
+
export interface QueueTurnAsk extends VisibleTurnAsk { sessionId: string }
|
|
112
|
+
|
|
113
|
+
/** X1: a turn-end ask answered in the last TURN_ASK_ANSWERED_KEEP_MS, and from where (never the reply's words). */
|
|
114
|
+
export interface AnsweredTurnAsk { sessionId: string; ask: string; stoppedAt: number; answeredAt: number; by: TurnAskAnswerSurface }
|
|
115
|
+
export type TurnAskAnswerSurface = 'mac' | 'web' | 'glasses' | 'mac_app' | 'cos'
|
|
116
|
+
export const TURN_ASK_ANSWERED_KEEP_MS = 120_000
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* X1: where a reply that cleared a turn-end ask came from, read from the words COS itself puts on it (contract
|
|
120
|
+
* v1.2: "Answer sent from <SURFACE> through COS, not typed in this session"; a dictated reply carries the
|
|
121
|
+
* `[COS-G2 turn` footer). A COS-spawned Continue child is COS. Anything else was typed at the Mac. Only the
|
|
122
|
+
* surface is kept.
|
|
123
|
+
*/
|
|
124
|
+
export function turnAskAnswerSurface(prompt: unknown, child: boolean): TurnAskAnswerSurface {
|
|
125
|
+
const text = typeof prompt === 'string' ? prompt.slice(0, 2_000) : ''
|
|
126
|
+
if (text.includes('Answer sent from COS Control Web through COS')) return 'web'
|
|
127
|
+
if (text.includes('Answer sent from COS Control on the Mac through COS')) return 'mac_app'
|
|
128
|
+
if (text.includes('[COS-G2 turn')) return 'glasses'
|
|
129
|
+
return child ? 'cos' : 'mac'
|
|
130
|
+
}
|
|
131
|
+
|
|
110
132
|
interface AskState {
|
|
111
133
|
sessionId: string
|
|
112
134
|
promptId: string | null
|
|
@@ -140,6 +162,8 @@ export class TurnEndAskTracker {
|
|
|
140
162
|
private readonly asks = new Map<string, AskState>()
|
|
141
163
|
/** The newest Stop per session, for a wrap_up whose (async) PostToolUse lands after it. */
|
|
142
164
|
private readonly lastStop = new Map<string, { at: number; promptId: string | null }>()
|
|
165
|
+
/** X1: asks answered in the last TURN_ASK_ANSWERED_KEEP_MS that a surface had shown, newest last. */
|
|
166
|
+
private readonly answered: AnsweredTurnAsk[] = []
|
|
143
167
|
private poll: ReturnType<typeof setTimeout> | null = null
|
|
144
168
|
private pollInFlight = false
|
|
145
169
|
private stopped = false
|
|
@@ -162,7 +186,14 @@ export class TurnEndAskTracker {
|
|
|
162
186
|
const current = this.asks.get(sessionId)
|
|
163
187
|
// Any prompt, from the desk or a COS-spawned Continue child, is a reply: the question is answered.
|
|
164
188
|
if (env.event === 'UserPromptSubmit') {
|
|
165
|
-
if (current) {
|
|
189
|
+
if (current) {
|
|
190
|
+
this.asks.delete(sessionId)
|
|
191
|
+
this.counters.clearedByReply++
|
|
192
|
+
if (current.stoppedAt !== null && current.visibleAt !== null) {
|
|
193
|
+
this.answered.push({ sessionId, ask: current.capture.ask, stoppedAt: current.stoppedAt, answeredAt: env.ts, by: turnAskAnswerSurface(p.prompt, child) })
|
|
194
|
+
if (this.answered.length > MAX_TURN_ASKS) this.answered.splice(0, this.answered.length - MAX_TURN_ASKS)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
166
197
|
return
|
|
167
198
|
}
|
|
168
199
|
// A COS-spawned child's other events are not the session's own turn.
|
|
@@ -234,6 +265,25 @@ export class TurnEndAskTracker {
|
|
|
234
265
|
return { ask: state.capture.ask, choices: [...state.capture.choices], stoppedAt: state.stoppedAt }
|
|
235
266
|
}
|
|
236
267
|
|
|
268
|
+
/** X1 (the one queue): every ask a row would show now, oldest stop first. */
|
|
269
|
+
visibleAsks(now = this.deps.now()): QueueTurnAsk[] {
|
|
270
|
+
if (!this.deps.enabled()) return []
|
|
271
|
+
const out: QueueTurnAsk[] = []
|
|
272
|
+
for (const state of [...this.asks.values()]) {
|
|
273
|
+
const shown = this.visibleAsk(state.sessionId, now)
|
|
274
|
+
if (shown) out.push({ sessionId: state.sessionId, ...shown })
|
|
275
|
+
}
|
|
276
|
+
return out.sort((a, b) => a.stoppedAt - b.stoppedAt)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** X1: asks answered within TURN_ASK_ANSWERED_KEEP_MS (by when the reply arrived), oldest first. */
|
|
280
|
+
recentlyAnswered(now = this.deps.now()): AnsweredTurnAsk[] {
|
|
281
|
+
if (!this.deps.enabled()) return []
|
|
282
|
+
// Kept in the order replies arrived, so dropping from the front drops exactly the ones past the window.
|
|
283
|
+
while (this.answered.length > 0 && now - this.answered[0]!.answeredAt > TURN_ASK_ANSWERED_KEEP_MS) this.answered.shift()
|
|
284
|
+
return this.answered.map(a => ({ ...a }))
|
|
285
|
+
}
|
|
286
|
+
|
|
237
287
|
/** The session's row ended: nothing it asked can still be answered. */
|
|
238
288
|
sessionRowEnded(sessionId: string): void {
|
|
239
289
|
const key = sessionId.toLowerCase()
|
|
@@ -444,6 +444,8 @@ export function classifyWebLogin(
|
|
|
444
444
|
}
|
|
445
445
|
if (codeStore && typeof input.code === 'string' && input.code.trim().length > 0) {
|
|
446
446
|
if (codeStore.consume(input.code)) return { ok: true, kind: 'code' }
|
|
447
|
+
// The glasses' 14-character code: by shape only, never looked up (see looksLikeGlassesPairingCode).
|
|
448
|
+
if (looksLikeGlassesPairingCode(input.code)) return { ok: false, reason: 'glasses_code', length: GLASSES_PAIRING_CODE_TYPED_LENGTH }
|
|
447
449
|
const code = normalizeDeviceCode(input.code)
|
|
448
450
|
let glasses = false
|
|
449
451
|
if (code && isGlassesCode) {
|
|
@@ -506,6 +508,21 @@ export function normalizeDeviceCode(input: unknown): string | null {
|
|
|
506
508
|
return clean
|
|
507
509
|
}
|
|
508
510
|
|
|
511
|
+
/**
|
|
512
|
+
* 7.0.32 (O1 00:13, fix 4 of round 2): the code the glasses pair with is 14 characters of the same alphabet. Typed into
|
|
513
|
+
* the browser's sign-in box, it is recognised by its SHAPE only (the same cleaning as normalizeDeviceCode, then exactly
|
|
514
|
+
* 14 characters of the alphabet), never looked up, so a 14-character guess learns nothing about the live glasses code.
|
|
515
|
+
* No browser sign-in code has 14 characters, so nothing valid changes meaning. The 8-character lookup stays as it is.
|
|
516
|
+
*/
|
|
517
|
+
export const GLASSES_PAIRING_CODE_TYPED_LENGTH = 14
|
|
518
|
+
export function looksLikeGlassesPairingCode(input: unknown): boolean {
|
|
519
|
+
if (typeof input !== 'string') return false
|
|
520
|
+
const clean = input.toUpperCase().replace(/[\s-]/g, '').replace(/O/g, '0').replace(/[IL]/g, '1')
|
|
521
|
+
if (clean.length !== GLASSES_PAIRING_CODE_TYPED_LENGTH) return false
|
|
522
|
+
for (const ch of clean) if (!DEVICE_CODE_ALPHABET.includes(ch)) return false
|
|
523
|
+
return true
|
|
524
|
+
}
|
|
525
|
+
|
|
509
526
|
/**
|
|
510
527
|
* Short sign-in codes for another device (the phone), minted by a browser or app that is
|
|
511
528
|
* already signed in. Same shape as glasses pairing: 8 Crockford characters (40 bits), five
|
|
@@ -94,6 +94,7 @@ import { createSessionPreviewRouter } from './session-preview.js'
|
|
|
94
94
|
import { organizerKey, sessionOrganizer, type OrganizerRowFields } from '../lib/session-organizer.js'
|
|
95
95
|
import { listOlderSessions, shortRowFor, OLDER_DEFAULT_DAYS, OLDER_DEFAULT_LIMIT, OLDER_MAX_DAYS, OLDER_MAX_LIMIT, OLDER_MIN_DAYS } from '../lib/agent-session-older.js'
|
|
96
96
|
import { cachedListModel, warmListModels } from '../lib/list-model-memo.js'
|
|
97
|
+
import { noteSessionTitle } from '../lib/session-title-cache.js'
|
|
97
98
|
import { cosStartedSessionIds } from '../lib/cos-started-sessions.js'
|
|
98
99
|
|
|
99
100
|
/** Step 2a: the organizer, or null when it is unavailable or empty (the list is then exactly today's). */
|
|
@@ -649,6 +650,8 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
|
|
|
649
650
|
}))
|
|
650
651
|
// Step 2a: read the models this answer lacked, in the background, for a later poll. Never awaited.
|
|
651
652
|
warmListModels(sessions)
|
|
653
|
+
// X1 (the one queue): the titles this list showed, so the questions route can name a waiting session.
|
|
654
|
+
for (const entry of sessions) noteSessionTitle(entry.session_id, entry.display_label)
|
|
652
655
|
} catch (error) {
|
|
653
656
|
console.error(`[agent-sessions] list failed: ${error instanceof Error ? error.message : error}`)
|
|
654
657
|
res.status(500).json({ error: 'Failed to read agent sessions', reason: 'agent_sessions_read_failed' })
|
|
@@ -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
|
-
|
|
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,34 +2,34 @@
|
|
|
2
2
|
"v": 1,
|
|
3
3
|
"algorithm": "sha256",
|
|
4
4
|
"files": {
|
|
5
|
-
"/app.css": "
|
|
6
|
-
"/app.js": "
|
|
5
|
+
"/app.css": "f2d9c657580959c8a49ce5dd947c027aded0f4e69e4367b8247f092c0ccb0c8f",
|
|
6
|
+
"/app.js": "c5740309a27ba9b1513be466e687ec483311045c2f0088ffb3d5507afb208db2",
|
|
7
7
|
"/apple-touch-icon.png": "04ec7eca0bf6f53d89900d146ac32d4826759d6258bcdbd1ccdcc517f26f532c",
|
|
8
|
-
"/control.css": "
|
|
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": "
|
|
12
|
+
"/index.html": "d9d1cede8b6ef2a3680f5b62e24fffcbf65e2eff3c33de31a69263f53d78520c",
|
|
13
13
|
"/mark.svg": "8e380d33d393027df192eb1afa08f15a380c2070c72d68f27311b49d9e94ca18",
|
|
14
14
|
"/memories/d3.min.js": "f2094bbf6141b359722c4fe454eb6c4b0f0e42cc10cc7af921fc158fceb86539",
|
|
15
15
|
"/memories/graph-explorer.css": "ec128de59811f45c71b6bededd737989309205e903951eb8211ade7664f87486",
|
|
16
16
|
"/memories/graph-explorer.js": "98bde3df25f42022b098d047d2c8e7a22b972c1d817a18a24a0ab075d77871d3",
|
|
17
17
|
"/memories/memory-workspace.js": "013f6c8c72c3474768f8c374edccde0e3656fc243e56528e258c3c29acca64f5",
|
|
18
18
|
"/memory-review.js": "f27fe519f7ad7eccc3b44124fae724aa51e2eb3cb060d2f1795600e0e41b074f",
|
|
19
|
-
"/panes/cos-chat.css": "
|
|
19
|
+
"/panes/cos-chat.css": "6edd123904869ff1d04093ffa8e45cf40863ac532054ce703eb44a59a2f395c4",
|
|
20
20
|
"/panes/cos-chat.js": "b94fca907500be21f777c5d5fa23b7415be48dcbd41d9fa30a4295ed1e3415d3",
|
|
21
21
|
"/panes/dictation.js": "e9434b353a1a3ab3f21d4cef5b2d5f927a733f6af5f58285fda6a78472761f91",
|
|
22
22
|
"/panes/meetings.css": "635470e4d5f5665b046eab7382734c9772df569069c9b1eb10cfde6151014bfa",
|
|
23
23
|
"/panes/meetings.js": "e057f4dae697138111a4c07e8ec6c2a0502b5d93023cf491460bc7bf20172c85",
|
|
24
24
|
"/panes/memories.css": "36731d6fd21ad1516dfeccf34fe5b2d3d267d21e40b47704055a0685334b579e",
|
|
25
|
-
"/panes/memories.js": "
|
|
26
|
-
"/panes/messages.js": "
|
|
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": "
|
|
29
|
+
"/panes/questions.js": "60115b6f5d5380d6dac1ceb50e6a56bd6d10f8d49fb64429fbe73d6890270111",
|
|
30
30
|
"/panes/reader-media.js": "4cf892a855aa2d0dce6978e69296cd49b8036d8fc62f12773b3fcadc210b2ef0",
|
|
31
31
|
"/panes/sessions.css": "654e0509ab2b699b9ea92b6d9e2f7522146df1f0b6355915bef03caaf63addc4",
|
|
32
|
-
"/panes/settings.js": "
|
|
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": "
|
|
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)
|
|
708
|
-
|
|
709
|
-
html.short-screen .section-btn .caret
|
|
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
|
}
|