Files
dsh-context/src/client/index.ts
T
bowenliang123 021db5f2a2 build: migrate tsup to tsdown, mirroring the official client-bundle preset
- banner/intro/footer three-part closure-factory wrap (verbatim from
  packages/client/tsdown.client.ts), intro now owns var module/exports
- deps.neverBundle/alwaysBundle guards: host keeps production deps
  external, client keeps only module-table specifiers external
- bundle purity gate: any non-requested @deepseek-ai/* value import is
  a build error (INLINE_SAFE / vendored-library / generated-remote
  allowances mirrored)
- NODE_ENV + import.meta.env defines for inlined node-idiom deps
- client sourcemap on, matching the official artifact contract
- client smoke test now runs the bundle verbatim through the
  __ModuleLoader__ handoff instead of slicing the factory body by
  exact output strings (decoupled from bundler layout)
2026-08-22 02:14:44 +08:00

109 lines
4.4 KiB
TypeScript

/**
* dsh-context — Client half (installed package bundle entry).
*
* Registers a "上下文/Context" tab in the conversation view ring
* (`conversation.view` slot, beside Chat/Trajectory) and renders the
* context-composition timeline: current makeup, per-request stacked-bar
* history, context events, and the live message list.
*
* Since v0.9 the tab needs no custom data plane: the Host half pushes its
* fold through the harness's session-projection pipeline
* (`contextTimeline` projection key), and this half reads the finished value
* from the framework standard kit (`useProjection('contextTimeline')`, a
* standard prop on every session-scope slot component). No polling, no RPC,
* no client-side cache.
*
* This module is the body of the package's `./client` bundle: tsdown
* (tsdown.config.ts) bundles it (external `react` — the browser module table
* supplies it via the injected `require`) into the web boot handoff
* (`window.__ModuleLoader__.load({id, factory})`). All imports from other
* client modules are inlined by the bundler; everything here is zero-runtime
* beyond the bundled source.
*/
import { DICT_EN, DICT_ZH } from './i18n'
import { registerContextCommand } from './command'
import { makeContextModal } from './components/contextModal'
import { modalStoreOf } from './modalStore'
import type { ClientCtx, SessionsFace } from './services'
import { STYLES } from './styles'
import { makeContextView } from './components/contextView'
import { makeViewKit } from './viewkit'
import { React, h } from './react'
const NS = 'dsh-context'
function apply(ctx: ClientCtx): void {
// Bilingual dictionaries; the tab label thunk and all UI text follow the
// active locale through the bound translate (missing keys fall back to
// zh, then the key itself). The registration rides ctx.effect, so a stop
// or HMR reload disposes it.
ctx.effect(() => {
return ctx.locale.register(NS, { zh: DICT_ZH, en: DICT_EN })
}, 'dsh-context: dictionaries')
const t = ctx.locale.bind(NS)
// Theme-native styles, injected as a plugin-owned <style> tag (the web
// boot loader claims and removes tags carrying data-plugin on unload).
ctx.effect(() => {
const tag = document.createElement('style')
tag.setAttribute('data-plugin', 'dsh-context')
tag.textContent = STYLES
document.head.appendChild(tag)
return () => {
if (tag.parentNode !== null) tag.parentNode.removeChild(tag)
}
}, 'dsh-context: styles')
const kit = makeViewKit(t)
const ContextView = makeContextView(ctx, kit)
// Contribute the history-pagination verb as a standard prop, so the
// Context browser can pull older conversation pages on demand when the
// reader expands an element outside the loaded window. Fail-soft: hosts
// without the provide channel (or a duplicate-name refusal) keep the
// preview-plus-hint degradation.
ctx.effect(() => {
const noop = (): void => {}
const sessions = ctx.get('sessions') as SessionsFace | undefined
if (sessions === undefined || typeof sessions.provide !== 'function') return noop
try {
return sessions.provide({
props: ['loadOlderHistory'],
resolve: binding => ({ props: { loadOlderHistory: () => binding.session.loadOlder() } }),
})
} catch {
return noop
}
}, 'dsh-context: loadOlderHistory prop')
ctx.slots.inject('conversation.view', () => {
return ctx.slots.register(
// order 20 renders right of Chat (0) and Trajectory (10); the locale
// namespace put the framework `t` seat on the component's props too.
{ name: 'conversation.view', id: 'context', order: 20, locale: NS, label: () => t('tab') },
props => h(ContextView, props),
)
})
// `/context` slash command: opens the context modal (see command.ts for
// the trigger source). The modal itself renders from the input overlay
// slot, opened per session through the hooks-compartment store.
registerContextCommand(ctx, kit)
const ContextModal = makeContextModal(ctx, kit)
ctx.slots.inject('conversation.input.overlay', () => {
return ctx.slots.register(
{ name: 'conversation.input.overlay', id: 'context-modal', order: 10, locale: NS,
inject: (sessionId: string) => ({ hooks: { contextModal: modalStoreOf(sessionId) } }) },
props => h(ContextModal, props),
)
})
}
module.exports = {
name: 'dsh-context',
inject: ['slots', 'locale'],
apply,
}