Files
deepseek-harness/packages/client/web/src/boot.ts
T
mektpoy 31a83ebc4b refactor(client): let chrome rows own the macOS window drag
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
2026-09-20 19:01:15 +08:00

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.
})))
}
}