The macOS window's draggable area was a hand-written band: a 52px strip, deepened to 76px by a marker the Conversation header had to keep in step with its own height. It drifted from the layout — blank runs inside a chrome row did not drag, and a control a few pixels away dragged the window instead. Chrome rows now mark their own element (`data-window-drag`) and the shell declares the surface once (`html[data-platform='darwin'] [data-window-drag]`), so a row's own box is its draggable geometry and no fixed band has to match any row's height. The frame band, its 52/76 figures, the marker, and the per-sheet drag rules are gone; interactive controls and overlays portal beside `#root` where one rule subtracts them. Recollection becomes one shell watcher (`window-drag/recall.ts`): Electron rebuilds the window's drag rects only when a computed app-region value changes (electron#32341), so the watcher measures every marked row each frame while the surface moves, sets `data-window-drag-recall` for the frames whose geometry changed, and clears it on the frame that finds the geometry settled. The right panel's hand-written pulse is deleted with it. Coverage is a contract with executable parts. `window-drag/regions.ts` publishes the composition rule and the interactive selector that both the sheet and its spec compare against; the ui-theme gate asserts the package tree declares drag in exactly two places and pairs every row's mark with its sheet, selector, and authored height; and the browser lane boots the shipped composition in Chromium with the darwin marker to assert per-row geometry, a content body under a marked head row, a covering overlay, and the watcher's pulse. The plugin manager's detail views mark the head row they share rather than the pane they render in, so their text and form labels stay content. Both real machines agree: the macOS matrix ran on a real `hiddenInset` window with HID-level pointer input and window-frame readback, and the Windows matrix on a Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not, and the controls keep their clicks. Refs #4619
124 lines
5.3 KiB
TypeScript
124 lines
5.3 KiB
TypeScript
/**
|
|
* Web boot kernel. It owns only the module system, Cordis loader, and a
|
|
* framework-free boot page; plugin composition and the renderer handoff are
|
|
* `bootClient` and `mountClient`. The dynamic UI renderer receives the mount
|
|
* point after every client entry activates.
|
|
* @module @deepseek-ai/dsh-client-web/src/boot
|
|
*/
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import type {
|
|
BootManifest, ClientModuleCreateOptions, ClientModuleSystem, DshWindow,
|
|
} from '@deepseek-ai/dsh-client-modules/client'
|
|
import { bootClient } from './boot-client.ts'
|
|
import { BootPage } from './boot-page.ts'
|
|
import { mountClient } from './mount.ts'
|
|
import { getStaticModules } from './seed.ts'
|
|
import { installWindowDragRecall } from './window-drag/recall.ts'
|
|
import './base.css'
|
|
|
|
/** Module transport hook replaced by jsdom tests. */
|
|
export type BootSeams = Pick<ClientModuleCreateOptions, 'loadBundle'>
|
|
|
|
/** Browser boot entry consumed by `apps/web`. */
|
|
export class AppWebEntry {
|
|
private readonly container: HTMLElement
|
|
private readonly seams: BootSeams | undefined
|
|
private readonly page: BootPage
|
|
private ctx: Context | undefined
|
|
private stopDragRecall: (() => void) | undefined
|
|
private modules!: ClientModuleSystem
|
|
private manifest!: BootManifest
|
|
|
|
/**
|
|
* Draw the boot page; {@link run} starts the loader.
|
|
* @param container - Application mount point.
|
|
* @param seams - Optional module transport replacement.
|
|
*/
|
|
constructor(container: HTMLElement, seams?: BootSeams) {
|
|
this.container = container
|
|
this.seams = seams
|
|
this.page = new BootPage(container)
|
|
}
|
|
|
|
/**
|
|
* Load and activate every client entry, then hand the mount point to the
|
|
* UI renderer. Plugin failures remain visible on the boot page.
|
|
* @param onFailure - Optional carrier-owned fatal presentation; keeps the boot page visible.
|
|
* @returns Resolves after application mount or failure reporting.
|
|
*/
|
|
async run(onFailure?: (reason: unknown) => void): Promise<void> {
|
|
try {
|
|
// Boot-readiness gate: whichever bootstrap applies the injection table
|
|
// settles this deferred once every row has taken effect — the served
|
|
// index resolves it in the rendered tail, so the await returns on the
|
|
// next microtask; an asynchronous bootstrap resolves it after its last
|
|
// row, or rejects it into the failure rendering below. An absent global
|
|
// means no bootstrap owns the document and there is nothing to wait for.
|
|
await (globalThis as { __DSH_BOOT_READY__?: { promise: Promise<void> } }).__DSH_BOOT_READY__?.promise
|
|
const win = globalThis as DshWindow
|
|
const moduleLoader = win.__ModuleLoader__
|
|
if (moduleLoader === undefined) {
|
|
throw new Error('web boot: window.__ModuleLoader__ bootstrap facade is missing')
|
|
}
|
|
// A pre-injected transport (the worker preview page) owns bundle bytes;
|
|
// its loadBundle is the default and explicit seams still win. The global
|
|
// is `ClientTransportHooks`, owned by @deepseek-ai/dsh-client-connection;
|
|
// this structural slice reads one optional member without adding a
|
|
// package edge.
|
|
const transport = (globalThis as {
|
|
__DSH_TRANSPORT__?: { loadBundle?: ClientModuleCreateOptions['loadBundle'] }
|
|
}).__DSH_TRANSPORT__
|
|
this.modules = moduleLoader.create({
|
|
boot: win.__DSH_BOOT__,
|
|
staticModules: getStaticModules(),
|
|
...transport?.loadBundle === undefined ? {} : { loadBundle: transport.loadBundle },
|
|
...this.seams,
|
|
})
|
|
this.manifest = this.modules.manifest
|
|
|
|
const prefetching = this.prefetchImmediateTier()
|
|
const ctx = new Context()
|
|
this.ctx = ctx
|
|
this.page.setTotal(this.manifest.plugins.length)
|
|
await prefetching
|
|
await bootClient({
|
|
ctx,
|
|
modules: this.modules,
|
|
manifest: this.manifest,
|
|
onEntryState: (name, state) => {
|
|
if (onFailure === undefined || state !== 'failed') this.page.setState(name, state)
|
|
},
|
|
})
|
|
// The shell owns the one watcher that keeps Electron's window drag rects in
|
|
// step with the rows that own them (electron#32341), so no chrome row has to
|
|
// know that trap. It installs before the first mount, so the surface the
|
|
// renderer draws is the one the first frame measures.
|
|
this.stopDragRecall = installWindowDragRecall({ document: this.container.ownerDocument })
|
|
await mountClient(ctx, this.container)
|
|
} catch (reason) {
|
|
console.error(reason)
|
|
if (onFailure !== undefined) onFailure(reason)
|
|
else this.page.fail(reason instanceof Error ? reason.message : String(reason))
|
|
}
|
|
}
|
|
|
|
/** Dispose the client plugin tree and whichever page owns the mount point. */
|
|
async dispose(): Promise<void> {
|
|
this.stopDragRecall?.()
|
|
this.stopDragRecall = undefined
|
|
const ctx = this.ctx
|
|
this.ctx = undefined
|
|
if (ctx !== undefined) await ctx.fiber.dispose()
|
|
this.page.dispose()
|
|
}
|
|
|
|
/** Prefetch stage-one bundles and their dynamic requests before concurrent plugin imports. */
|
|
private async prefetchImmediateTier(): Promise<void> {
|
|
await Promise.all(this.manifest.plugins
|
|
.filter(row => row.immediately)
|
|
.map(row => this.modules.prefetch(row.id).catch((_prefetchError: unknown) => {
|
|
// Prefetch only starts transport early; the Loader import retries and reports this bundle failure.
|
|
})))
|
|
}
|
|
}
|