/** * Auto-continue plugin, browser half (thin shell). * * Since 0.8.0 the auto-continue ENGINE runs inside the host process (single * instance — see src/host/engine.ts), so this half only: * - registers the `auto-continue` settings card (`settings.plugin.item`), * - subscribes to the host status bridge (SSE) and shows browser * notifications with action buttons (Resume now / Pause 1h) via the bridge * action endpoint, * - feeds the card's stats / paused-sessions panels from the bridge state. */ import type { Context as ClientContext } from '@deepseek-ai/cordis'; // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client'; // Type-only: pulls the settings-surface SlotMap merge and ctx.settingsScope. import type {} from '@deepseek-ai/dsh-client-ui-settings/client'; // Type-only: pulls the `settings.plugin.item` SlotMap merge. import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'; import { type AutoContinueSettings } from './engine.ts'; import { en, zh, type SettingsCardKey } from './locales.ts'; import { AutoContinueSettingsCard, AutoContinueSettingsCardController, } from './settings-card.tsx'; import { startBridge } from './bridge.ts'; /** Dictionary namespace owned by this plugin. */ const NS = 'auto-continue'; /** Settings namespace the settings card edits (the host engine reads it). */ const SETTINGS_NS = 'auto-continue'; declare module '@deepseek-ai/dsh-client-ui-slots' { interface LocaleNamespaceMap { /** auto-continue settings-card copy. */ 'auto-continue': SettingsCardKey; } } /** Services required by this plugin. */ export const inject = ['slots', 'locale', 'settingsScope']; // 浏览器侧辅助(设置卡片用): 桥状态读取与暂停解除。 export { pausedSessions, readTodayStats, resetTodayStats, unpauseSession, } from './bridge.ts'; /** * Plugin body: settings card + host status bridge (notifications, stats, * paused sessions). * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'auto-continue: dictionaries'); const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS }); /** * Mirror the browser locale into the host config, with a hard per-page budget. * * The write is only needed while the stored value disagrees with the page, so * every notification re-checks the snapshot — but two unbounded writers hid in * that check, and both of them hammer `settings.yaml` until the host settings * write queue backs up (observed: ~1 write / 400 ms when a completed write does * not fold back before the next document tick, and a two-page ping-pong when * two pages with different UI languages keep overwriting each other). * * So: keep the subscription and the `locale/change` hook (a scope that is not * `ready` yet still mirrors once it is, and a real language switch still wins), * but cap this page at {@link MAX_MIRROR_ATTEMPTS} writes per active locale. * * The budget must NOT be reset by a matching snapshot. In the real * `SettingsScopeController`, a successful `set()` folds the mutation response * into the WRITING page's own mirror (`mirror.acceptView` -> scope `derive()` -> * subscriber), so under the two-page ping-pong each page first sees its own * accepted value — matching its active locale — and would hand itself a fresh * budget before the other page writes back. That is an unbounded loop again * (probed: 400+ host writes). Reset only when the browser's active locale * genuinely changes; a matching snapshot then simply needs no write. */ const MAX_MIRROR_ATTEMPTS = 3; let mirroredLocale: string | undefined; let mirrorAttempts = 0; const syncLocale = (): void => { const active = ctx.locale.getLocale().active; const snapshot = scope.getSnapshot(); if (snapshot.status !== 'ready' || !snapshot.writable || snapshot.mode !== 'host') return; // A genuine language switch is the only thing that earns a fresh budget: the // attempt count must survive matching snapshots, because the writing page's // own acknowledgement arrives as a match (see above). Tracking `mirroredLocale` // on every pass is what lets a later real switch be recognised. if (mirroredLocale !== active) { mirroredLocale = active; mirrorAttempts = 0; } // Converged: nothing to write. if (snapshot.value?.locale === active) return; if (mirrorAttempts >= MAX_MIRROR_ATTEMPTS) return; mirrorAttempts += 1; void scope.set('locale', active); }; ctx.effect(() => scope.subscribe(syncLocale), 'auto-continue: locale settings sync'); ctx.on('locale/change', syncLocale); syncLocale(); // 状态桥: 订阅 host 的通知与运行时状态, 弹浏览器通知并驱动卡片面板。 ctx.effect(() => startBridge(), 'auto-continue: host bridge'); // Plugin configuration card: one staged form over the `auto-continue` // settings namespace, contributed to the plugin-configuration section // (Settings → Plugins). Since DSH 0.1.0-rc.7 `settings.plugin.item` is a // keyed slot dispatched by the settings namespace it edits, so the entry // registers with `key` (the namespace), like the official cards. const controller = new AutoContinueSettingsCardController(scope); ctx.slots.inject('settings.plugin.item', () => ctx.slots.register( { name: 'settings.plugin.item', key: SETTINGS_NS, locale: NS, inject: () => controller.inject(), }, AutoContinueSettingsCard, ), ); }