dsh-notch 0.3.1 → 0.3.6

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 (102) hide show
  1. package/README.en.md +58 -47
  2. package/README.md +60 -55
  3. package/cordis.patch.yml +3 -0
  4. package/desktop/README.md +29 -0
  5. package/desktop/notch-lifecycle.mjs +120 -0
  6. package/desktop/restart-probe.mjs +61 -0
  7. package/docs/desktop-017rc2.md +23 -0
  8. package/docs/elastic-edge-dock.md +127 -0
  9. package/docs/motion-audit.md +1 -1
  10. package/docs/releases/v0.3.2.md +38 -0
  11. package/docs/releases/v0.3.3.md +9 -0
  12. package/docs/subagent-compatibility.md +51 -0
  13. package/docs/verification/elastic-algorithm-timing.json +337 -0
  14. package/docs/verification/elastic-attention.json +44 -0
  15. package/docs/verification/elastic-frame-timing.json +240 -0
  16. package/docs/verification/elastic-tension.json +59 -0
  17. package/docs/verification/elastic-tip-taper.json +181 -0
  18. package/dshx.yml +2 -0
  19. package/lib/client.js +112 -0
  20. package/lib/client.js.map +1 -0
  21. package/lib/index.mjs +990 -0
  22. package/lib/index.mjs.map +1 -0
  23. package/package.json +66 -10
  24. package/src/board.ts +178 -35
  25. package/src/client/index.ts +104 -0
  26. package/src/dsh-notch.ts +19 -2
  27. package/src/http.ts +17 -2
  28. package/src/session-state.ts +15 -1
  29. package/src/store.ts +3 -3
  30. package/src/types.ts +3 -0
  31. package/BASELINE.md +0 -13
  32. package/DESIGN.md +0 -34
  33. package/cordis.yml +0 -5
  34. package/macos/Package.swift +0 -17
  35. package/macos/STATUS-MOTION.md +0 -49
  36. package/macos/Sources/Client.swift +0 -139
  37. package/macos/Sources/IdleRobot.swift +0 -423
  38. package/macos/Sources/NotchMarkdown.swift +0 -266
  39. package/macos/Sources/Panel.swift +0 -153
  40. package/macos/Sources/Resources/Idle/balance.json +0 -1
  41. package/macos/Sources/Resources/Idle/blink.json +0 -1
  42. package/macos/Sources/Resources/Idle/cube-in.json +0 -1
  43. package/macos/Sources/Resources/Idle/dance.json +0 -1
  44. package/macos/Sources/Resources/Idle/hop.json +0 -1
  45. package/macos/Sources/Resources/Idle/nod.json +0 -1
  46. package/macos/Sources/Resources/Idle/satellite-in.json +0 -1
  47. package/macos/Sources/Resources/Idle/satellite-out.json +0 -1
  48. package/macos/Sources/Resources/Idle/scan.json +0 -1
  49. package/macos/Sources/Resources/Idle/sleep.json +0 -1
  50. package/macos/Sources/Resources/Idle/sneeze.json +0 -1
  51. package/macos/Sources/Resources/Idle/stretch.json +0 -1
  52. package/macos/Sources/Resources/Idle/tilt.json +0 -1
  53. package/macos/Sources/ReviewStudio.swift +0 -337
  54. package/macos/Sources/RootView.swift +0 -1093
  55. package/macos/Sources/StatusOrbit.swift +0 -642
  56. package/macos/Sources/main.swift +0 -153
  57. package/macos/Tests/ExpandedHeightProbe.swift +0 -108
  58. package/macos/Tests/GeometryProbe.swift +0 -98
  59. package/macos/Tests/IdleProbe.swift +0 -294
  60. package/macos/Tests/MotionProbe.swift +0 -106
  61. package/macos/Tests/OutcomeProbe.swift +0 -133
  62. package/macos/Tests/ScrollbarProbe.swift +0 -91
  63. package/macos/Tests/expanded-height.sh +0 -13
  64. package/macos/Tests/geometry.sh +0 -14
  65. package/macos/Tests/idle.sh +0 -14
  66. package/macos/Tests/motion.sh +0 -14
  67. package/macos/Tests/outcome.sh +0 -14
  68. package/macos/Tests/scrollbar.sh +0 -13
  69. package/tests/answer-lifecycle.test.mjs +0 -23
  70. package/tests/board.test.mjs +0 -7
  71. package/tests/browse-sync.test.mjs +0 -20
  72. package/tests/mirror.test.mjs +0 -4
  73. package/tests/peer-range.test.mjs +0 -21
  74. package/tests/session-state.test.mjs +0 -54
  75. package/tests/unread-hold.test.mjs +0 -90
  76. package/tools/desktop-shell/README.md +0 -56
  77. package/tools/desktop-shell/child-log.mjs +0 -6
  78. package/tools/desktop-shell/child-log.test.mjs +0 -14
  79. package/tools/desktop-shell/package-recovery.py +0 -78
  80. package/tools/desktop-shell/recovery-probe.cjs +0 -50
  81. package/tools/desktop-shell/renderer-recovery.mjs +0 -95
  82. package/tools/desktop-shell/renderer-recovery.test.mjs +0 -24
  83. package/tools/diagnostics/completion-watch.py +0 -101
  84. package/tools/diagnostics/completion-watch.test.py +0 -17
  85. package/tools/diagnostics/renderer-diagnostics.mjs +0 -56
  86. package/tools/diagnostics/renderer-diagnostics.test.mjs +0 -32
  87. package/tools/idle/LICENSE.open-bot-motion +0 -21
  88. package/tools/idle/export.mjs +0 -40
  89. package/tools/idle/index.html +0 -4
  90. package/tools/idle/motions.js +0 -83
  91. package/tools/idle/original.js +0 -1673
  92. package/tools/idle/renderer.js +0 -1673
  93. package/tools/recording/Client.swift +0 -76
  94. package/tools/recording/DemoCatalog.swift +0 -174
  95. package/tools/recording/README.md +0 -23
  96. package/tools/recording/build.sh +0 -41
  97. package/tools/recording/main.swift +0 -100
  98. package/tools/review/ReviewStudio.swift +0 -236
  99. package/tools/review/audit.sh +0 -19
  100. package/tools/review/build.sh +0 -7
  101. package/tools/review/main.swift +0 -158
  102. package/tools/review/render-audit.swift +0 -32
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../desktop/notch-lifecycle.mjs","../src/session-state.ts","../src/store.ts","../src/board.ts","../src/http.ts","../src/dsh-notch.ts"],"sourcesContent":["import { spawn, execFileSync } from 'node:child_process'\nimport { readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync, statSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\n\n/** PID alone is not ownership: require both executable and process start time. */\nexport function processIdentity(pid) {\n if (!Number.isSafeInteger(pid) || pid <= 1) return { state: 'dead' }\n try {\n const text = execFileSync('/bin/ps', ['-p', String(pid), '-o', 'lstart=', '-o', 'comm='],\n { encoding: 'utf8', env: { ...process.env, LC_ALL: 'C' }, timeout: 1000 }).trim()\n const match = /^(.{24})\\s+(.+)$/.exec(text)\n if (match) return { state: 'alive', stamp: match[1], command: match[2] }\n } catch {}\n try { process.kill(pid, 0) } catch (error) {\n if (error.code === 'ESRCH') return { state: 'dead' }\n }\n return { state: 'unknown' }\n}\n\nexport function processLiveness(pid) {\n if (!Number.isSafeInteger(pid) || pid <= 1) return 'dead'\n try { process.kill(pid, 0); return 'alive' } catch (error) {\n return error.code === 'ESRCH' ? 'dead' : 'unknown'\n }\n}\n\n/** The desktop owns an adopted helper too. If it exits during adoption, retry. */\nexport function superviseNotch({ bin, pidPath, log, interval = 250, env = process.env,\n probe = processIdentity, alive = processLiveness, launch = spawn, signal = process.kill.bind(process) }) {\n let current, pending, timer, stopped = false\n const starts = []\n const note = text => log.write(`[notch] ${text}\\n`)\n const storedPID = () => {\n try { return Number(readFileSync(pidPath, 'utf8').trim()) } catch { return 0 }\n }\n const owned = pid => {\n const identity = probe(pid)\n return identity.state === 'alive' && resolve(identity.command) === resolve(bin)\n ? { pid, ...identity, checkedAt: Date.now() } : undefined\n }\n const schedule = () => {\n clearTimeout(timer)\n if (!stopped) { timer = setTimeout(reconcile, interval); timer.unref?.() }\n }\n function reconcile() {\n if (stopped) return\n if (pending) { schedule(); return }\n // External installers hold this short lease while replacing executable + PID.\n // A crashed installer cannot disable recovery indefinitely.\n try {\n if (Date.now() - statSync(`${pidPath}.updating`).mtimeMs < 60000) { schedule(); return }\n } catch {}\n if (current) {\n const state = alive(current.pid)\n if (state === 'unknown' || (state === 'alive' && Date.now() - current.checkedAt < 5000)) {\n schedule(); return\n }\n const live = probe(current.pid)\n if (live.state === 'unknown' || (live.state === 'alive' && live.stamp === current.stamp && live.command === current.command)) {\n current.checkedAt = Date.now()\n schedule(); return\n }\n current = undefined\n }\n const pid = storedPID()\n const identity = probe(pid)\n if (identity.state === 'unknown') { schedule(); return }\n current = owned(pid)\n if (current) { note(`adopt ${current.pid}`); schedule(); return }\n while (starts.length && Date.now() - starts[0] > 10000) starts.shift()\n if (starts.length >= 3) { note('helper repeatedly exited; retry paused'); stopped = true; return }\n starts.push(Date.now())\n const child = launch(bin, [], { stdio: ['ignore', 'pipe', 'pipe'], detached: false, env })\n pending = child\n child.stdout?.pipe(log, { end: false })\n child.stderr?.pipe(log, { end: false })\n child.once('spawn', () => {\n if (pending !== child) return\n pending = undefined\n if (stopped) { child.kill('SIGTERM'); return }\n current = owned(child.pid)\n // A successful spawn still must be attributable before touching its PID file.\n if (!current) { child.kill('SIGTERM'); schedule(); return }\n mkdirSync(dirname(pidPath), { recursive: true })\n const stage = `${pidPath}.${process.pid}.tmp`\n writeFileSync(stage, `${child.pid}\\n`)\n renameSync(stage, pidPath)\n note(`spawn ${child.pid}`)\n schedule()\n })\n child.once('error', error => { if (pending === child) pending = undefined; note(`spawn error: ${error.message}`); schedule() })\n child.once('exit', () => {\n if (pending === child) pending = undefined\n if (current?.pid === child.pid) current = undefined\n schedule()\n })\n schedule()\n }\n const stop = () => {\n stopped = true\n clearTimeout(timer)\n pending?.kill('SIGTERM')\n // A native hot replacement may have updated the file since the last check.\n const candidates = [current, owned(storedPID())].filter(Boolean)\n const seen = new Set()\n for (const item of candidates) {\n if (seen.has(item.pid)) continue\n seen.add(item.pid)\n const live = owned(item.pid)\n if (!live || live.stamp !== item.stamp) continue\n try { signal(item.pid, 'SIGTERM'); note(`stop ${item.pid}`) } catch (error) {\n if (error.code !== 'ESRCH') note(`stop failed: ${error.message}`)\n }\n if (storedPID() === item.pid) { try { unlinkSync(pidPath) } catch {} }\n }\n current = undefined\n }\n reconcile()\n return { get pid() { return current?.pid ?? pending?.pid }, stop, kill: stop }\n}\n","import type { Session } from '@deepseek-ai/dsh-session'\nimport type { NotchLastTurn } from './types.ts'\n\nexport interface FoldedSession {\n busy: boolean\n lastTurn?: NotchLastTurn\n}\n\nconst FAILED = new Set(['error', 'blocked', 'max-tokens'])\n\nexport function foldSession(session: Session): FoldedSession {\n let busy = false\n let lastTurn: NotchLastTurn | undefined\n for (const event of session.snapshotEvents()) {\n if (event.type === 'turn/start') {\n busy = true\n lastTurn = undefined\n continue\n }\n if (event.type === 'turn/end') {\n busy = false\n const kind = event.data.reason.kind\n lastTurn = { at: event.time, kind, failed: FAILED.has(kind) }\n }\n }\n return { busy, lastTurn }\n}\n\nexport function isChildSession(session: Session): boolean {\n // parentSession is also seed lineage for an independent user fork.\n // Use the same presentation discriminator as Harness's workspace sidebar.\n return session.header.origin === 'subagent'\n}\n\n/** Follow only delegation edges; user forks remain separate conversations. */\nexport function conversationOwner(session: Session, sessions: ReadonlyMap<string, Session>): Session | undefined {\n let current: Session | undefined = session\n const visited = new Set<string>()\n while (current && isChildSession(current)) {\n if (visited.has(current.id)) return undefined\n visited.add(current.id)\n current = current.header.parentSession === undefined ? undefined : sessions.get(current.header.parentSession)\n }\n return current\n}\n","import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'\nimport { resolveDshHome } from '@deepseek-ai/dsh-home-paths'\nimport { join } from 'node:path'\nimport { randomBytes } from 'node:crypto'\n\nexport const DIR = join(resolveDshHome(), 'dsh-notch')\nexport const RUNTIME = join(DIR, 'runtime.json')\nconst SEEN = join(DIR, 'seen.json')\n\nexport interface RuntimeFile {\n origin: string\n token: string\n pid: number\n writtenAt: number\n}\n\nfunction ensureDir(): void {\n mkdirSync(DIR, { recursive: true, mode: 0o700 })\n}\n\nfunction readJson<T>(path: string): T | undefined {\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as T\n } catch {\n return undefined\n }\n}\n\nexport function loadOrCreateToken(): string {\n const existing = readJson<RuntimeFile>(RUNTIME)\n if (existing?.token && existing.token.length >= 16) return existing.token\n return randomBytes(24).toString('hex')\n}\n\nexport function writeRuntime(file: RuntimeFile): void {\n ensureDir()\n writeFileSync(RUNTIME, `${JSON.stringify(file, null, 2)}\\n`, { encoding: 'utf8', mode: 0o600 })\n}\n\nexport function loadSeen(): Record<string, number> {\n const value = readJson<Record<string, number>>(SEEN)\n if (!value || typeof value !== 'object') return {}\n return value\n}\n\nexport function saveSeen(map: Record<string, number>): void {\n ensureDir()\n writeFileSync(SEEN, `${JSON.stringify(map)}\\n`, { encoding: 'utf8', mode: 0o600 })\n}\n","import { randomUUID } from 'node:crypto'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Session } from '@deepseek-ai/dsh-session'\nimport type { JobRegistry } from '@deepseek-ai/dsh-jobs'\nimport type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'\nimport type {\n AskUserQuestionAnswer,\n AskUserQuestionRequest,\n} from '@deepseek-ai/dsh-user-questions'\nimport { conversationOwner, foldSession, isChildSession } from './session-state.ts'\nimport { loadSeen, saveSeen } from './store.ts'\nimport type {\n ApprovalOutcomeWire,\n NotchAnswerItem,\n NotchApproval,\n NotchAsk,\n NotchFocus,\n NotchQuestion,\n NotchRow,\n NotchSnapshot,\n} from './types.ts'\n\ninterface HeldApproval {\n kind: 'approval'\n id: string\n sessionId: string\n toolName: string\n reason?: string\n resolve: (outcome: ApprovalOutcome) => void\n}\n\ninterface HeldAsk {\n kind: 'ask'\n id: string\n sessionId: string\n questions: NotchQuestion[]\n resolve: (answer: AskUserQuestionAnswer) => void\n}\n\ntype Held = HeldApproval | HeldAsk\n\ninterface SidebarRow { id: string; title: string; completed: boolean; running: boolean; updatedAt?: number }\nexport class Board {\n private sidebar: { clientId: string; at: number; focused: boolean; rows: SidebarRow[]; projectionVersion?: number } | undefined\n private readonly instance = randomUUID()\n private readonly traces: Record<string, unknown>[] = []\n private lastMirrorTrace = ''\n private lastRowTrace = ''\n\n /** Bounded, content-free diagnostics for state-source races. */\n diagnostics(): unknown {\n return { stateRevision: 4, instance: this.instance,\n sidebar: this.sidebar && { clientId: this.sidebar.clientId, at: this.sidebar.at, focused: this.sidebar.focused },\n traces: this.traces }\n }\n\n private trace(event: string, value: Record<string, unknown>): void {\n this.traces.push({ at: Date.now(), event, ...value })\n if (this.traces.length > 160) this.traces.shift()\n }\n\n syncSidebar(input: unknown): boolean {\n if (!input || typeof input !== 'object') return false\n const data = input as { clientId?: unknown; focused?: unknown; rows?: unknown; projectionVersion?: unknown; viewed?: { id?: unknown; at?: unknown } }\n if (typeof data.clientId !== 'string' || !Array.isArray(data.rows) || data.rows.length > 1000) return false\n const rows: SidebarRow[] = []\n for (const row of data.rows) {\n // Session IDs are opaque; API-created conversations need no session- prefix.\n if (!row || typeof row.id !== 'string' || row.id.length === 0 || typeof row.title !== 'string' || typeof row.completed !== 'boolean' || typeof row.running !== 'boolean') return false\n rows.push({ id: row.id, title: row.title.slice(0, 512), completed: row.completed, running: row.running,\n ...(typeof row.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? { updatedAt: row.updatedAt } : {}) })\n }\n const sessions = new Map(this.ctx.sessions.list().map(session => [session.id, session]))\n // Completion is a fact, not a page-local negative flag. A page that was\n // reloaded or selected a different conversation cannot retract another\n // page's reminder. Explicit, timestamped reading acknowledgements can.\n for (const row of rows) {\n if (!row.completed || row.running) continue\n const session = sessions.get(row.id)\n if (session && (isChildSession(session) || this.isBusy(session))) continue\n const turn = session ? foldSession(session).lastTurn : undefined\n const seen = this.seen[row.id]\n const completedAt = turn?.at ?? row.updatedAt\n if (seen !== undefined && (completedAt === undefined || seen >= completedAt)) continue\n this.browserCompletions.set(row.id, row)\n }\n const viewed = data.viewed\n if (data.projectionVersion === 3 && data.focused === true && viewed\n && typeof viewed.id === 'string' && typeof viewed.at === 'number' && Number.isFinite(viewed.at)\n && rows.some(row => row.id === viewed.id && !row.running)) {\n const session = sessions.get(viewed.id)\n if (!session || (!isChildSession(session) && !this.conversationBusy(session, sessions))) this.readThrough(viewed.id, Math.min(viewed.at, Date.now()))\n }\n // Legacy clients can acknowledge only a reminder they themselves showed.\n if (data.projectionVersion !== 3 && data.focused === true && this.sidebar?.clientId === data.clientId) {\n for (const previous of this.sidebar.rows) {\n if (!previous.completed || rows.some(row => row.id === previous.id && row.completed)) continue\n const session = sessions.get(previous.id)\n const at = session ? foldSession(session).lastTurn?.at : Date.now()\n if (at !== undefined && (!session || !this.conversationBusy(session, sessions))) this.readThrough(previous.id, at)\n }\n }\n // A background browser cannot overwrite the last foreground page's state.\n if (this.sidebar && this.sidebar.clientId !== data.clientId && data.focused !== true && Date.now() - this.sidebar.at < 5000) { this.bump(); return true }\n this.sidebar = { clientId: data.clientId, at: Date.now(), focused: data.focused === true, rows,\n ...(data.projectionVersion === 2 || data.projectionVersion === 3 ? { projectionVersion: data.projectionVersion } : {}) }\n const mirrorTrace = { clientId: data.clientId, focused: data.focused === true,\n rows: rows.map(({ id, running, completed }) => ({ id, running, completed })) }\n const signature = JSON.stringify(mirrorTrace)\n if (signature !== this.lastMirrorTrace) { this.lastMirrorTrace = signature; this.trace('sidebar', mirrorTrace) }\n this.bump()\n return true\n }\n\n private sidebarRows(): SidebarRow[] | undefined {\n return this.sidebar && Date.now() - this.sidebar.at < 5000 ? this.sidebar.rows : undefined\n }\n\n private readonly pending = new Map<string, Held>()\n private readonly seen = loadSeen()\n private running = new Set<string>()\n private readonly pendingUnread = new Set<string>()\n private readonly browserCompletions = new Map<string, SidebarRow>()\n private readonly listeners = new Set<() => void>()\n private focus: NotchFocus | null = null\n\n constructor(private readonly ctx: Context) {}\n\n onChange(fn: () => void): () => void {\n this.listeners.add(fn)\n return () => { this.listeners.delete(fn) }\n }\n\n notify(): void {\n this.bump()\n }\n\n private bump(): void {\n for (const fn of this.listeners) {\n try { fn() } catch (error) { this.ctx.logger.warn('dsh-notch: subscriber failed: %s', String(error)) }\n }\n }\n\n snapshot(origin: string): NotchSnapshot {\n const rows: NotchRow[] = []\n const sessions = this.ctx.sessions.list()\n const byId = new Map<string, Session>(sessions.map(session => [session.id, session]))\n const groups = new Map<Session, Session[]>()\n for (const session of sessions) {\n const owner = conversationOwner(session, byId)\n if (!owner) {\n // Broken/unloaded lineage must not invent a task or lose a user question.\n const ids = new Set([session.id])\n const approval = this.heldApproval(ids)\n const ask = this.heldAsk(ids)\n if (approval || ask) rows.push({\n id: session.id, title: this.titleOf(session), child: true,\n busy: false, unread: false,\n ...(approval ? { approval } : {}), ...(ask ? { ask } : {}),\n })\n continue\n }\n const members = groups.get(owner) ?? []\n members.push(session)\n groups.set(owner, members)\n }\n for (const [owner, members] of groups) {\n const row = this.rowFor(owner, members)\n if (row) rows.push(row)\n }\n const freshSidebar = this.sidebarRows()\n const coldRows = new Map(this.browserCompletions)\n for (const source of freshSidebar ?? []) {\n if (source.running) coldRows.set(source.id, source)\n }\n for (const source of coldRows.values()) {\n // The Host owns loaded session classification. A stale mirror must not\n // resurrect a filtered child as child:false or override a settled root.\n if (byId.has(source.id) || (!source.completed && !source.running)) continue\n // Heartbeat freshness is a liveness lease, not an unread-state change.\n // Retain completed cold rows until acknowledged; expire running-only\n // claims when their source stops reporting.\n if (source.running && !freshSidebar) continue\n const seen = this.seen[source.id]\n if (source.completed && seen !== undefined && (source.updatedAt === undefined || seen >= source.updatedAt)) continue\n rows.push({ id: source.id, title: source.title, child: false, busy: source.running, unread: source.completed })\n }\n rows.sort((a, b) => Number(Boolean(b.approval || b.ask)) - Number(Boolean(a.approval || a.ask))\n || Number(b.busy) - Number(a.busy)\n || Number(b.unread) - Number(a.unread)\n || (b.lastTurn?.at ?? 0) - (a.lastTurn?.at ?? 0))\n this.running = new Set(\n sessions.filter((session) => this.isBusy(session)).map((session) => session.id),\n )\n const stateRows = rows.map(row => ({ id: row.id, busy: row.busy, unread: row.unread,\n turnAt: row.lastTurn?.at, action: Boolean(row.ask || row.approval) }))\n const rowSignature = JSON.stringify(stateRows)\n if (rowSignature !== this.lastRowTrace) {\n this.lastRowTrace = rowSignature\n this.trace('snapshot', { rows: stateRows, clientId: this.sidebar?.clientId,\n mirrorAt: this.sidebar?.at, running: [...this.running], pendingUnread: [...this.pendingUnread] })\n }\n return { ok: true, stateRevision: 4, generatedAt: Date.now(), origin, rows,\n sidebarSyncedAt: freshSidebar ? this.sidebar?.at : undefined,\n sidebarProjectionVersion: freshSidebar ? this.sidebar?.projectionVersion : undefined }\n }\n\n markSeen(sessionId: string): void {\n this.pendingUnread.delete(sessionId)\n this.browserCompletions.delete(sessionId)\n this.seen[sessionId] = Date.now()\n saveSeen(this.seen)\n this.bump()\n }\n\n markAllSeen(): void {\n this.pendingUnread.clear()\n const now = Date.now()\n for (const id of this.browserCompletions.keys()) this.seen[id] = now\n this.browserCompletions.clear()\n for (const session of this.ctx.sessions.list()) {\n this.seen[session.id] = now\n }\n saveSeen(this.seen)\n this.bump()\n }\n\n private readThrough(sessionId: string, at: number): void {\n const seen = this.seen[sessionId]\n if ((seen ?? -Infinity) >= at) return\n const session = this.ctx.sessions.list().find(item => item.id === sessionId)\n const turn = session && foldSession(session).lastTurn\n const completedAt = turn?.at ?? this.browserCompletions.get(sessionId)?.updatedAt\n // An idle focused page reports every 400 ms. Persist once per result,\n // not on every heartbeat for as long as that conversation stays open.\n if (seen !== undefined && (completedAt === undefined || seen >= completedAt)) return\n this.seen[sessionId] = at\n if (completedAt === undefined || completedAt <= at) {\n this.pendingUnread.delete(sessionId)\n this.browserCompletions.delete(sessionId)\n }\n saveSeen(this.seen)\n this.trace('read', { sessionId, through: at })\n }\n\n noteTurnEnd(session: Session, at = foldSession(session).lastTurn?.at): void {\n if (isChildSession(session)) return\n if (at !== undefined && (this.seen[session.id] ?? -Infinity) < at) this.pendingUnread.add(session.id)\n }\n\n /**\n * Record a \"show this session in a DSH UI\" wish from the native helper.\n * Returns false for unknown sessions. The wish expires after 60s; every\n * open DSH page consumes it idempotently, so no per-client cursor is kept.\n */\n requestFocus(sessionId: string): boolean {\n const known = this.ctx.sessions.list().some((session) => session.id === sessionId)\n const mirrored = this.browserCompletions.has(sessionId) || this.sidebarRows()?.some(row => row.id === sessionId)\n if (!known && !mirrored) return false\n this.focus = { sessionId, at: Date.now() }\n this.bump()\n return true\n }\n\n peekFocus(): NotchFocus | null {\n if (this.focus === null) return null\n if (Date.now() - this.focus.at > 60_000) {\n this.focus = null\n return null\n }\n return this.focus\n }\n\n decideApproval(id: string, outcome: ApprovalOutcomeWire): boolean {\n const held = this.pending.get(id)\n if (!held || held.kind !== 'approval') return false\n this.pending.delete(id)\n this.seen[held.sessionId] = Date.now()\n saveSeen(this.seen)\n held.resolve(outcome)\n this.bump()\n return true\n }\n\n answerAsk(id: string, answers: NotchAnswerItem[]): boolean {\n const held = this.pending.get(id)\n if (!held || held.kind !== 'ask') return false\n this.pending.delete(id)\n this.seen[held.sessionId] = Date.now()\n saveSeen(this.seen)\n held.resolve({ answers })\n this.bump()\n return true\n }\n\n holdApproval(request: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome> {\n const sessionId = String(request.agent.id)\n const id = randomUUID()\n const fromNotch = new Promise<ApprovalOutcome>((resolve) => {\n const held: HeldApproval = {\n kind: 'approval',\n id,\n sessionId,\n toolName: request.toolName,\n resolve,\n }\n if (request.reason) held.reason = request.reason\n this.pending.set(id, held)\n this.bump()\n request.signal?.addEventListener('abort', () => {\n if (!this.pending.delete(id)) return\n this.bump()\n }, { once: true })\n })\n return Promise.race([fromNotch, next()]).finally(() => {\n if (this.pending.delete(id)) this.bump()\n })\n }\n\n holdAsk(request: AskUserQuestionRequest, next: () => Promise<AskUserQuestionAnswer>): Promise<AskUserQuestionAnswer> {\n const sessionId = request.agent ? String(request.agent.id) : ''\n if (!sessionId) return next()\n const originalSignal = request.signal\n const downstream = new AbortController()\n request.signal = originalSignal\n ? AbortSignal.any([originalSignal, downstream.signal])\n : downstream.signal\n const id = randomUUID()\n const questions: NotchQuestion[] = request.questions.map((item) => ({\n id: item.id,\n question: item.question,\n ...(item.detail === undefined ? {} : { detail: item.detail }),\n ...(item.header === undefined ? {} : { header: item.header }),\n ...(item.options === undefined ? {} : { options: item.options }),\n ...(item.multiSelect === undefined ? {} : { multiSelect: item.multiSelect }),\n ...(item.intent === undefined ? {} : { intent: item.intent }),\n }))\n const fromNotch = new Promise<AskUserQuestionAnswer>((resolve) => {\n this.pending.set(id, { kind: 'ask', id, sessionId, questions, resolve })\n this.bump()\n request.signal?.addEventListener('abort', () => {\n if (!this.pending.delete(id)) return\n this.bump()\n }, { once: true })\n })\n return Promise.race([fromNotch, Promise.resolve().then(next)]).finally(() => {\n // The losing Web answerer must end too, so its pending interaction unmounts.\n // Cancel only this delegated wait, never the owning agent's signal.\n downstream.abort(new Error('Question settled through another answerer'))\n if (originalSignal === undefined) delete request.signal\n else request.signal = originalSignal\n if (this.pending.delete(id)) this.bump()\n })\n }\n\n private isBusy(session: Session): boolean {\n return this.ctx.agents.get(session.id)?.status === 'running'\n }\n\n private conversationBusy(owner: Session, sessions: ReadonlyMap<string, Session>): boolean {\n for (const member of sessions.values()) {\n if (conversationOwner(member, sessions)?.id === owner.id\n && (this.isBusy(member) || this.hasSubagentJob(member))) return true\n }\n return false\n }\n\n private hasSubagentJob(session: Session): boolean {\n const agent = this.ctx.agents.get(session.id)\n if (!agent) return false\n // Remote backends have no local Agent/Session. The official background\n // job registry retains their owner through running and stopping, including\n // jobs that were already active when Notch was hot-loaded.\n const jobs = this.ctx.get('jobs') as Pick<JobRegistry, 'list'> | undefined\n return jobs?.list(agent.id).some(job => job.owner === session.id\n && job.kind === 'subagent'\n && (job.status === 'running' || job.status === 'stopping')) ?? false\n }\n\n private rowFor(session: Session, members: Session[]): NotchRow | undefined {\n const folded = foldSession(session)\n const child = isChildSession(session)\n const lastSeen = this.seen[session.id]\n const busy = members.some(member => this.isBusy(member) || this.hasSubagentJob(member))\n // Completion belongs to the owner's turn, never an individual worker.\n if (folded.busy) {\n this.pendingUnread.delete(session.id)\n } else if (this.running.has(session.id) && folded.lastTurn) {\n this.pendingUnread.add(session.id)\n }\n const dismissed = lastSeen !== undefined && (folded.lastTurn === undefined || lastSeen >= folded.lastTurn.at)\n const unread = !dismissed && !busy\n && (this.pendingUnread.has(session.id) || this.browserCompletions.has(session.id))\n const memberIds = new Set(members.map(member => member.id))\n // Keep the original request id/resolver so answering the root's yellow\n // lamp settles the correct child. Further child questions queue here.\n const approval = this.heldApproval(memberIds)\n const ask = this.heldAsk(memberIds)\n if (!busy && !unread && !approval && !ask) return undefined\n const title = this.titleOf(session)\n const row: NotchRow = {\n id: session.id,\n title,\n child,\n busy,\n unread,\n }\n if (folded.lastTurn && !busy) row.lastTurn = folded.lastTurn\n if (approval) row.approval = approval\n if (ask) row.ask = ask\n return row\n }\n\n private heldApproval(sessionIds: ReadonlySet<string>): NotchApproval | undefined {\n for (const held of this.pending.values()) {\n if (held.kind === 'approval' && sessionIds.has(held.sessionId)) {\n return {\n id: held.id,\n toolName: held.toolName,\n ...(held.reason === undefined ? {} : { reason: held.reason }),\n }\n }\n }\n return undefined\n }\n\n private heldAsk(sessionIds: ReadonlySet<string>): NotchAsk | undefined {\n for (const held of this.pending.values()) {\n if (held.kind === 'ask' && sessionIds.has(held.sessionId)) {\n return { id: held.id, questions: held.questions }\n }\n }\n return undefined\n }\n\n private titleOf(session: Session): string {\n const service = this.ctx.get('sessionTitle') as { get(target: Session): { title: string } | undefined } | undefined\n const title = service?.get(session)?.title?.trim()\n if (title) return title\n return session.id.slice(0, 8)\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type { Board } from './board.ts'\nimport type { ApprovalOutcomeWire, NotchAnswerItem } from './types.ts'\n\nconst PREFIX = '/dsh-notch'\n\nexport function isLoopback(req: IncomingMessage): boolean {\n const ip = req.socket.remoteAddress ?? ''\n return ip === '127.0.0.1' || ip === '::1' || ip === ':ffff:127.0.0.1' || ip === '::ffff:127.0.0.1'\n}\n\nexport function authorized(req: IncomingMessage, token: string): boolean {\n const header = req.headers.authorization\n if (header === `Bearer ${token}`) return true\n try {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n return url.searchParams.get('token') === token\n } catch {\n return false\n }\n}\n\n/**\n * Browser-side trust for pages served by this same Host: loopback remote,\n * never cross-site, and a matching Origin when one is present. Same rule the\n * shipped workspace file panel uses — no secret ever lands in page JS.\n */\nexport function browserTrusted(req: IncomingMessage): boolean {\n if (!isLoopback(req)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const host = req.headers.host\n if (host === undefined) return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try {\n return new URL(origin).host === new URL(`http://${host}`).host\n } catch {\n return false\n }\n}\n\n/** Accept same-origin Web writes and requests forwarded by the Desktop shell. */\nexport function sidebarTrusted(req: IncomingMessage, origin: string): boolean {\n if (!browserTrusted(req) || !req.headers['content-type']?.startsWith('application/json')) return false\n if (req.headers.origin !== undefined) return true\n // Desktop validates dsh-app://app, then removes Origin and fetch metadata.\n // A script-only header still prevents a foreign page from submitting a form.\n return req.headers['x-dsh-notch-client'] === '1'\n && req.headers['sec-fetch-site'] === undefined\n && req.headers.host === new URL(origin).host\n}\n\nfunction send(res: ServerResponse, status: number, body: unknown): void {\n const json = JSON.stringify(body)\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n })\n res.end(json)\n}\n\nfunction readBody(req: IncomingMessage, limit = 64 * 1024): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > limit) {\n reject(new Error('body too large'))\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))\n req.on('error', reject)\n })\n}\n\nexport function attachHttp(ctx: Context, board: Board, token: string, origin: string): () => void {\n const streams = new Map<string, ServerResponse>()\n const push = () => {\n const payload = `data: ${JSON.stringify(board.snapshot(origin))}\\n\\n`\n for (const [id, res] of streams) {\n try {\n res.write(payload)\n } catch {\n streams.delete(id)\n }\n }\n }\n const stopListen = board.onChange(push)\n\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1')\n const path = url.pathname\n const method = req.method ?? 'GET'\n\n // Browser pages served by this same Host consume focus wishes through\n // same-origin trust (no secret in page JS); everything else keeps the\n // notch Bearer token.\n if (method === 'GET' && path === `${PREFIX}/pending-focus`) {\n if (!browserTrusted(req)) {\n send(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n send(res, 200, { ok: true, focus: board.peekFocus() })\n return\n }\n\n if (method === 'POST' && path === `${PREFIX}/sidebar`) {\n if (!sidebarTrusted(req, origin)) {\n send(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n const ok = board.syncSidebar(JSON.parse(await readBody(req, 512 * 1024)))\n send(res, ok ? 200 : 400, { ok })\n return\n }\n\n if (!isLoopback(req) || !authorized(req, token)) {\n send(res, 403, { ok: false, error: 'forbidden' })\n return\n }\n\n if (method === 'GET' && path === `${PREFIX}/status`) {\n send(res, 200, board.snapshot(origin))\n return\n }\n\n if (method === 'GET' && path === `${PREFIX}/diagnostics`) {\n send(res, 200, board.diagnostics())\n return\n }\n\n if (method === 'GET' && path === `${PREFIX}/events`) {\n const id = randomUUID()\n res.writeHead(200, {\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n })\n streams.set(id, res)\n res.write(`data: ${JSON.stringify(board.snapshot(origin))}\\n\\n`)\n req.on('close', () => { streams.delete(id) })\n return\n }\n\n if (method === 'POST' && path === `${PREFIX}/seen`) {\n const body = JSON.parse(await readBody(req)) as { sessionId?: string; all?: boolean }\n if (body.all) {\n board.markAllSeen()\n send(res, 200, { ok: true })\n return\n }\n if (!body.sessionId) {\n send(res, 400, { ok: false, error: 'sessionId required' })\n return\n }\n board.markSeen(body.sessionId)\n send(res, 200, { ok: true })\n return\n }\n\n if (method === 'POST' && path === `${PREFIX}/approve`) {\n const body = JSON.parse(await readBody(req)) as { id?: string; outcome?: ApprovalOutcomeWire }\n if (!body.id || (body.outcome !== 'allowed-once' && body.outcome !== 'rejected')) {\n send(res, 400, { ok: false, error: 'id and outcome required' })\n return\n }\n if (!board.decideApproval(body.id, body.outcome)) {\n send(res, 404, { ok: false, error: 'not pending' })\n return\n }\n send(res, 200, { ok: true })\n return\n }\n\n if (method === 'POST' && path === `${PREFIX}/answer`) {\n const body = JSON.parse(await readBody(req)) as { id?: string; answers?: NotchAnswerItem[] }\n if (!body.id || !Array.isArray(body.answers)) {\n send(res, 400, { ok: false, error: 'id and answers required' })\n return\n }\n if (!board.answerAsk(body.id, body.answers)) {\n send(res, 404, { ok: false, error: 'not pending' })\n return\n }\n send(res, 200, { ok: true })\n return\n }\n\n if (method === 'POST' && path === `${PREFIX}/focus`) {\n const body = JSON.parse(await readBody(req)) as { sessionId?: string }\n if (!body.sessionId) {\n send(res, 400, { ok: false, error: 'sessionId required' })\n return\n }\n if (!board.requestFocus(body.sessionId)) {\n send(res, 404, { ok: false, error: 'unknown session' })\n return\n }\n send(res, 200, { ok: true })\n return\n }\n\n send(res, 404, { ok: false, error: 'not found' })\n }\n\n const disposeRoute = ctx.webServer.register({\n kind: 'prefix',\n path: PREFIX,\n handler: (req, res) => {\n void handler(req, res).catch((error: unknown) => {\n if (!res.headersSent) send(res, 400, { ok: false, error: String(error) })\n })\n },\n })\n\n return () => {\n stopListen()\n for (const res of streams.values()) {\n try { res.end() } catch { /* already closed */ }\n }\n streams.clear()\n disposeRoute()\n }\n}\n","import type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport { createWriteStream, existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { superviseNotch } from '../desktop/notch-lifecycle.mjs'\nimport type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'\nimport type { AskUserQuestionAnswer, AskUserQuestionRequest } from '@deepseek-ai/dsh-user-questions'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport type {} from '@deepseek-ai/dsh-session'\nimport { Board } from './board.ts'\nimport { attachHttp } from './http.ts'\nimport { DIR, RUNTIME, loadOrCreateToken, writeRuntime } from './store.ts'\n\nexport const name = 'dsh-notch'\nexport const inject = ['sessions', 'webServer', 'approval', 'userQuestions', 'agents']\nexport const Config = Schema.object({\n helperPath: Schema.string().default('').description('Optional installed native Notch executable; managed for this Host lifetime.'),\n})\n\nexport function apply(ctx: Context, config: { helperPath?: string } = {}) {\n console.log('[my-plugins/dsh-notch] loaded')\n const board = new Board(ctx)\n const token = loadOrCreateToken()\n const origin = `http://${ctx.webServer.host}:${String(ctx.webServer.port)}`\n writeRuntime({\n origin,\n token,\n pid: process.pid,\n writtenAt: Date.now(),\n })\n if (config.helperPath) {\n if (!existsSync(config.helperPath)) throw new Error('Configured Notch helper does not exist')\n ctx.effect(() => {\n const log = createWriteStream(join(DIR, 'helper.log'), { flags: 'a', mode: 0o600 })\n const helper = superviseNotch({ bin: config.helperPath, pidPath: join(DIR, 'helper.pid'), log,\n env: { ...process.env, DSH_NOTCH_RUNTIME_FILE: RUNTIME } })\n return () => { helper.stop(); log.end() }\n }, 'dsh-notch: native helper')\n }\n\n ctx.effect(() => attachHttp(ctx, board, token, origin), 'dsh-notch: http')\n\n const notify = debounce(() => board.notify(), 80)\n ctx.effect(() => notify.dispose, 'dsh-notch: notification timer')\n ctx.effect(() => ctx.on('session/created', notify), 'dsh-notch: created')\n ctx.effect(() => ctx.on('session/disposed', notify), 'dsh-notch: disposed')\n ctx.effect(() => ctx.on('session/event', (session, event) => {\n const type = String(event.type)\n if (type === 'turn/end') board.noteTurnEnd(session, event.time)\n if (type === 'turn/start' || type === 'turn/end' || type === 'session/title') {\n notify()\n }\n // When a turn starts or user sends a message in DSH, mark session as read\n if (type === 'turn/start' || (type === 'user/message' && (event.data as Record<string, unknown>)?.source === 'user')) {\n board.markSeen(session.id)\n }\n }), 'dsh-notch: events')\n ctx.effect(() => ctx.on('agent/status', notify), 'dsh-notch: agent-status')\n\n\n // Only intercept user-questions/request (AskUserQuestion).\n // Do NOT intercept internal tool approval/request which causes false alarms\n // when background agent tools run sandbox checks.\n ctx.on('user-questions/request', (\n request: AskUserQuestionRequest,\n next: () => Promise<AskUserQuestionAnswer>,\n ) => board.holdAsk(request, next), { prepend: true })\n}\n\nfunction debounce(fn: () => void, ms: number): (() => void) & { dispose: () => void } {\n let timer: ReturnType<typeof setTimeout> | undefined\n const notify = () => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => { timer = undefined; fn() }, ms)\n }\n return Object.assign(notify, { dispose: () => { if (timer) clearTimeout(timer); timer = undefined } })\n}\n"],"mappings":";;;;;;;;AAKA,SAAgB,gBAAgB,KAAK;CACnC,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,OAAO,GAAG,OAAO,EAAE,OAAO,OAAO;CACnE,IAAI;EACF,MAAM,OAAO,aAAa,WAAW;GAAC;GAAM,OAAO,GAAG;GAAG;GAAM;GAAW;GAAM;EAAO,GACrF;GAAE,UAAU;GAAQ,KAAK;IAAE,GAAG,QAAQ;IAAK,QAAQ;GAAI;GAAG,SAAS;EAAK,CAAC,CAAC,CAAC,KAAK;EAClF,MAAM,QAAQ,mBAAmB,KAAK,IAAI;EAC1C,IAAI,OAAO,OAAO;GAAE,OAAO;GAAS,OAAO,MAAM;GAAI,SAAS,MAAM;EAAG;CACzE,QAAQ,CAAC;CACT,IAAI;EAAE,QAAQ,KAAK,KAAK,CAAC;CAAE,SAAS,OAAO;EACzC,IAAI,MAAM,SAAS,SAAS,OAAO,EAAE,OAAO,OAAO;CACrD;CACA,OAAO,EAAE,OAAO,UAAU;AAC5B;AAEA,SAAgB,gBAAgB,KAAK;CACnC,IAAI,CAAC,OAAO,cAAc,GAAG,KAAK,OAAO,GAAG,OAAO;CACnD,IAAI;EAAE,QAAQ,KAAK,KAAK,CAAC;EAAG,OAAO;CAAQ,SAAS,OAAO;EACzD,OAAO,MAAM,SAAS,UAAU,SAAS;CAC3C;AACF;;AAGA,SAAgB,eAAe,EAAE,KAAK,SAAS,KAAK,WAAW,KAAK,MAAM,QAAQ,KAChF,QAAQ,iBAAiB,QAAQ,iBAAiB,SAAS,OAAO,SAAS,QAAQ,KAAK,KAAK,OAAO,KAAK;CACzG,IAAI,SAAS,SAAS,OAAO,UAAU;CACvC,MAAM,SAAS,CAAC;CAChB,MAAM,QAAO,SAAQ,IAAI,MAAM,WAAW,KAAK,GAAG;CAClD,MAAM,kBAAkB;EACtB,IAAI;GAAE,OAAO,OAAO,aAAa,SAAS,MAAM,CAAC,CAAC,KAAK,CAAC;EAAE,QAAQ;GAAE,OAAO;EAAE;CAC/E;CACA,MAAM,SAAQ,QAAO;EACnB,MAAM,WAAW,MAAM,GAAG;EAC1B,OAAO,SAAS,UAAU,WAAW,QAAQ,SAAS,OAAO,MAAM,QAAQ,GAAG,IAC1E;GAAE;GAAK,GAAG;GAAU,WAAW,KAAK,IAAI;EAAE,IAAI,KAAA;CACpD;CACA,MAAM,iBAAiB;EACrB,aAAa,KAAK;EAClB,IAAI,CAAC,SAAS;GAAE,QAAQ,WAAW,WAAW,QAAQ;GAAG,MAAM,QAAQ;EAAE;CAC3E;CACA,SAAS,YAAY;EACnB,IAAI,SAAS;EACb,IAAI,SAAS;GAAE,SAAS;GAAG;EAAO;EAGlC,IAAI;GACF,IAAI,KAAK,IAAI,IAAI,SAAS,GAAG,QAAQ,UAAU,CAAC,CAAC,UAAU,KAAO;IAAE,SAAS;IAAG;GAAO;EACzF,QAAQ,CAAC;EACT,IAAI,SAAS;GACX,MAAM,QAAQ,MAAM,QAAQ,GAAG;GAC/B,IAAI,UAAU,aAAc,UAAU,WAAW,KAAK,IAAI,IAAI,QAAQ,YAAY,KAAO;IACvF,SAAS;IAAG;GACd;GACA,MAAM,OAAO,MAAM,QAAQ,GAAG;GAC9B,IAAI,KAAK,UAAU,aAAc,KAAK,UAAU,WAAW,KAAK,UAAU,QAAQ,SAAS,KAAK,YAAY,QAAQ,SAAU;IAC5H,QAAQ,YAAY,KAAK,IAAI;IAC7B,SAAS;IAAG;GACd;GACA,UAAU,KAAA;EACZ;EACA,MAAM,MAAM,UAAU;EAEtB,IADiB,MAAM,GACZ,CAAC,CAAC,UAAU,WAAW;GAAE,SAAS;GAAG;EAAO;EACvD,UAAU,MAAM,GAAG;EACnB,IAAI,SAAS;GAAE,KAAK,SAAS,QAAQ,KAAK;GAAG,SAAS;GAAG;EAAO;EAChE,OAAO,OAAO,UAAU,KAAK,IAAI,IAAI,OAAO,KAAK,KAAO,OAAO,MAAM;EACrE,IAAI,OAAO,UAAU,GAAG;GAAE,KAAK,wCAAwC;GAAG,UAAU;GAAM;EAAO;EACjG,OAAO,KAAK,KAAK,IAAI,CAAC;EACtB,MAAM,QAAQ,OAAO,KAAK,CAAC,GAAG;GAAE,OAAO;IAAC;IAAU;IAAQ;GAAM;GAAG,UAAU;GAAO;EAAI,CAAC;EACzF,UAAU;EACV,MAAM,QAAQ,KAAK,KAAK,EAAE,KAAK,MAAM,CAAC;EACtC,MAAM,QAAQ,KAAK,KAAK,EAAE,KAAK,MAAM,CAAC;EACtC,MAAM,KAAK,eAAe;GACxB,IAAI,YAAY,OAAO;GACvB,UAAU,KAAA;GACV,IAAI,SAAS;IAAE,MAAM,KAAK,SAAS;IAAG;GAAO;GAC7C,UAAU,MAAM,MAAM,GAAG;GAEzB,IAAI,CAAC,SAAS;IAAE,MAAM,KAAK,SAAS;IAAG,SAAS;IAAG;GAAO;GAC1D,UAAU,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;GAC/C,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,IAAI;GACxC,cAAc,OAAO,GAAG,MAAM,IAAI,GAAG;GACrC,WAAW,OAAO,OAAO;GACzB,KAAK,SAAS,MAAM,KAAK;GACzB,SAAS;EACX,CAAC;EACD,MAAM,KAAK,UAAS,UAAS;GAAE,IAAI,YAAY,OAAO,UAAU,KAAA;GAAW,KAAK,gBAAgB,MAAM,SAAS;GAAG,SAAS;EAAE,CAAC;EAC9H,MAAM,KAAK,cAAc;GACvB,IAAI,YAAY,OAAO,UAAU,KAAA;GACjC,IAAI,SAAS,QAAQ,MAAM,KAAK,UAAU,KAAA;GAC1C,SAAS;EACX,CAAC;EACD,SAAS;CACX;CACA,MAAM,aAAa;EACjB,UAAU;EACV,aAAa,KAAK;EAClB,SAAS,KAAK,SAAS;EAEvB,MAAM,aAAa,CAAC,SAAS,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,OAAO;EAC/D,MAAM,uBAAO,IAAI,IAAI;EACrB,KAAK,MAAM,QAAQ,YAAY;GAC7B,IAAI,KAAK,IAAI,KAAK,GAAG,GAAG;GACxB,KAAK,IAAI,KAAK,GAAG;GACjB,MAAM,OAAO,MAAM,KAAK,GAAG;GAC3B,IAAI,CAAC,QAAQ,KAAK,UAAU,KAAK,OAAO;GACxC,IAAI;IAAE,OAAO,KAAK,KAAK,SAAS;IAAG,KAAK,QAAQ,KAAK,KAAK;GAAE,SAAS,OAAO;IAC1E,IAAI,MAAM,SAAS,SAAS,KAAK,gBAAgB,MAAM,SAAS;GAClE;GACA,IAAI,UAAU,MAAM,KAAK,KAAO,IAAI;IAAE,WAAW,OAAO;GAAE,QAAQ,CAAC;EACrE;EACA,UAAU,KAAA;CACZ;CACA,UAAU;CACV,OAAO;EAAE,IAAI,MAAM;GAAE,OAAO,SAAS,OAAO,SAAS;EAAI;EAAG;EAAM,MAAM;CAAK;AAC/E;;;AC/GA,MAAM,yBAAS,IAAI,IAAI;CAAC;CAAS;CAAW;AAAY,CAAC;AAEzD,SAAgB,YAAY,SAAiC;CAC3D,IAAI,OAAO;CACX,IAAI;CACJ,KAAK,MAAM,SAAS,QAAQ,eAAe,GAAG;EAC5C,IAAI,MAAM,SAAS,cAAc;GAC/B,OAAO;GACP,WAAW,KAAA;GACX;EACF;EACA,IAAI,MAAM,SAAS,YAAY;GAC7B,OAAO;GACP,MAAM,OAAO,MAAM,KAAK,OAAO;GAC/B,WAAW;IAAE,IAAI,MAAM;IAAM;IAAM,QAAQ,OAAO,IAAI,IAAI;GAAE;EAC9D;CACF;CACA,OAAO;EAAE;EAAM;CAAS;AAC1B;AAEA,SAAgB,eAAe,SAA2B;CAGxD,OAAO,QAAQ,OAAO,WAAW;AACnC;;AAGA,SAAgB,kBAAkB,SAAkB,UAA6D;CAC/G,IAAI,UAA+B;CACnC,MAAM,0BAAU,IAAI,IAAY;CAChC,OAAO,WAAW,eAAe,OAAO,GAAG;EACzC,IAAI,QAAQ,IAAI,QAAQ,EAAE,GAAG,OAAO,KAAA;EACpC,QAAQ,IAAI,QAAQ,EAAE;EACtB,UAAU,QAAQ,OAAO,kBAAkB,KAAA,IAAY,KAAA,IAAY,SAAS,IAAI,QAAQ,OAAO,aAAa;CAC9G;CACA,OAAO;AACT;;;ACvCA,MAAa,MAAM,KAAK,eAAe,GAAG,WAAW;AACrD,MAAa,UAAU,KAAK,KAAK,cAAc;AAC/C,MAAM,OAAO,KAAK,KAAK,WAAW;AASlC,SAAS,YAAkB;CACzB,UAAU,KAAK;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;AACjD;AAEA,SAAS,SAAY,MAA6B;CAChD,IAAI;EACF,OAAO,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC;CAC9C,QAAQ;EACN;CACF;AACF;AAEA,SAAgB,oBAA4B;CAC1C,MAAM,WAAW,SAAsB,OAAO;CAC9C,IAAI,UAAU,SAAS,SAAS,MAAM,UAAU,IAAI,OAAO,SAAS;CACpE,OAAO,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;AACvC;AAEA,SAAgB,aAAa,MAAyB;CACpD,UAAU;CACV,cAAc,SAAS,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,EAAE,KAAK;EAAE,UAAU;EAAQ,MAAM;CAAM,CAAC;AAChG;AAEA,SAAgB,WAAmC;CACjD,MAAM,QAAQ,SAAiC,IAAI;CACnD,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO,CAAC;CACjD,OAAO;AACT;AAEA,SAAgB,SAAS,KAAmC;CAC1D,UAAU;CACV,cAAc,MAAM,GAAG,KAAK,UAAU,GAAG,EAAE,KAAK;EAAE,UAAU;EAAQ,MAAM;CAAM,CAAC;AACnF;;;ACNA,IAAa,QAAb,MAAmB;CAoFY;CAnF7B;CACA,WAA4B,WAAW;CACvC,SAAqD,CAAC;CACtD,kBAA0B;CAC1B,eAAuB;;CAGvB,cAAuB;EACrB,OAAO;GAAE,eAAe;GAAG,UAAU,KAAK;GACxC,SAAS,KAAK,WAAW;IAAE,UAAU,KAAK,QAAQ;IAAU,IAAI,KAAK,QAAQ;IAAI,SAAS,KAAK,QAAQ;GAAQ;GAC/G,QAAQ,KAAK;EAAO;CACxB;CAEA,MAAc,OAAe,OAAsC;EACjE,KAAK,OAAO,KAAK;GAAE,IAAI,KAAK,IAAI;GAAG;GAAO,GAAG;EAAM,CAAC;EACpD,IAAI,KAAK,OAAO,SAAS,KAAK,KAAK,OAAO,MAAM;CAClD;CAEA,YAAY,OAAyB;EACnC,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;EAChD,MAAM,OAAO;EACb,IAAI,OAAO,KAAK,aAAa,YAAY,CAAC,MAAM,QAAQ,KAAK,IAAI,KAAK,KAAK,KAAK,SAAS,KAAM,OAAO;EACtG,MAAM,OAAqB,CAAC;EAC5B,KAAK,MAAM,OAAO,KAAK,MAAM;GAE3B,IAAI,CAAC,OAAO,OAAO,IAAI,OAAO,YAAY,IAAI,GAAG,WAAW,KAAK,OAAO,IAAI,UAAU,YAAY,OAAO,IAAI,cAAc,aAAa,OAAO,IAAI,YAAY,WAAW,OAAO;GACjL,KAAK,KAAK;IAAE,IAAI,IAAI;IAAI,OAAO,IAAI,MAAM,MAAM,GAAG,GAAG;IAAG,WAAW,IAAI;IAAW,SAAS,IAAI;IAC7F,GAAI,OAAO,IAAI,cAAc,YAAY,OAAO,SAAS,IAAI,SAAS,IAAI,EAAE,WAAW,IAAI,UAAU,IAAI,CAAC;GAAG,CAAC;EAClH;EACA,MAAM,WAAW,IAAI,IAAI,KAAK,IAAI,SAAS,KAAK,CAAC,CAAC,KAAI,YAAW,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;EAIvF,KAAK,MAAM,OAAO,MAAM;GACtB,IAAI,CAAC,IAAI,aAAa,IAAI,SAAS;GACnC,MAAM,UAAU,SAAS,IAAI,IAAI,EAAE;GACnC,IAAI,YAAY,eAAe,OAAO,KAAK,KAAK,OAAO,OAAO,IAAI;GAClE,MAAM,OAAO,UAAU,YAAY,OAAO,CAAC,CAAC,WAAW,KAAA;GACvD,MAAM,OAAO,KAAK,KAAK,IAAI;GAC3B,MAAM,cAAc,MAAM,MAAM,IAAI;GACpC,IAAI,SAAS,KAAA,MAAc,gBAAgB,KAAA,KAAa,QAAQ,cAAc;GAC9E,KAAK,mBAAmB,IAAI,IAAI,IAAI,GAAG;EACzC;EACA,MAAM,SAAS,KAAK;EACpB,IAAI,KAAK,sBAAsB,KAAK,KAAK,YAAY,QAAQ,UACxD,OAAO,OAAO,OAAO,YAAY,OAAO,OAAO,OAAO,YAAY,OAAO,SAAS,OAAO,EAAE,KAC3F,KAAK,MAAK,QAAO,IAAI,OAAO,OAAO,MAAM,CAAC,IAAI,OAAO,GAAG;GAC3D,MAAM,UAAU,SAAS,IAAI,OAAO,EAAE;GACtC,IAAI,CAAC,WAAY,CAAC,eAAe,OAAO,KAAK,CAAC,KAAK,iBAAiB,SAAS,QAAQ,GAAI,KAAK,YAAY,OAAO,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC;EACtJ;EAEA,IAAI,KAAK,sBAAsB,KAAK,KAAK,YAAY,QAAQ,KAAK,SAAS,aAAa,KAAK,UAC3F,KAAK,MAAM,YAAY,KAAK,QAAQ,MAAM;GACxC,IAAI,CAAC,SAAS,aAAa,KAAK,MAAK,QAAO,IAAI,OAAO,SAAS,MAAM,IAAI,SAAS,GAAG;GACtF,MAAM,UAAU,SAAS,IAAI,SAAS,EAAE;GACxC,MAAM,KAAK,UAAU,YAAY,OAAO,CAAC,CAAC,UAAU,KAAK,KAAK,IAAI;GAClE,IAAI,OAAO,KAAA,MAAc,CAAC,WAAW,CAAC,KAAK,iBAAiB,SAAS,QAAQ,IAAI,KAAK,YAAY,SAAS,IAAI,EAAE;EACnH;EAGF,IAAI,KAAK,WAAW,KAAK,QAAQ,aAAa,KAAK,YAAY,KAAK,YAAY,QAAQ,KAAK,IAAI,IAAI,KAAK,QAAQ,KAAK,KAAM;GAAE,KAAK,KAAK;GAAG,OAAO;EAAK;EACxJ,KAAK,UAAU;GAAE,UAAU,KAAK;GAAU,IAAI,KAAK,IAAI;GAAG,SAAS,KAAK,YAAY;GAAM;GACxF,GAAI,KAAK,sBAAsB,KAAK,KAAK,sBAAsB,IAAI,EAAE,mBAAmB,KAAK,kBAAkB,IAAI,CAAC;EAAG;EACzH,MAAM,cAAc;GAAE,UAAU,KAAK;GAAU,SAAS,KAAK,YAAY;GACvE,MAAM,KAAK,KAAK,EAAE,IAAI,SAAS,iBAAiB;IAAE;IAAI;IAAS;GAAU,EAAE;EAAE;EAC/E,MAAM,YAAY,KAAK,UAAU,WAAW;EAC5C,IAAI,cAAc,KAAK,iBAAiB;GAAE,KAAK,kBAAkB;GAAW,KAAK,MAAM,WAAW,WAAW;EAAE;EAC/G,KAAK,KAAK;EACV,OAAO;CACT;CAEA,cAAgD;EAC9C,OAAO,KAAK,WAAW,KAAK,IAAI,IAAI,KAAK,QAAQ,KAAK,MAAO,KAAK,QAAQ,OAAO,KAAA;CACnF;CAEA,0BAA2B,IAAI,IAAkB;CACjD,OAAwB,SAAS;CACjC,0BAAkB,IAAI,IAAY;CAClC,gCAAiC,IAAI,IAAY;CACjD,qCAAsC,IAAI,IAAwB;CAClE,4BAA6B,IAAI,IAAgB;CACjD,QAAmC;CAEnC,YAAY,KAA+B;EAAd,KAAA,MAAA;CAAe;CAE5C,SAAS,IAA4B;EACnC,KAAK,UAAU,IAAI,EAAE;EACrB,aAAa;GAAE,KAAK,UAAU,OAAO,EAAE;EAAE;CAC3C;CAEA,SAAe;EACb,KAAK,KAAK;CACZ;CAEA,OAAqB;EACnB,KAAK,MAAM,MAAM,KAAK,WACpB,IAAI;GAAE,GAAG;EAAE,SAAS,OAAO;GAAE,KAAK,IAAI,OAAO,KAAK,oCAAoC,OAAO,KAAK,CAAC;EAAE;CAEzG;CAEA,SAAS,QAA+B;EACtC,MAAM,OAAmB,CAAC;EAC1B,MAAM,WAAW,KAAK,IAAI,SAAS,KAAK;EACxC,MAAM,OAAO,IAAI,IAAqB,SAAS,KAAI,YAAW,CAAC,QAAQ,IAAI,OAAO,CAAC,CAAC;EACpF,MAAM,yBAAS,IAAI,IAAwB;EAC3C,KAAK,MAAM,WAAW,UAAU;GAC9B,MAAM,QAAQ,kBAAkB,SAAS,IAAI;GAC7C,IAAI,CAAC,OAAO;IAEV,MAAM,sBAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;IAChC,MAAM,WAAW,KAAK,aAAa,GAAG;IACtC,MAAM,MAAM,KAAK,QAAQ,GAAG;IAC5B,IAAI,YAAY,KAAK,KAAK,KAAK;KAC7B,IAAI,QAAQ;KAAI,OAAO,KAAK,QAAQ,OAAO;KAAG,OAAO;KACrD,MAAM;KAAO,QAAQ;KACrB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;KAAI,GAAI,MAAM,EAAE,IAAI,IAAI,CAAC;IAC1D,CAAC;IACD;GACF;GACA,MAAM,UAAU,OAAO,IAAI,KAAK,KAAK,CAAC;GACtC,QAAQ,KAAK,OAAO;GACpB,OAAO,IAAI,OAAO,OAAO;EAC3B;EACA,KAAK,MAAM,CAAC,OAAO,YAAY,QAAQ;GACrC,MAAM,MAAM,KAAK,OAAO,OAAO,OAAO;GACtC,IAAI,KAAK,KAAK,KAAK,GAAG;EACxB;EACA,MAAM,eAAe,KAAK,YAAY;EACtC,MAAM,WAAW,IAAI,IAAI,KAAK,kBAAkB;EAChD,KAAK,MAAM,UAAU,gBAAgB,CAAC,GACpC,IAAI,OAAO,SAAS,SAAS,IAAI,OAAO,IAAI,MAAM;EAEpD,KAAK,MAAM,UAAU,SAAS,OAAO,GAAG;GAGtC,IAAI,KAAK,IAAI,OAAO,EAAE,KAAM,CAAC,OAAO,aAAa,CAAC,OAAO,SAAU;GAInE,IAAI,OAAO,WAAW,CAAC,cAAc;GACrC,MAAM,OAAO,KAAK,KAAK,OAAO;GAC9B,IAAI,OAAO,aAAa,SAAS,KAAA,MAAc,OAAO,cAAc,KAAA,KAAa,QAAQ,OAAO,YAAY;GAC5G,KAAK,KAAK;IAAE,IAAI,OAAO;IAAI,OAAO,OAAO;IAAO,OAAO;IAAO,MAAM,OAAO;IAAS,QAAQ,OAAO;GAAU,CAAC;EAChH;EACA,KAAK,MAAM,GAAG,MAAM,OAAO,QAAQ,EAAE,YAAY,EAAE,GAAG,CAAC,IAAI,OAAO,QAAQ,EAAE,YAAY,EAAE,GAAG,CAAC,KACzF,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,IAAI,KAC9B,OAAO,EAAE,MAAM,IAAI,OAAO,EAAE,MAAM,MACjC,EAAE,UAAU,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE;EAClD,KAAK,UAAU,IAAI,IACjB,SAAS,QAAQ,YAAY,KAAK,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,YAAY,QAAQ,EAAE,CAChF;EACA,MAAM,YAAY,KAAK,KAAI,SAAQ;GAAE,IAAI,IAAI;GAAI,MAAM,IAAI;GAAM,QAAQ,IAAI;GAC3E,QAAQ,IAAI,UAAU;GAAI,QAAQ,QAAQ,IAAI,OAAO,IAAI,QAAQ;EAAE,EAAE;EACvE,MAAM,eAAe,KAAK,UAAU,SAAS;EAC7C,IAAI,iBAAiB,KAAK,cAAc;GACtC,KAAK,eAAe;GACpB,KAAK,MAAM,YAAY;IAAE,MAAM;IAAW,UAAU,KAAK,SAAS;IAChE,UAAU,KAAK,SAAS;IAAI,SAAS,CAAC,GAAG,KAAK,OAAO;IAAG,eAAe,CAAC,GAAG,KAAK,aAAa;GAAE,CAAC;EACpG;EACA,OAAO;GAAE,IAAI;GAAM,eAAe;GAAG,aAAa,KAAK,IAAI;GAAG;GAAQ;GACpE,iBAAiB,eAAe,KAAK,SAAS,KAAK,KAAA;GACnD,0BAA0B,eAAe,KAAK,SAAS,oBAAoB,KAAA;EAAU;CACzF;CAEA,SAAS,WAAyB;EAChC,KAAK,cAAc,OAAO,SAAS;EACnC,KAAK,mBAAmB,OAAO,SAAS;EACxC,KAAK,KAAK,aAAa,KAAK,IAAI;EAChC,SAAS,KAAK,IAAI;EAClB,KAAK,KAAK;CACZ;CAEA,cAAoB;EAClB,KAAK,cAAc,MAAM;EACzB,MAAM,MAAM,KAAK,IAAI;EACrB,KAAK,MAAM,MAAM,KAAK,mBAAmB,KAAK,GAAG,KAAK,KAAK,MAAM;EACjE,KAAK,mBAAmB,MAAM;EAC9B,KAAK,MAAM,WAAW,KAAK,IAAI,SAAS,KAAK,GAC3C,KAAK,KAAK,QAAQ,MAAM;EAE1B,SAAS,KAAK,IAAI;EAClB,KAAK,KAAK;CACZ;CAEA,YAAoB,WAAmB,IAAkB;EACvD,MAAM,OAAO,KAAK,KAAK;EACvB,KAAK,QAAQ,cAAc,IAAI;EAC/B,MAAM,UAAU,KAAK,IAAI,SAAS,KAAK,CAAC,CAAC,MAAK,SAAQ,KAAK,OAAO,SAAS;EAE3E,MAAM,eADO,WAAW,YAAY,OAAO,CAAC,CAAC,SAAA,EACnB,MAAM,KAAK,mBAAmB,IAAI,SAAS,CAAC,EAAE;EAGxE,IAAI,SAAS,KAAA,MAAc,gBAAgB,KAAA,KAAa,QAAQ,cAAc;EAC9E,KAAK,KAAK,aAAa;EACvB,IAAI,gBAAgB,KAAA,KAAa,eAAe,IAAI;GAClD,KAAK,cAAc,OAAO,SAAS;GACnC,KAAK,mBAAmB,OAAO,SAAS;EAC1C;EACA,SAAS,KAAK,IAAI;EAClB,KAAK,MAAM,QAAQ;GAAE;GAAW,SAAS;EAAG,CAAC;CAC/C;CAEA,YAAY,SAAkB,KAAK,YAAY,OAAO,CAAC,CAAC,UAAU,IAAU;EAC1E,IAAI,eAAe,OAAO,GAAG;EAC7B,IAAI,OAAO,KAAA,MAAc,KAAK,KAAK,QAAQ,OAAO,aAAa,IAAI,KAAK,cAAc,IAAI,QAAQ,EAAE;CACtG;;;;;;CAOA,aAAa,WAA4B;EACvC,MAAM,QAAQ,KAAK,IAAI,SAAS,KAAK,CAAC,CAAC,MAAM,YAAY,QAAQ,OAAO,SAAS;EACjF,MAAM,WAAW,KAAK,mBAAmB,IAAI,SAAS,KAAK,KAAK,YAAY,CAAC,EAAE,MAAK,QAAO,IAAI,OAAO,SAAS;EAC/G,IAAI,CAAC,SAAS,CAAC,UAAU,OAAO;EAChC,KAAK,QAAQ;GAAE;GAAW,IAAI,KAAK,IAAI;EAAE;EACzC,KAAK,KAAK;EACV,OAAO;CACT;CAEA,YAA+B;EAC7B,IAAI,KAAK,UAAU,MAAM,OAAO;EAChC,IAAI,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,KAAQ;GACvC,KAAK,QAAQ;GACb,OAAO;EACT;EACA,OAAO,KAAK;CACd;CAEA,eAAe,IAAY,SAAuC;EAChE,MAAM,OAAO,KAAK,QAAQ,IAAI,EAAE;EAChC,IAAI,CAAC,QAAQ,KAAK,SAAS,YAAY,OAAO;EAC9C,KAAK,QAAQ,OAAO,EAAE;EACtB,KAAK,KAAK,KAAK,aAAa,KAAK,IAAI;EACrC,SAAS,KAAK,IAAI;EAClB,KAAK,QAAQ,OAAO;EACpB,KAAK,KAAK;EACV,OAAO;CACT;CAEA,UAAU,IAAY,SAAqC;EACzD,MAAM,OAAO,KAAK,QAAQ,IAAI,EAAE;EAChC,IAAI,CAAC,QAAQ,KAAK,SAAS,OAAO,OAAO;EACzC,KAAK,QAAQ,OAAO,EAAE;EACtB,KAAK,KAAK,KAAK,aAAa,KAAK,IAAI;EACrC,SAAS,KAAK,IAAI;EAClB,KAAK,QAAQ,EAAE,QAAQ,CAAC;EACxB,KAAK,KAAK;EACV,OAAO;CACT;CAEA,aAAa,SAA0B,MAAgE;EACrG,MAAM,YAAY,OAAO,QAAQ,MAAM,EAAE;EACzC,MAAM,KAAK,WAAW;EACtB,MAAM,YAAY,IAAI,SAA0B,YAAY;GAC1D,MAAM,OAAqB;IACzB,MAAM;IACN;IACA;IACA,UAAU,QAAQ;IAClB;GACF;GACA,IAAI,QAAQ,QAAQ,KAAK,SAAS,QAAQ;GAC1C,KAAK,QAAQ,IAAI,IAAI,IAAI;GACzB,KAAK,KAAK;GACV,QAAQ,QAAQ,iBAAiB,eAAe;IAC9C,IAAI,CAAC,KAAK,QAAQ,OAAO,EAAE,GAAG;IAC9B,KAAK,KAAK;GACZ,GAAG,EAAE,MAAM,KAAK,CAAC;EACnB,CAAC;EACD,OAAO,QAAQ,KAAK,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc;GACrD,IAAI,KAAK,QAAQ,OAAO,EAAE,GAAG,KAAK,KAAK;EACzC,CAAC;CACH;CAEA,QAAQ,SAAiC,MAA4E;EACnH,MAAM,YAAY,QAAQ,QAAQ,OAAO,QAAQ,MAAM,EAAE,IAAI;EAC7D,IAAI,CAAC,WAAW,OAAO,KAAK;EAC5B,MAAM,iBAAiB,QAAQ;EAC/B,MAAM,aAAa,IAAI,gBAAgB;EACvC,QAAQ,SAAS,iBACb,YAAY,IAAI,CAAC,gBAAgB,WAAW,MAAM,CAAC,IACnD,WAAW;EACf,MAAM,KAAK,WAAW;EACtB,MAAM,YAA6B,QAAQ,UAAU,KAAK,UAAU;GAClE,IAAI,KAAK;GACT,UAAU,KAAK;GACf,GAAI,KAAK,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;GAC3D,GAAI,KAAK,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;GAC3D,GAAI,KAAK,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC9D,GAAI,KAAK,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa,KAAK,YAAY;GAC1E,GAAI,KAAK,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;EAC7D,EAAE;EACF,MAAM,YAAY,IAAI,SAAgC,YAAY;GAChE,KAAK,QAAQ,IAAI,IAAI;IAAE,MAAM;IAAO;IAAI;IAAW;IAAW;GAAQ,CAAC;GACvE,KAAK,KAAK;GACV,QAAQ,QAAQ,iBAAiB,eAAe;IAC9C,IAAI,CAAC,KAAK,QAAQ,OAAO,EAAE,GAAG;IAC9B,KAAK,KAAK;GACZ,GAAG,EAAE,MAAM,KAAK,CAAC;EACnB,CAAC;EACD,OAAO,QAAQ,KAAK,CAAC,WAAW,QAAQ,QAAQ,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc;GAG3E,WAAW,sBAAM,IAAI,MAAM,2CAA2C,CAAC;GACvE,IAAI,mBAAmB,KAAA,GAAW,OAAO,QAAQ;QAC5C,QAAQ,SAAS;GACtB,IAAI,KAAK,QAAQ,OAAO,EAAE,GAAG,KAAK,KAAK;EACzC,CAAC;CACH;CAEA,OAAe,SAA2B;EACxC,OAAO,KAAK,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC,EAAE,WAAW;CACrD;CAEA,iBAAyB,OAAgB,UAAiD;EACxF,KAAK,MAAM,UAAU,SAAS,OAAO,GACnC,IAAI,kBAAkB,QAAQ,QAAQ,CAAC,EAAE,OAAO,MAAM,OAChD,KAAK,OAAO,MAAM,KAAK,KAAK,eAAe,MAAM,IAAI,OAAO;EAEpE,OAAO;CACT;CAEA,eAAuB,SAA2B;EAChD,MAAM,QAAQ,KAAK,IAAI,OAAO,IAAI,QAAQ,EAAE;EAC5C,IAAI,CAAC,OAAO,OAAO;EAKnB,OADa,KAAK,IAAI,IAAI,MAChB,CAAC,EAAE,KAAK,MAAM,EAAE,CAAC,CAAC,MAAK,QAAO,IAAI,UAAU,QAAQ,MACzD,IAAI,SAAS,eACZ,IAAI,WAAW,aAAa,IAAI,WAAW,WAAW,KAAK;CACnE;CAEA,OAAe,SAAkB,SAA0C;EACzE,MAAM,SAAS,YAAY,OAAO;EAClC,MAAM,QAAQ,eAAe,OAAO;EACpC,MAAM,WAAW,KAAK,KAAK,QAAQ;EACnC,MAAM,OAAO,QAAQ,MAAK,WAAU,KAAK,OAAO,MAAM,KAAK,KAAK,eAAe,MAAM,CAAC;EAEtF,IAAI,OAAO,MACT,KAAK,cAAc,OAAO,QAAQ,EAAE;OAC/B,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE,KAAK,OAAO,UAChD,KAAK,cAAc,IAAI,QAAQ,EAAE;EAGnC,MAAM,SAAS,EADG,aAAa,KAAA,MAAc,OAAO,aAAa,KAAA,KAAa,YAAY,OAAO,SAAS,QAC7E,CAAC,SACxB,KAAK,cAAc,IAAI,QAAQ,EAAE,KAAK,KAAK,mBAAmB,IAAI,QAAQ,EAAE;EAClF,MAAM,YAAY,IAAI,IAAI,QAAQ,KAAI,WAAU,OAAO,EAAE,CAAC;EAG1D,MAAM,WAAW,KAAK,aAAa,SAAS;EAC5C,MAAM,MAAM,KAAK,QAAQ,SAAS;EAClC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,YAAY,CAAC,KAAK,OAAO,KAAA;EAClD,MAAM,QAAQ,KAAK,QAAQ,OAAO;EAClC,MAAM,MAAgB;GACpB,IAAI,QAAQ;GACZ;GACA;GACA;GACA;EACF;EACA,IAAI,OAAO,YAAY,CAAC,MAAM,IAAI,WAAW,OAAO;EACpD,IAAI,UAAU,IAAI,WAAW;EAC7B,IAAI,KAAK,IAAI,MAAM;EACnB,OAAO;CACT;CAEA,aAAqB,YAA4D;EAC/E,KAAK,MAAM,QAAQ,KAAK,QAAQ,OAAO,GACrC,IAAI,KAAK,SAAS,cAAc,WAAW,IAAI,KAAK,SAAS,GAC3D,OAAO;GACL,IAAI,KAAK;GACT,UAAU,KAAK;GACf,GAAI,KAAK,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO;EAC7D;CAIN;CAEA,QAAgB,YAAuD;EACrE,KAAK,MAAM,QAAQ,KAAK,QAAQ,OAAO,GACrC,IAAI,KAAK,SAAS,SAAS,WAAW,IAAI,KAAK,SAAS,GACtD,OAAO;GAAE,IAAI,KAAK;GAAI,WAAW,KAAK;EAAU;CAItD;CAEA,QAAgB,SAA0B;EAExC,MAAM,QADU,KAAK,IAAI,IAAI,cACT,CAAC,EAAE,IAAI,OAAO,CAAC,EAAE,OAAO,KAAK;EACjD,IAAI,OAAO,OAAO;EAClB,OAAO,QAAQ,GAAG,MAAM,GAAG,CAAC;CAC9B;AACF;;;ACnbA,MAAM,SAAS;AAEf,SAAgB,WAAW,KAA+B;CACxD,MAAM,KAAK,IAAI,OAAO,iBAAiB;CACvC,OAAO,OAAO,eAAe,OAAO,SAAS,OAAO,qBAAqB,OAAO;AAClF;AAEA,SAAgB,WAAW,KAAsB,OAAwB;CAEvE,IADe,IAAI,QAAQ,kBACZ,UAAU,SAAS,OAAO;CACzC,IAAI;EAEF,OAAO,IADS,IAAI,IAAI,OAAO,KAAK,kBAC3B,CAAC,CAAC,aAAa,IAAI,OAAO,MAAM;CAC3C,QAAQ;EACN,OAAO;CACT;AACF;;;;;;AAOA,SAAgB,eAAe,KAA+B;CAC5D,IAAI,CAAC,WAAW,GAAG,GAAG,OAAO;CAC7B,IAAI,IAAI,QAAQ,sBAAsB,cAAc,OAAO;CAC3D,MAAM,OAAO,IAAI,QAAQ;CACzB,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,MAAM,SAAS,IAAI,QAAQ;CAC3B,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,IAAI;EACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,UAAU,MAAM,CAAC,CAAC;CAC5D,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,eAAe,KAAsB,QAAyB;CAC5E,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,IAAI,QAAQ,eAAe,EAAE,WAAW,kBAAkB,GAAG,OAAO;CACjG,IAAI,IAAI,QAAQ,WAAW,KAAA,GAAW,OAAO;CAG7C,OAAO,IAAI,QAAQ,0BAA0B,OACxC,IAAI,QAAQ,sBAAsB,KAAA,KAClC,IAAI,QAAQ,SAAS,IAAI,IAAI,MAAM,CAAC,CAAC;AAC5C;AAEA,SAAS,KAAK,KAAqB,QAAgB,MAAqB;CACtE,MAAM,OAAO,KAAK,UAAU,IAAI;CAChC,IAAI,UAAU,QAAQ;EACpB,gBAAgB;EAChB,iBAAiB;CACnB,CAAC;CACD,IAAI,IAAI,IAAI;AACd;AAEA,SAAS,SAAS,KAAsB,QAAQ,KAAK,MAAuB;CAC1E,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ,MAAM;GACd,IAAI,OAAO,OAAO;IAChB,uBAAO,IAAI,MAAM,gBAAgB,CAAC;IAClC,IAAI,QAAQ;IACZ;GACF;GACA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,IAAI,GAAG,aAAa,QAAQ,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,CAAC;EACnE,IAAI,GAAG,SAAS,MAAM;CACxB,CAAC;AACH;AAEA,SAAgB,WAAW,KAAc,OAAc,OAAe,QAA4B;CAChG,MAAM,0BAAU,IAAI,IAA4B;CAChD,MAAM,aAAa;EACjB,MAAM,UAAU,SAAS,KAAK,UAAU,MAAM,SAAS,MAAM,CAAC,EAAE;EAChE,KAAK,MAAM,CAAC,IAAI,QAAQ,SACtB,IAAI;GACF,IAAI,MAAM,OAAO;EACnB,QAAQ;GACN,QAAQ,OAAO,EAAE;EACnB;CAEJ;CACA,MAAM,aAAa,MAAM,SAAS,IAAI;CAEtC,MAAM,UAAU,OAAO,KAAsB,QAAuC;EAElF,MAAM,OAAO,IADG,IAAI,IAAI,OAAO,KAAK,kBACrB,CAAC,CAAC;EACjB,MAAM,SAAS,IAAI,UAAU;EAK7B,IAAI,WAAW,SAAS,SAAS,GAAG,OAAO,iBAAiB;GAC1D,IAAI,CAAC,eAAe,GAAG,GAAG;IACxB,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IAChD;GACF;GACA,KAAK,KAAK,KAAK;IAAE,IAAI;IAAM,OAAO,MAAM,UAAU;GAAE,CAAC;GACrD;EACF;EAEA,IAAI,WAAW,UAAU,SAAS,GAAG,OAAO,WAAW;GACrD,IAAI,CAAC,eAAe,KAAK,MAAM,GAAG;IAChC,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAY,CAAC;IAChD;GACF;GACA,MAAM,KAAK,MAAM,YAAY,KAAK,MAAM,MAAM,SAAS,KAAK,MAAM,IAAI,CAAC,CAAC;GACxE,KAAK,KAAK,KAAK,MAAM,KAAK,EAAE,GAAG,CAAC;GAChC;EACF;EAEA,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,WAAW,KAAK,KAAK,GAAG;GAC/C,KAAK,KAAK,KAAK;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC;GAChD;EACF;EAEA,IAAI,WAAW,SAAS,SAAS,GAAG,OAAO,UAAU;GACnD,KAAK,KAAK,KAAK,MAAM,SAAS,MAAM,CAAC;GACrC;EACF;EAEA,IAAI,WAAW,SAAS,SAAS,GAAG,OAAO,eAAe;GACxD,KAAK,KAAK,KAAK,MAAM,YAAY,CAAC;GAClC;EACF;EAEA,IAAI,WAAW,SAAS,SAAS,GAAG,OAAO,UAAU;GACnD,MAAM,KAAK,WAAW;GACtB,IAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,iBAAiB;IACjB,YAAY;GACd,CAAC;GACD,QAAQ,IAAI,IAAI,GAAG;GACnB,IAAI,MAAM,SAAS,KAAK,UAAU,MAAM,SAAS,MAAM,CAAC,EAAE,KAAK;GAC/D,IAAI,GAAG,eAAe;IAAE,QAAQ,OAAO,EAAE;GAAE,CAAC;GAC5C;EACF;EAEA,IAAI,WAAW,UAAU,SAAS,GAAG,OAAO,QAAQ;GAClD,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;GAC3C,IAAI,KAAK,KAAK;IACZ,MAAM,YAAY;IAClB,KAAK,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;IAC3B;GACF;GACA,IAAI,CAAC,KAAK,WAAW;IACnB,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IACzD;GACF;GACA,MAAM,SAAS,KAAK,SAAS;GAC7B,KAAK,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;GAC3B;EACF;EAEA,IAAI,WAAW,UAAU,SAAS,GAAG,OAAO,WAAW;GACrD,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;GAC3C,IAAI,CAAC,KAAK,MAAO,KAAK,YAAY,kBAAkB,KAAK,YAAY,YAAa;IAChF,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA0B,CAAC;IAC9D;GACF;GACA,IAAI,CAAC,MAAM,eAAe,KAAK,IAAI,KAAK,OAAO,GAAG;IAChD,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAc,CAAC;IAClD;GACF;GACA,KAAK,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;GAC3B;EACF;EAEA,IAAI,WAAW,UAAU,SAAS,GAAG,OAAO,UAAU;GACpD,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;GAC3C,IAAI,CAAC,KAAK,MAAM,CAAC,MAAM,QAAQ,KAAK,OAAO,GAAG;IAC5C,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAA0B,CAAC;IAC9D;GACF;GACA,IAAI,CAAC,MAAM,UAAU,KAAK,IAAI,KAAK,OAAO,GAAG;IAC3C,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAc,CAAC;IAClD;GACF;GACA,KAAK,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;GAC3B;EACF;EAEA,IAAI,WAAW,UAAU,SAAS,GAAG,OAAO,SAAS;GACnD,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,GAAG,CAAC;GAC3C,IAAI,CAAC,KAAK,WAAW;IACnB,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAqB,CAAC;IACzD;GACF;GACA,IAAI,CAAC,MAAM,aAAa,KAAK,SAAS,GAAG;IACvC,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO;IAAkB,CAAC;IACtD;GACF;GACA,KAAK,KAAK,KAAK,EAAE,IAAI,KAAK,CAAC;GAC3B;EACF;EAEA,KAAK,KAAK,KAAK;GAAE,IAAI;GAAO,OAAO;EAAY,CAAC;CAClD;CAEA,MAAM,eAAe,IAAI,UAAU,SAAS;EAC1C,MAAM;EACN,MAAM;EACN,UAAU,KAAK,QAAQ;GACrB,QAAa,KAAK,GAAG,CAAC,CAAC,OAAO,UAAmB;IAC/C,IAAI,CAAC,IAAI,aAAa,KAAK,KAAK,KAAK;KAAE,IAAI;KAAO,OAAO,OAAO,KAAK;IAAE,CAAC;GAC1E,CAAC;EACH;CACF,CAAC;CAED,aAAa;EACX,WAAW;EACX,KAAK,MAAM,OAAO,QAAQ,OAAO,GAC/B,IAAI;GAAE,IAAI,IAAI;EAAE,QAAQ,CAAuB;EAEjD,QAAQ,MAAM;EACd,aAAa;CACf;AACF;;;ACxNA,MAAa,OAAO;AACpB,MAAa,SAAS;CAAC;CAAY;CAAa;CAAY;CAAiB;AAAQ;AACrF,MAAa,SAAS,OAAO,OAAO,EAClC,YAAY,OAAO,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,YAAY,6EAA6E,EACnI,CAAC;AAED,SAAgB,MAAM,KAAc,SAAkC,CAAC,GAAG;CACxE,QAAQ,IAAI,+BAA+B;CAC3C,MAAM,QAAQ,IAAI,MAAM,GAAG;CAC3B,MAAM,QAAQ,kBAAkB;CAChC,MAAM,SAAS,UAAU,IAAI,UAAU,KAAK,GAAG,OAAO,IAAI,UAAU,IAAI;CACxE,aAAa;EACX;EACA;EACA,KAAK,QAAQ;EACb,WAAW,KAAK,IAAI;CACtB,CAAC;CACD,IAAI,OAAO,YAAY;EACrB,IAAI,CAAC,WAAW,OAAO,UAAU,GAAG,MAAM,IAAI,MAAM,wCAAwC;EAC5F,IAAI,aAAa;GACf,MAAM,MAAM,kBAAkB,KAAK,KAAK,YAAY,GAAG;IAAE,OAAO;IAAK,MAAM;GAAM,CAAC;GAClF,MAAM,SAAS,eAAe;IAAE,KAAK,OAAO;IAAY,SAAS,KAAK,KAAK,YAAY;IAAG;IACxF,KAAK;KAAE,GAAG,QAAQ;KAAK,wBAAwB;IAAQ;GAAE,CAAC;GAC5D,aAAa;IAAE,OAAO,KAAK;IAAG,IAAI,IAAI;GAAE;EAC1C,GAAG,0BAA0B;CAC/B;CAEA,IAAI,aAAa,WAAW,KAAK,OAAO,OAAO,MAAM,GAAG,iBAAiB;CAEzE,MAAM,SAAS,eAAe,MAAM,OAAO,GAAG,EAAE;CAChD,IAAI,aAAa,OAAO,SAAS,+BAA+B;CAChE,IAAI,aAAa,IAAI,GAAG,mBAAmB,MAAM,GAAG,oBAAoB;CACxE,IAAI,aAAa,IAAI,GAAG,oBAAoB,MAAM,GAAG,qBAAqB;CAC1E,IAAI,aAAa,IAAI,GAAG,kBAAkB,SAAS,UAAU;EAC3D,MAAM,OAAO,OAAO,MAAM,IAAI;EAC9B,IAAI,SAAS,YAAY,MAAM,YAAY,SAAS,MAAM,IAAI;EAC9D,IAAI,SAAS,gBAAgB,SAAS,cAAc,SAAS,iBAC3D,OAAO;EAGT,IAAI,SAAS,gBAAiB,SAAS,kBAAmB,MAAM,MAAkC,WAAW,QAC3G,MAAM,SAAS,QAAQ,EAAE;CAE7B,CAAC,GAAG,mBAAmB;CACvB,IAAI,aAAa,IAAI,GAAG,gBAAgB,MAAM,GAAG,yBAAyB;CAM1E,IAAI,GAAG,2BACL,SACA,SACG,MAAM,QAAQ,SAAS,IAAI,GAAG,EAAE,SAAS,KAAK,CAAC;AACtD;AAEA,SAAS,SAAS,IAAgB,IAAoD;CACpF,IAAI;CACJ,MAAM,eAAe;EACnB,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ,iBAAiB;GAAE,QAAQ,KAAA;GAAW,GAAG;EAAE,GAAG,EAAE;CAC1D;CACA,OAAO,OAAO,OAAO,QAAQ,EAAE,eAAe;EAAE,IAAI,OAAO,aAAa,KAAK;EAAG,QAAQ,KAAA;CAAU,EAAE,CAAC;AACvG"}
package/package.json CHANGED
@@ -1,29 +1,55 @@
1
1
  {
2
2
  "name": "dsh-notch",
3
- "version": "0.3.1",
3
+ "version": "0.3.6",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "registry": "https://registry.npmjs.org/"
7
+ },
4
8
  "type": "module",
5
- "main": "src/dsh-notch.ts",
9
+ "main": "lib/index.mjs",
6
10
  "description": "Native macOS notch for live DSH sessions, approvals, and model questions.",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/aa2246740/dsh-notch"
14
+ },
15
+ "homepage": "https://github.com/aa2246740/dsh-notch#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/aa2246740/dsh-notch/issues"
18
+ },
7
19
  "scripts": {
8
- "test": "node --import tsx --test tests/*.test.mjs",
20
+ "test": "node --import ./tests/isolate-home.mjs --import tsx --test tests/*.test.mjs",
9
21
  "test:motion": "sh macos/Tests/motion.sh",
10
22
  "test:geometry": "sh macos/Tests/geometry.sh",
11
23
  "build:macos": "swift build --package-path macos -c release",
12
24
  "test:outcome": "sh macos/Tests/outcome.sh",
25
+ "test:presentation": "sh macos/Tests/presentation.sh",
26
+ "test:edge-dock": "sh macos/Tests/edge-dock.sh",
27
+ "build:elastic-preview": "sh tools/elastic-preview/build.sh",
13
28
  "test:idle": "sh macos/Tests/idle.sh",
14
29
  "build:demo": "sh tools/recording/build.sh",
15
30
  "test:scrollbar": "sh macos/Tests/scrollbar.sh",
16
- "test:expanded-height": "sh macos/Tests/expanded-height.sh"
31
+ "test:expanded-height": "sh macos/Tests/expanded-height.sh",
32
+ "test:host-lifetime": "sh macos/Tests/host-lifetime.sh",
33
+ "build": "tsdown --config-loader native",
34
+ "test:desktop-launch": "sh macos/Tests/desktop-launch.sh"
17
35
  },
18
36
  "devDependencies": {
37
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
38
+ "semver": "7.8.5",
39
+ "tsdown": "^0.22.2",
19
40
  "tsx": "4.22.4"
20
41
  },
21
42
  "peerDependencies": {
22
- "@deepseek-ai/dsh": ">=0.1.7-rc.1 <0.1.8",
23
- "@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.1.8",
24
- "@deepseek-ai/dsh-session": ">=0.1.7-rc.1 <0.1.8",
25
- "@deepseek-ai/dsh-user-approval": ">=0.1.7-rc.1 <0.1.8",
26
- "@deepseek-ai/dsh-user-questions": ">=0.1.7-rc.1 <0.1.8"
43
+ "@deepseek-ai/dsh": ">=0.2.0-rc.1 <0.2.1",
44
+ "@deepseek-ai/dsh-home-paths": ">=0.2.0-rc.1 <0.2.1",
45
+ "@deepseek-ai/dsh-host-webserver": ">=0.2.0-rc.1 <0.2.1",
46
+ "@deepseek-ai/dsh-session": ">=0.2.0-rc.1 <0.2.1",
47
+ "@deepseek-ai/dsh-user-approval": ">=0.2.0-rc.1 <0.2.1",
48
+ "@deepseek-ai/dsh-user-questions": ">=0.2.0-rc.1 <0.2.1",
49
+ "@deepseek-ai/schemastery": "^3.18.4",
50
+ "@deepseek-ai/dsh-api-session-controller": ">=0.2.0-rc.1 <0.2.1",
51
+ "@deepseek-ai/dsh-client-ui-workspace": ">=0.2.0-rc.1 <0.2.1",
52
+ "@deepseek-ai/dsh-client-ui-session": ">=0.2.0-rc.1 <0.2.1"
27
53
  },
28
54
  "peerDependenciesMeta": {
29
55
  "@deepseek-ai/dsh": {
@@ -42,5 +68,35 @@
42
68
  "optional": true
43
69
  }
44
70
  },
45
- "license": "MIT"
71
+ "license": "MIT",
72
+ "dsh": {
73
+ "bundle": {
74
+ "patch": "./cordis.patch.yml"
75
+ },
76
+ "client": {
77
+ "platform": "web",
78
+ "inject": [
79
+ "@deepseek-ai/dsh-api-session-controller",
80
+ "@deepseek-ai/dsh-client-ui-workspace",
81
+ "@deepseek-ai/dsh-client-ui-session"
82
+ ]
83
+ }
84
+ },
85
+ "files": [
86
+ "src",
87
+ "cordis.patch.yml",
88
+ "package.json",
89
+ "README.md",
90
+ "LICENSE",
91
+ "lib",
92
+ "desktop",
93
+ "dshx.yml",
94
+ "README.en.md",
95
+ "docs"
96
+ ],
97
+ "exports": {
98
+ ".": "./lib/index.mjs",
99
+ "./client": "./lib/client.js",
100
+ "./package.json": "./package.json"
101
+ }
46
102
  }
package/src/board.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from 'node:crypto'
2
2
  import type { Context } from '@deepseek-ai/cordis'
3
3
  import type { Session } from '@deepseek-ai/dsh-session'
4
+ import type { JobRegistry } from '@deepseek-ai/dsh-jobs'
4
5
  import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
5
6
  import type {
6
7
  AskUserQuestionAnswer,
7
8
  AskUserQuestionRequest,
8
9
  } from '@deepseek-ai/dsh-user-questions'
9
- import { foldSession, isChildSession } from './session-state.ts'
10
+ import { conversationOwner, foldSession, isChildSession } from './session-state.ts'
10
11
  import { loadSeen, saveSeen } from './store.ts'
11
12
  import type {
12
13
  ApprovalOutcomeWire,
@@ -38,22 +39,75 @@ interface HeldAsk {
38
39
 
39
40
  type Held = HeldApproval | HeldAsk
40
41
 
41
- interface SidebarRow { id: string; title: string; completed: boolean; running: boolean }
42
+ interface SidebarRow { id: string; title: string; completed: boolean; running: boolean; updatedAt?: number }
42
43
  export class Board {
43
- private sidebar: { clientId: string; at: number; rows: SidebarRow[] } | undefined
44
+ private sidebar: { clientId: string; at: number; focused: boolean; rows: SidebarRow[]; projectionVersion?: number } | undefined
45
+ private readonly instance = randomUUID()
46
+ private readonly traces: Record<string, unknown>[] = []
47
+ private lastMirrorTrace = ''
48
+ private lastRowTrace = ''
49
+
50
+ /** Bounded, content-free diagnostics for state-source races. */
51
+ diagnostics(): unknown {
52
+ return { stateRevision: 4, instance: this.instance,
53
+ sidebar: this.sidebar && { clientId: this.sidebar.clientId, at: this.sidebar.at, focused: this.sidebar.focused },
54
+ traces: this.traces }
55
+ }
56
+
57
+ private trace(event: string, value: Record<string, unknown>): void {
58
+ this.traces.push({ at: Date.now(), event, ...value })
59
+ if (this.traces.length > 160) this.traces.shift()
60
+ }
44
61
 
45
62
  syncSidebar(input: unknown): boolean {
46
63
  if (!input || typeof input !== 'object') return false
47
- const data = input as { clientId?: unknown; focused?: unknown; rows?: unknown }
64
+ const data = input as { clientId?: unknown; focused?: unknown; rows?: unknown; projectionVersion?: unknown; viewed?: { id?: unknown; at?: unknown } }
48
65
  if (typeof data.clientId !== 'string' || !Array.isArray(data.rows) || data.rows.length > 1000) return false
49
66
  const rows: SidebarRow[] = []
50
67
  for (const row of data.rows) {
51
- if (!row || typeof row.id !== 'string' || !row.id.startsWith('session-') || typeof row.title !== 'string' || typeof row.completed !== 'boolean' || typeof row.running !== 'boolean') return false
52
- rows.push({ id: row.id, title: row.title.slice(0, 512), completed: row.completed, running: row.running })
68
+ // Session IDs are opaque; API-created conversations need no session- prefix.
69
+ if (!row || typeof row.id !== 'string' || row.id.length === 0 || typeof row.title !== 'string' || typeof row.completed !== 'boolean' || typeof row.running !== 'boolean') return false
70
+ rows.push({ id: row.id, title: row.title.slice(0, 512), completed: row.completed, running: row.running,
71
+ ...(typeof row.updatedAt === 'number' && Number.isFinite(row.updatedAt) ? { updatedAt: row.updatedAt } : {}) })
72
+ }
73
+ const sessions = new Map(this.ctx.sessions.list().map(session => [session.id, session]))
74
+ // Completion is a fact, not a page-local negative flag. A page that was
75
+ // reloaded or selected a different conversation cannot retract another
76
+ // page's reminder. Explicit, timestamped reading acknowledgements can.
77
+ for (const row of rows) {
78
+ if (!row.completed || row.running) continue
79
+ const session = sessions.get(row.id)
80
+ if (session && (isChildSession(session) || this.isBusy(session))) continue
81
+ const turn = session ? foldSession(session).lastTurn : undefined
82
+ const seen = this.seen[row.id]
83
+ const completedAt = turn?.at ?? row.updatedAt
84
+ if (seen !== undefined && (completedAt === undefined || seen >= completedAt)) continue
85
+ this.browserCompletions.set(row.id, row)
86
+ }
87
+ const viewed = data.viewed
88
+ if (data.projectionVersion === 3 && data.focused === true && viewed
89
+ && typeof viewed.id === 'string' && typeof viewed.at === 'number' && Number.isFinite(viewed.at)
90
+ && rows.some(row => row.id === viewed.id && !row.running)) {
91
+ const session = sessions.get(viewed.id)
92
+ if (!session || (!isChildSession(session) && !this.conversationBusy(session, sessions))) this.readThrough(viewed.id, Math.min(viewed.at, Date.now()))
93
+ }
94
+ // Legacy clients can acknowledge only a reminder they themselves showed.
95
+ if (data.projectionVersion !== 3 && data.focused === true && this.sidebar?.clientId === data.clientId) {
96
+ for (const previous of this.sidebar.rows) {
97
+ if (!previous.completed || rows.some(row => row.id === previous.id && row.completed)) continue
98
+ const session = sessions.get(previous.id)
99
+ const at = session ? foldSession(session).lastTurn?.at : Date.now()
100
+ if (at !== undefined && (!session || !this.conversationBusy(session, sessions))) this.readThrough(previous.id, at)
101
+ }
53
102
  }
54
103
  // A background browser cannot overwrite the last foreground page's state.
55
- if (this.sidebar && this.sidebar.clientId !== data.clientId && data.focused !== true && Date.now() - this.sidebar.at < 5000) return true
56
- this.sidebar = { clientId: data.clientId, at: Date.now(), rows }
104
+ if (this.sidebar && this.sidebar.clientId !== data.clientId && data.focused !== true && Date.now() - this.sidebar.at < 5000) { this.bump(); return true }
105
+ this.sidebar = { clientId: data.clientId, at: Date.now(), focused: data.focused === true, rows,
106
+ ...(data.projectionVersion === 2 || data.projectionVersion === 3 ? { projectionVersion: data.projectionVersion } : {}) }
107
+ const mirrorTrace = { clientId: data.clientId, focused: data.focused === true,
108
+ rows: rows.map(({ id, running, completed }) => ({ id, running, completed })) }
109
+ const signature = JSON.stringify(mirrorTrace)
110
+ if (signature !== this.lastMirrorTrace) { this.lastMirrorTrace = signature; this.trace('sidebar', mirrorTrace) }
57
111
  this.bump()
58
112
  return true
59
113
  }
@@ -64,8 +118,9 @@ export class Board {
64
118
 
65
119
  private readonly pending = new Map<string, Held>()
66
120
  private readonly seen = loadSeen()
67
- private readonly running = new Set<string>()
121
+ private running = new Set<string>()
68
122
  private readonly pendingUnread = new Set<string>()
123
+ private readonly browserCompletions = new Map<string, SidebarRow>()
69
124
  private readonly listeners = new Set<() => void>()
70
125
  private focus: NotchFocus | null = null
71
126
 
@@ -88,13 +143,46 @@ export class Board {
88
143
 
89
144
  snapshot(origin: string): NotchSnapshot {
90
145
  const rows: NotchRow[] = []
91
- for (const session of this.ctx.sessions.list()) {
92
- const row = this.rowFor(session)
146
+ const sessions = this.ctx.sessions.list()
147
+ const byId = new Map<string, Session>(sessions.map(session => [session.id, session]))
148
+ const groups = new Map<Session, Session[]>()
149
+ for (const session of sessions) {
150
+ const owner = conversationOwner(session, byId)
151
+ if (!owner) {
152
+ // Broken/unloaded lineage must not invent a task or lose a user question.
153
+ const ids = new Set([session.id])
154
+ const approval = this.heldApproval(ids)
155
+ const ask = this.heldAsk(ids)
156
+ if (approval || ask) rows.push({
157
+ id: session.id, title: this.titleOf(session), child: true,
158
+ busy: false, unread: false,
159
+ ...(approval ? { approval } : {}), ...(ask ? { ask } : {}),
160
+ })
161
+ continue
162
+ }
163
+ const members = groups.get(owner) ?? []
164
+ members.push(session)
165
+ groups.set(owner, members)
166
+ }
167
+ for (const [owner, members] of groups) {
168
+ const row = this.rowFor(owner, members)
93
169
  if (row) rows.push(row)
94
170
  }
95
- for (const source of this.sidebarRows() ?? []) {
96
- if (rows.some(row => row.id === source.id) || (!source.completed && !source.running)) continue
97
- if (source.completed && this.seen[source.id] !== undefined) continue
171
+ const freshSidebar = this.sidebarRows()
172
+ const coldRows = new Map(this.browserCompletions)
173
+ for (const source of freshSidebar ?? []) {
174
+ if (source.running) coldRows.set(source.id, source)
175
+ }
176
+ for (const source of coldRows.values()) {
177
+ // The Host owns loaded session classification. A stale mirror must not
178
+ // resurrect a filtered child as child:false or override a settled root.
179
+ if (byId.has(source.id) || (!source.completed && !source.running)) continue
180
+ // Heartbeat freshness is a liveness lease, not an unread-state change.
181
+ // Retain completed cold rows until acknowledged; expire running-only
182
+ // claims when their source stops reporting.
183
+ if (source.running && !freshSidebar) continue
184
+ const seen = this.seen[source.id]
185
+ if (source.completed && seen !== undefined && (source.updatedAt === undefined || seen >= source.updatedAt)) continue
98
186
  rows.push({ id: source.id, title: source.title, child: false, busy: source.running, unread: source.completed })
99
187
  }
100
188
  rows.sort((a, b) => Number(Boolean(b.approval || b.ask)) - Number(Boolean(a.approval || a.ask))
@@ -102,13 +190,24 @@ export class Board {
102
190
  || Number(b.unread) - Number(a.unread)
103
191
  || (b.lastTurn?.at ?? 0) - (a.lastTurn?.at ?? 0))
104
192
  this.running = new Set(
105
- this.ctx.sessions.list().filter((session) => this.isBusy(session)).map((session) => session.id),
193
+ sessions.filter((session) => this.isBusy(session)).map((session) => session.id),
106
194
  )
107
- return { ok: true, generatedAt: Date.now(), origin, rows, sidebarSyncedAt: this.sidebarRows() ? this.sidebar?.at : undefined }
195
+ const stateRows = rows.map(row => ({ id: row.id, busy: row.busy, unread: row.unread,
196
+ turnAt: row.lastTurn?.at, action: Boolean(row.ask || row.approval) }))
197
+ const rowSignature = JSON.stringify(stateRows)
198
+ if (rowSignature !== this.lastRowTrace) {
199
+ this.lastRowTrace = rowSignature
200
+ this.trace('snapshot', { rows: stateRows, clientId: this.sidebar?.clientId,
201
+ mirrorAt: this.sidebar?.at, running: [...this.running], pendingUnread: [...this.pendingUnread] })
202
+ }
203
+ return { ok: true, stateRevision: 4, generatedAt: Date.now(), origin, rows,
204
+ sidebarSyncedAt: freshSidebar ? this.sidebar?.at : undefined,
205
+ sidebarProjectionVersion: freshSidebar ? this.sidebar?.projectionVersion : undefined }
108
206
  }
109
207
 
110
208
  markSeen(sessionId: string): void {
111
209
  this.pendingUnread.delete(sessionId)
210
+ this.browserCompletions.delete(sessionId)
112
211
  this.seen[sessionId] = Date.now()
113
212
  saveSeen(this.seen)
114
213
  this.bump()
@@ -117,6 +216,8 @@ export class Board {
117
216
  markAllSeen(): void {
118
217
  this.pendingUnread.clear()
119
218
  const now = Date.now()
219
+ for (const id of this.browserCompletions.keys()) this.seen[id] = now
220
+ this.browserCompletions.clear()
120
221
  for (const session of this.ctx.sessions.list()) {
121
222
  this.seen[session.id] = now
122
223
  }
@@ -124,6 +225,29 @@ export class Board {
124
225
  this.bump()
125
226
  }
126
227
 
228
+ private readThrough(sessionId: string, at: number): void {
229
+ const seen = this.seen[sessionId]
230
+ if ((seen ?? -Infinity) >= at) return
231
+ const session = this.ctx.sessions.list().find(item => item.id === sessionId)
232
+ const turn = session && foldSession(session).lastTurn
233
+ const completedAt = turn?.at ?? this.browserCompletions.get(sessionId)?.updatedAt
234
+ // An idle focused page reports every 400 ms. Persist once per result,
235
+ // not on every heartbeat for as long as that conversation stays open.
236
+ if (seen !== undefined && (completedAt === undefined || seen >= completedAt)) return
237
+ this.seen[sessionId] = at
238
+ if (completedAt === undefined || completedAt <= at) {
239
+ this.pendingUnread.delete(sessionId)
240
+ this.browserCompletions.delete(sessionId)
241
+ }
242
+ saveSeen(this.seen)
243
+ this.trace('read', { sessionId, through: at })
244
+ }
245
+
246
+ noteTurnEnd(session: Session, at = foldSession(session).lastTurn?.at): void {
247
+ if (isChildSession(session)) return
248
+ if (at !== undefined && (this.seen[session.id] ?? -Infinity) < at) this.pendingUnread.add(session.id)
249
+ }
250
+
127
251
  /**
128
252
  * Record a "show this session in a DSH UI" wish from the native helper.
129
253
  * Returns false for unknown sessions. The wish expires after 60s; every
@@ -131,7 +255,8 @@ export class Board {
131
255
  */
132
256
  requestFocus(sessionId: string): boolean {
133
257
  const known = this.ctx.sessions.list().some((session) => session.id === sessionId)
134
- if (!known && !this.sidebarRows()?.some(row => row.id === sessionId)) return false
258
+ const mirrored = this.browserCompletions.has(sessionId) || this.sidebarRows()?.some(row => row.id === sessionId)
259
+ if (!known && !mirrored) return false
135
260
  this.focus = { sessionId, at: Date.now() }
136
261
  this.bump()
137
262
  return true
@@ -232,28 +357,46 @@ export class Board {
232
357
  return this.ctx.agents.get(session.id)?.status === 'running'
233
358
  }
234
359
 
235
- private rowFor(session: Session): NotchRow | undefined {
360
+ private conversationBusy(owner: Session, sessions: ReadonlyMap<string, Session>): boolean {
361
+ for (const member of sessions.values()) {
362
+ if (conversationOwner(member, sessions)?.id === owner.id
363
+ && (this.isBusy(member) || this.hasSubagentJob(member))) return true
364
+ }
365
+ return false
366
+ }
367
+
368
+ private hasSubagentJob(session: Session): boolean {
369
+ const agent = this.ctx.agents.get(session.id)
370
+ if (!agent) return false
371
+ // Remote backends have no local Agent/Session. The official background
372
+ // job registry retains their owner through running and stopping, including
373
+ // jobs that were already active when Notch was hot-loaded.
374
+ const jobs = this.ctx.get('jobs') as Pick<JobRegistry, 'list'> | undefined
375
+ return jobs?.list(agent.id).some(job => job.owner === session.id
376
+ && job.kind === 'subagent'
377
+ && (job.status === 'running' || job.status === 'stopping')) ?? false
378
+ }
379
+
380
+ private rowFor(session: Session, members: Session[]): NotchRow | undefined {
236
381
  const folded = foldSession(session)
237
382
  const child = isChildSession(session)
238
383
  const lastSeen = this.seen[session.id]
239
- const busy = this.isBusy(session)
240
- if (busy) {
384
+ const busy = members.some(member => this.isBusy(member) || this.hasSubagentJob(member))
385
+ // Completion belongs to the owner's turn, never an individual worker.
386
+ if (folded.busy) {
241
387
  this.pendingUnread.delete(session.id)
242
- } else if (this.running.has(session.id) && folded.lastTurn && !folded.lastTurn.failed) {
388
+ } else if (this.running.has(session.id) && folded.lastTurn) {
243
389
  this.pendingUnread.add(session.id)
244
390
  }
245
391
  const dismissed = lastSeen !== undefined && (folded.lastTurn === undefined || lastSeen >= folded.lastTurn.at)
246
- const mirror = this.sidebarRows()
247
- const unread = dismissed
248
- ? false
249
- : mirror !== undefined
250
- ? mirror.some(row => row.id === session.id && row.completed)
251
- : this.pendingUnread.has(session.id)
252
- || (folded.lastTurn !== undefined && lastSeen !== undefined && folded.lastTurn.at > lastSeen)
253
- const approval = this.heldApproval(session.id)
254
- const ask = this.heldAsk(session.id)
392
+ const unread = !dismissed && !busy
393
+ && (this.pendingUnread.has(session.id) || this.browserCompletions.has(session.id))
394
+ const memberIds = new Set(members.map(member => member.id))
395
+ // Keep the original request id/resolver so answering the root's yellow
396
+ // lamp settles the correct child. Further child questions queue here.
397
+ const approval = this.heldApproval(memberIds)
398
+ const ask = this.heldAsk(memberIds)
255
399
  if (!busy && !unread && !approval && !ask) return undefined
256
- if (child && !approval && !ask && !busy) return undefined
257
400
  const title = this.titleOf(session)
258
401
  const row: NotchRow = {
259
402
  id: session.id,
@@ -268,9 +411,9 @@ export class Board {
268
411
  return row
269
412
  }
270
413
 
271
- private heldApproval(sessionId: string): NotchApproval | undefined {
414
+ private heldApproval(sessionIds: ReadonlySet<string>): NotchApproval | undefined {
272
415
  for (const held of this.pending.values()) {
273
- if (held.kind === 'approval' && held.sessionId === sessionId) {
416
+ if (held.kind === 'approval' && sessionIds.has(held.sessionId)) {
274
417
  return {
275
418
  id: held.id,
276
419
  toolName: held.toolName,
@@ -281,9 +424,9 @@ export class Board {
281
424
  return undefined
282
425
  }
283
426
 
284
- private heldAsk(sessionId: string): NotchAsk | undefined {
427
+ private heldAsk(sessionIds: ReadonlySet<string>): NotchAsk | undefined {
285
428
  for (const held of this.pending.values()) {
286
- if (held.kind === 'ask' && held.sessionId === sessionId) {
429
+ if (held.kind === 'ask' && sessionIds.has(held.sessionId)) {
287
430
  return { id: held.id, questions: held.questions }
288
431
  }
289
432
  }