dsh-plugin-shop 0.8.7 → 0.9.0-beta.1

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The shop's two buttons on dsh's Plugins page (design
3
+ * 2026-10-16-shop-sidebar-page §7), in the head of a detail page, before the
4
+ * page's own switch and uninstall (`plugins.detail.actions`, dsh 0.1.7-rc.2
5
+ * and later): "Open Plugin shop" on the shop's own bundle page, and "View in
6
+ * Plugin shop" on the page of any bundle the shelf lists. Any other subject
7
+ * renders nothing, as the slot's contract asks of an entry.
8
+ *
9
+ * It renders inside dsh's page, so it carries its own error boundary: a
10
+ * throw here costs the reader a button, never dsh's page.
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import type { InjectFace, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
14
+ import type { ShopCatalogResult } from '../host/index.ts';
15
+ import type { ShopPageStore } from './page-store.ts';
16
+ /** One bundle as a detail contribution sees it: the field this reads of the
17
+ * Plugins page's `PluginPackageRef` (`dsh-client-ui-plugin-manager`'s slot
18
+ * contract, identical in 0.1.7-rc.2 and 0.2.0-rc.2). */
19
+ export interface PluginPackageRef {
20
+ readonly name: string;
21
+ }
22
+ /** What a detail page is about, as the Plugins page hands it over. */
23
+ export type PluginsSubject = {
24
+ readonly kind: 'bundle';
25
+ readonly pkg: PluginPackageRef;
26
+ } | {
27
+ readonly kind: 'row';
28
+ readonly pkg: PluginPackageRef;
29
+ } | {
30
+ readonly kind: 'item';
31
+ readonly id: string;
32
+ };
33
+ /** The face index.ts hands the buttons. */
34
+ export interface ShopDetailActionInjected {
35
+ /** The catalog a listing question is asked of (index.ts `listing`). */
36
+ listing: () => Promise<ShopCatalogResult>;
37
+ /** Whether the page can be opened at all: false without a layout service
38
+ * that can select a panel. */
39
+ canOpen: () => boolean;
40
+ openShop: () => void;
41
+ page: ShopPageStore;
42
+ }
43
+ export type ShopDetailActionProps = {
44
+ subject: PluginsSubject;
45
+ } & PropsLocale<'settings.shop'> & InjectFace<ShopDetailActionInjected>;
46
+ export declare function ShopDetailAction(props: ShopDetailActionProps): ReactNode;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The shop's glyph in dsh's sidebar (design 2026-10-16-shop-sidebar-page
3
+ * §4.3): a bag with a whale tail, drawn the way dsh draws its own panel
4
+ * glyphs — a 16×16 view box, one-pixel `currentColor` strokes, no fill — so
5
+ * the row's colour and its selection treatment reach it unchanged. `active`
6
+ * is ignored, as dsh's own glyphs ignore it: the row draws the selection.
7
+ */
8
+ import type { ReactNode } from 'react';
9
+ export declare function ShopGlyph({ size }: {
10
+ size: number;
11
+ active: boolean;
12
+ }): ReactNode;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The shop's own page in dsh's main column (design
3
+ * 2026-10-16-shop-sidebar-page §4.2), framed as dsh frames its Plugins page:
4
+ * the page is its own scroll container, its head carries the title and the
5
+ * one intro line and clears the desktop title bar, and the body below is
6
+ * `ShopTab`, unchanged.
7
+ *
8
+ * It keeps the reader's scroll position in the page store (design
9
+ * 2026-10-16-shop-sidebar-page §5.7). The position is written on every
10
+ * scroll event — a browser fires at most one a frame, and the store takes it
11
+ * without telling anyone — with the card at the page's top edge. The page
12
+ * goes back there once, when the shelf is on screen again and the installed
13
+ * list has answered, unless the reader moved first: the same card at the
14
+ * same height, held there for a moment while the cards around it render, or
15
+ * the position when that card is no longer on the shelf or the shelf was
16
+ * refiltered since.
17
+ */
18
+ import { type ReactNode } from 'react';
19
+ import { type ShopTabProps } from './ShopTab.tsx';
20
+ /** How long the page holds the reader's card at its height once it is put
21
+ * back, in ms. The cards near it render a frame or two later and take their
22
+ * real heights, each correction brings a few more above it into range, and
23
+ * the shelf's entry animation moves it for 220ms (ShopTab.module.css).
24
+ * Measured on dsh 0.2.0-rc.2 with the live catalog, the card stood still
25
+ * within 200ms of being put back. */
26
+ export declare const HOLD_MS = 500;
27
+ export declare function ShopPage(props: ShopTabProps): ReactNode;
@@ -6,6 +6,7 @@
6
6
  import { type ReactNode } from 'react';
7
7
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
8
8
  import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult, ShopUpdateResult, ShopVersionResult } from '../host/index.ts';
9
+ import { type ShopPageStore } from './page-store.ts';
9
10
  /** The tab's Remote face: the Host result types, already unwrapped from the
10
11
  * wire envelope by `index.ts`; `catalog` throws on a wire error so the tab's
11
12
  * error state renders. */
@@ -50,8 +51,11 @@ export interface ShopTabInjected {
50
51
  /** Be told when the version answers start coming from another dsh process
51
52
  * — dsh restarted under the page, which nothing reloads — with the first
52
53
  * answer from it; returns the unsubscribe. The receipts a restart settles
53
- * are the tab's to clear (design §8, 2026-10-06 amendment). Optional: a
54
- * stub may not offer it. */
54
+ * are the page store's to clear (`noteRestarted`), which index.ts calls on
55
+ * every restart under the page, tab or no tab; the tab clears them itself
56
+ * only for the store it builds when none is handed over (design §8,
57
+ * 2026-10-06 amendment, and design 2026-10-16-shop-sidebar-page §5.5).
58
+ * Optional: a stub may not offer it. */
55
59
  onRestarted?: (listener: (fresh: ShopVersionResult) => void) => () => void;
56
60
  updateStart: (args: {
57
61
  version: string;
@@ -60,13 +64,26 @@ export interface ShopTabInjected {
60
64
  * reload. Injected so a test can observe it without touching jsdom's
61
65
  * non-writable `location.reload`. */
62
66
  reload?: () => void;
67
+ /** The page's store (page-store.ts): the reader's place on the shelf and
68
+ * every flow, kept across leaving and returning (design
69
+ * 2026-10-16-shop-sidebar-page §5). Optional: without it the tab builds a
70
+ * store of its own, which lives and dies with the tab — how the tab
71
+ * behaved before the store existed, and what a stub gets. */
72
+ page?: ShopPageStore;
63
73
  }
64
- /** Full component props assembled by the Settings slot renderer. */
65
- /** The shop body's props: since 2026-10-15 the shop is its own Settings
66
- * section (design §5.1), whose one owner prop, `close`, the body ignores. */
67
- export type ShopTabProps = PropsRuntime<'settings.section'> & PropsLocale<'settings.shop'> & InjectFace<ShopTabInjected>;
68
- /** The shop body inside its own error boundary, as `ShopSection` renders it
69
- * below the page's title and intro (the settings slot registers `ShopSection`,
70
- * `index.ts`). Exported under the name the body used to carry, so nothing can
71
- * render the body without the boundary. */
74
+ /** What only the page frame hands the body (ShopPage.tsx). */
75
+ export interface ShopTabOwnProps {
76
+ /** Called once per mount, on the first commit with the shelf on screen
77
+ * and an answer from the installed list, rows or none or a failure: the
78
+ * frame restores its scroll position then (design
79
+ * 2026-10-16-shop-sidebar-page §5.7). */
80
+ onShelfReady?: () => void;
81
+ }
82
+ /** The page's props as the main-column renderer assembles them (no owner
83
+ * props), plus what only the page frame hands the body. */
84
+ export type ShopTabProps = PropsRuntime<'main'> & PropsLocale<'settings.shop'> & InjectFace<ShopTabInjected> & ShopTabOwnProps;
85
+ /** The shop body inside its own error boundary, as `ShopPage` renders it
86
+ * below the page's head (index.ts registers `ShopPage` in the main column).
87
+ * Exported under the name the body used to carry, so nothing can render the
88
+ * body without the boundary. */
72
89
  export declare function ShopTab(props: ShopTabProps): ReactNode;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * One keyed registry of install-shaped flows: start, poll to terminal, reset.
3
+ *
4
+ * Framework-free, so a flow can belong to the page rather than to a component
5
+ * (design 2026-10-16-shop-sidebar-page §5). dsh mounts the shop only while it
6
+ * is on screen, and a flow held in a component's state — the poll loop an
7
+ * effect of that component — stopped the moment the reader looked elsewhere:
8
+ * the install ran on in the host, and nothing was left to fold its status or
9
+ * write its receipt. The page store (page-store.ts) owns three that outlive
10
+ * every component showing them, and `useRegistryFlows` (useFlows.ts) renders
11
+ * one for React.
12
+ *
13
+ * Install and uninstall are the same machine, and so is the shop's own
14
+ * update: they differ only in what the STARTING RPC's answer means, which is
15
+ * the three `begin*` mappings at the foot of this file.
16
+ */
17
+ import type { InstallArgs, ShopInstallResult, ShopInstallStatusResult, ShopUninstallResult, ShopUpdateResult } from '../host/index.ts';
18
+ import { type InstallView } from './present.ts';
19
+ /** The one idle view, so a key the registry holds no flow for and a key it
20
+ * reset to idle hand a memoized consumer the same object. */
21
+ export declare const IDLE_VIEW: InstallView;
22
+ /** `map` without every entry `drop` selects, or null when it selects none.
23
+ * The copy is made at the first match, so a prune that matches nothing
24
+ * copies nothing, and its caller, handed null, publishes nothing. The
25
+ * registry's `resetWhere` and the page store's owed notices share this
26
+ * contract, so a fourth prune cannot return an empty map instead. */
27
+ export declare function pruned<K, V>(map: ReadonlyMap<K, V>, drop: (value: V, key: K) => boolean): Map<K, V> | null;
28
+ /** What a registry holds at one moment. Replaced whole on every change, so a
29
+ * consumer can tell "changed" from "same" by identity. */
30
+ export interface FlowSnapshot {
31
+ readonly views: ReadonlyMap<string, InstallView>;
32
+ /** Identities whose start is in flight and unanswered. */
33
+ readonly starting: ReadonlySet<string>;
34
+ }
35
+ export interface FlowRegistryDeps<TArgs> {
36
+ /** The starting RPC with its answer mapped to the first view. Never
37
+ * rejects: the mapping folds a transport throw into a view. */
38
+ begin: (args: TArgs) => Promise<InstallView>;
39
+ installStatus: (args: {
40
+ installId: string;
41
+ }) => Promise<ShopInstallStatusResult>;
42
+ /** Once per identity that reaches a terminal state: which one, and the
43
+ * arguments its flow was started with. The outcome is load-bearing: a
44
+ * failed flow changed nothing on the server. */
45
+ onSettled?: (key: string, outcome: 'done' | 'failed', args: TArgs) => void;
46
+ }
47
+ export interface FlowRegistryOptions {
48
+ /** Keep polling while nothing subscribes. The page store's registries do,
49
+ * so an install the reader left finishes its receipt with no page showing
50
+ * it; a component's own registry does not, so its polling stops when the
51
+ * component unmounts and resumes if it mounts again. */
52
+ pollUnwatched: boolean;
53
+ }
54
+ export interface FlowRegistry<TArgs> {
55
+ getSnapshot: () => FlowSnapshot;
56
+ subscribe: (listener: () => void) => () => void;
57
+ /** Start `key`'s flow. A second start while the first is unanswered is
58
+ * ignored: the host does not dedupe, so a second RPC would download and
59
+ * install the same thing twice. */
60
+ start: (key: string, args: TArgs) => Promise<void>;
61
+ reset: (key: string) => void;
62
+ /** Reset every identity whose view `predicate` selects. It is handed the
63
+ * identity too, so a caller can select one identity's view by its kind. */
64
+ resetWhere: (predicate: (view: InstallView, key: string) => boolean) => void;
65
+ /** Stop for good: no more polling, starts, changes or listeners, a start
66
+ * still unanswered included. The snapshot stays readable. */
67
+ dispose: () => void;
68
+ }
69
+ /**
70
+ * @param deps read on every call, so an owner can hand over its latest
71
+ * functions without rebuilding the registry.
72
+ */
73
+ export declare function createFlowRegistry<TArgs>(deps: () => FlowRegistryDeps<TArgs>, options: FlowRegistryOptions): FlowRegistry<TArgs>;
74
+ /** An install's start: the host's business union mapped to the first view. */
75
+ export declare function beginInstall(install: (args: InstallArgs) => Promise<ShopInstallResult>): (args: InstallArgs) => Promise<InstallView>;
76
+ /** An uninstall's start. Its refusals: not in the catalog, not installed.
77
+ * The host reports no phase for an uninstall, which opens on `installing`. */
78
+ export declare function beginUninstall(uninstall: (args: {
79
+ name: string;
80
+ }) => Promise<ShopUninstallResult>): (args: {
81
+ name: string;
82
+ }) => Promise<InstallView>;
83
+ /** The shop's own update's start. Its refusal: a version that is not plain
84
+ * semver. */
85
+ export declare function beginUpdate(updateStart: (args: {
86
+ version: string;
87
+ }) => Promise<ShopUpdateResult>): (args: {
88
+ version: string;
89
+ }) => Promise<InstallView>;
@@ -1,17 +1,57 @@
1
1
  /**
2
- * Shop settings surface, browser half — one Settings section (`settings.section`)
3
- * that browses the catalog, installs with acknowledgement, toggles
4
- * enablement, lists installed plugins, and restarts dsh after installs.
2
+ * Shop surface, browser half — the shop's own page in dsh's main column
3
+ * (`main`), which browses the catalog, installs on one click, toggles
4
+ * enablement, lists installed plugins, and restarts dsh after installs; its
5
+ * row in the left sidebar (`sidebar.panellist`); and the two buttons on dsh's
6
+ * Plugins page that lead into it (`plugins.detail.actions`) (design
7
+ * 2026-10-16-shop-sidebar-page).
5
8
  * Mounts the shop Remote itself (the assembly does not know this package)
6
- * and holds no privilege beyond the nine `shop/*` methods (§5.3).
9
+ * and holds no privilege beyond the nine `shop/*` methods (the shop design's
10
+ * §5.3).
7
11
  */
8
12
  import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
9
13
  import type { ShopLocaleKey } from './locales.ts';
14
+ import { type PluginsSubject } from './ShopDetailAction.tsx';
10
15
  declare module '@deepseek-ai/dsh-client-ui-slots' {
11
16
  interface LocaleNamespaceMap {
12
- /** Shop Settings section copy. */
17
+ /** The shop page's copy. */
13
18
  'settings.shop': ShopLocaleKey;
14
19
  }
20
+ interface SlotMap {
21
+ /** dsh's left column of global panels (`dsh-client-ui-sidebar`, on
22
+ * 0.1.5-rc.3, 0.1.7-rc.2 and 0.2.0-rc.2): one row per entry, which owns
23
+ * the button, its label and its selection, and renders the entry's glyph
24
+ * at the size it asks. Declared here, in dsh's own terms, because the
25
+ * 0.1.1-rc.2 protocol this package builds against predates it — as
26
+ * `connection/reset` is below. */
27
+ 'sidebar.panellist': {
28
+ kind: 'list';
29
+ scope: 'root';
30
+ owner: {
31
+ size: number;
32
+ active: boolean;
33
+ };
34
+ };
35
+ /** The main column, keyed by panel id (`dsh-client-ui-layout`, the same
36
+ * harnesses): only the selected key is mounted, and the sidebar row with
37
+ * the same id selects it. Declared here for the same reason. */
38
+ 'main': {
39
+ kind: 'keyed';
40
+ scope: 'root';
41
+ };
42
+ /** Controls at the head of a detail page on dsh's Plugins page
43
+ * (`dsh-client-ui-plugin-manager`, 0.1.7-rc.2 and later), rendered with
44
+ * the page's subject; an entry renders null for a subject it has nothing
45
+ * for. On a harness without the Plugins page nothing declares it, and a
46
+ * registration into it waits without ever rendering. */
47
+ 'plugins.detail.actions': {
48
+ kind: 'list';
49
+ scope: 'root';
50
+ owner: {
51
+ subject: PluginsSubject;
52
+ };
53
+ };
54
+ }
15
55
  }
16
56
  declare module '@deepseek-ai/cordis' {
17
57
  interface Events {
@@ -26,10 +66,14 @@ declare module '@deepseek-ai/cordis' {
26
66
  }
27
67
  /** Dictionary namespace owned by this plugin. */
28
68
  export declare const NS = "settings.shop";
69
+ /** The id the shop's sidebar row and its main-column page share (design
70
+ * 2026-10-16-shop-sidebar-page §4.1). */
71
+ export declare const SHOP_PANEL = "shop";
29
72
  /** How long a stashed catalog may satisfy a plain open. The host applies the
30
73
  * same five-minute freshness window, so the client stash must not outlive it. */
31
74
  export declare const WARM_TTL_MS: number;
32
- /** Services required by the tab registration and the Remote mount.
75
+ /** Services required by the page, row and Plugins page button registrations
76
+ * and the Remote mount.
33
77
  *
34
78
  * `remote.shop` is deliberately ABSENT: this package both mounts its own
35
79
  * Remote (the assembly does not know it, §7.3) and consumes it, and cordis
@@ -40,5 +84,6 @@ export declare const WARM_TTL_MS: number;
40
84
  * namespace is therefore read through the reflect shop instead (`ctx.get`,
41
85
  * see apply), which has no inject requirement. */
42
86
  export declare const inject: string[];
43
- /** Mount the shop Remote and contribute the shop's Settings section. */
87
+ /** Mount the shop Remote and contribute the shop's page, its sidebar row and
88
+ * its buttons on dsh's Plugins page. */
44
89
  export declare function apply(ctx: ClientContext): Promise<void>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The listener plumbing the client's external stores share: the page store
3
+ * and its owed notices (page-store.ts), the flow registries
4
+ * (flow-registry.ts) and the restart monitor (restart-monitor.ts). Each keeps
5
+ * its listeners in a plain `Set`. React subscribes through `listen`, as
6
+ * `useSyncExternalStore` asks, and a change reaches every listener through
7
+ * `notify`.
8
+ *
9
+ * Functions over that `Set`, never an object a store keeps in its place. The
10
+ * restart monitor's store lives on `globalThis`, and a 0.8.x shop hands it
11
+ * over (design 2026-10-16-shop-sidebar-page §6). Its `listeners` is a plain
12
+ * `Set` that the predecessor's own code adds to, deletes from and iterates,
13
+ * so the field's shape is a contract across versions, and these functions
14
+ * take that `Set` as it is.
15
+ */
16
+ /** Add `listener`, and hand back what removes it. A forgotten set takes
17
+ * none: its store tells nobody anything more, so a listener subscribing in
18
+ * a hot swap's window would only be held. */
19
+ export declare function listen(listeners: Set<() => void>, listener: () => void): () => void;
20
+ /** Call every listener as the set stands when the call begins. Over a copy,
21
+ * so a listener that subscribes or unsubscribes one while it runs (React
22
+ * re-subscribing in the render the change caused) changes the next notify,
23
+ * never this one. */
24
+ export declare function notify(listeners: ReadonlySet<() => void>): void;
25
+ /** Drop every listener, and take none again: a disposed store tells nobody
26
+ * anything more. */
27
+ export declare function forget(listeners: Set<() => void>): void;
@@ -1,14 +1,21 @@
1
- /** Copy dictionaries for the shop's Settings section. */
1
+ /** Copy dictionaries for the shop's page in dsh's main column, its row in the
2
+ * sidebar, and its two buttons on dsh's Plugins page. */
2
3
  /** Simplified Chinese dictionary and key source of truth. */
3
4
  export declare const zh: {
4
- /** The shop's name in Settings (design §5.1, amended 2026-10-15): its nav
5
- * entry and its page title, one key so the two cannot drift apart. */
5
+ /** The shop's name: its sidebar row and its page title (design
6
+ * 2026-10-16-shop-sidebar-page §4.1), one key so the two cannot drift
7
+ * apart. */
6
8
  title: string;
7
- /** The shop's Settings page intro line (design §5.1, amended 2026-10-15),
8
- * as the built-in Models and 内置插件 pages carry one under their title. */
9
+ /** The page's intro line, under its title, as dsh's own pages carry one. */
9
10
  intro: string;
11
+ /** The Plugins page's button on the shop's own bundle page (design
12
+ * 2026-10-16-shop-sidebar-page §7). */
13
+ openShop: string;
14
+ /** Its button on a page of a bundle the shelf lists. */
15
+ viewInShop: string;
10
16
  loading: string;
11
17
  error: string;
18
+ installedLoading: string;
12
19
  installedError: string;
13
20
  retry: string;
14
21
  /** The shop's own error boundary caught a render error; the error's message
@@ -199,8 +206,11 @@ export type ShopLocaleKey = keyof typeof zh;
199
206
  export declare const en: {
200
207
  title: string;
201
208
  intro: string;
209
+ openShop: string;
210
+ viewInShop: string;
202
211
  loading: string;
203
212
  error: string;
213
+ installedLoading: string;
204
214
  installedError: string;
205
215
  retry: string;
206
216
  tabCrashed: string;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The shop page's state, held by the page rather than by the components that
3
+ * show it (design 2026-10-16-shop-sidebar-page §5).
4
+ *
5
+ * dsh mounts the shop only while it is on screen — the main column renders
6
+ * only the selected panel, as the Settings shell, the shop's home through
7
+ * 0.8.7, rendered only the open section — so what a component held in its
8
+ * own state went the moment the reader looked elsewhere: the search text,
9
+ * the filters, an install's progress and its receipt, the restart offer
10
+ * included (the shop design's §5.1, "Leaving the section"). This store keeps
11
+ * them, and it owns the flows' polling (flow-registry.ts), so an install the
12
+ * reader left still has its status folded and its receipt written.
13
+ *
14
+ * It sits on `globalThis` under a registry symbol for the reason
15
+ * `restart-monitor.ts` does: the symbol is the one thing a hot-swapped module
16
+ * instance shares with the one it replaced. Each `apply` still builds a new
17
+ * store (index.ts), because a re-applied bundle is a new page.
18
+ */
19
+ import type { InstallArgs, ShopInstallResult, ShopInstallStatusResult, ShopUninstallResult, ShopUpdateResult } from '../host/index.ts';
20
+ import { type FlowRegistry, type FlowRegistryOptions } from './flow-registry.ts';
21
+ import { type Category, type ShelfOrder } from './present.ts';
22
+ /** The reader's controls over the shelf, kept across leaving and returning. */
23
+ export interface ShelfView {
24
+ readonly query: string;
25
+ /** `installed` is a filter mode alongside the catalog categories: it selects
26
+ * by installed state, not by `catalog.category`. */
27
+ readonly category: Category | 'installed' | null;
28
+ /** Design 2026-10-12-shelf-orders §4. In memory like every control here: a
29
+ * page reload starts at the star order. */
30
+ readonly order: ShelfOrder;
31
+ /** Off by default: a filter nobody asked for must not hide listings on
32
+ * first open. Independent of `category`, because it subtracts from
33
+ * whatever the categories selected rather than competing with them. */
34
+ readonly hideIncompatible: boolean;
35
+ /** How far the shelf has grown, in §A1's batches. */
36
+ readonly visibleCount: number;
37
+ }
38
+ /** What a switch owes before the page shows the truth (ShopTab's
39
+ * `EnabledSwitch`). */
40
+ export type OwedNotice = 'reload' | 'restart';
41
+ /** Where a switch is drawn. An outdated plugin draws two, its card's and its
42
+ * Updatable row's, and each owes only what was done to it. */
43
+ declare const SWITCH_SURFACES: readonly ["card", "row"];
44
+ export type SwitchSurface = (typeof SWITCH_SURFACES)[number];
45
+ /** The owed map's key for one switch: `<surface>:<identity>`. */
46
+ export declare function switchKey(surface: SwitchSurface, identity: string): string;
47
+ /** The card the reader is at: the first on the shelf whose bottom lies below
48
+ * the page's top edge (design 2026-10-16-shop-sidebar-page §5.7). */
49
+ export interface ScrollAnchor {
50
+ /** Its identity (`identityKey`), since two listed entries can share a
51
+ * name. */
52
+ readonly key: string;
53
+ /** Its top relative to the page's top edge, in px: zero or negative while
54
+ * the card spans the edge, and at most the shelf's gap between cards while
55
+ * the edge falls in that gap. A reader above the shelf has no card. */
56
+ readonly offset: number;
57
+ }
58
+ export interface PageSnapshot {
59
+ readonly view: ShelfView;
60
+ /** A settle changed what the catalog's verdicts depend on, and no answer to
61
+ * a page's request for them since has landed (design
62
+ * 2026-09-01-harness-compatibility section 9.1;
63
+ * 2026-10-16-shop-sidebar-page §5.4). */
64
+ readonly reverdictPending: boolean;
65
+ /** How many reverdicts the page has been asked for: bumped with
66
+ * `reverdictPending` by every settle that raises one. A request carries the
67
+ * count it was asked at, so its answer clears only the latest. */
68
+ readonly reverdicts: number;
69
+ /** Bumped by every settle that may have changed the profile and by dsh
70
+ * restarting under the page: a mounted page re-reads the installed list. */
71
+ readonly settled: number;
72
+ }
73
+ /** The RPCs the store's flows run on: the shop's injected face. */
74
+ export interface PageStoreDeps {
75
+ install: (args: InstallArgs) => Promise<ShopInstallResult>;
76
+ uninstall: (args: {
77
+ name: string;
78
+ }) => Promise<ShopUninstallResult>;
79
+ updateStart: (args: {
80
+ version: string;
81
+ }) => Promise<ShopUpdateResult>;
82
+ installStatus: (args: {
83
+ installId: string;
84
+ }) => Promise<ShopInstallStatusResult>;
85
+ /** Tells the client half a package left the profile: index.ts's
86
+ * page-removed set, which the module table must never override. */
87
+ noteUninstalled?: (name: string) => void;
88
+ }
89
+ export interface ShopPageStore {
90
+ /** The shape a successor may read
91
+ * (design 2026-10-16-shop-sidebar-page §6). */
92
+ readonly schema: 1;
93
+ readonly installs: FlowRegistry<InstallArgs>;
94
+ readonly uninstalls: FlowRegistry<{
95
+ name: string;
96
+ }>;
97
+ readonly self: FlowRegistry<{
98
+ version: string;
99
+ }>;
100
+ getSnapshot: () => PageSnapshot;
101
+ subscribe: (listener: () => void) => () => void;
102
+ /** Publish the view with `patch` applied, unless that changes nothing: a
103
+ * pill pressed again, or a batch the shelf already shows. */
104
+ setView: (patch: Partial<ShelfView>) => void;
105
+ updateView: (update: (view: ShelfView) => Partial<ShelfView>) => void;
106
+ /** Whether the card of one identity is expanded. A channel of its own,
107
+ * apart from the page snapshot, as the owed notices below are: one card
108
+ * opens, and the shelf re-renders whole whenever the snapshot is
109
+ * replaced. */
110
+ isExpanded: (key: string) => boolean;
111
+ /** Be told when any card opens or closes. */
112
+ subscribeExpanded: (listener: () => void) => () => void;
113
+ /** Open the card if it is closed, close it if it is open. */
114
+ toggleExpanded: (key: string) => void;
115
+ /** Open the card, and leave it open if it is: a jump to one plugin opens
116
+ * its card from outside it. */
117
+ expand: (key: string) => void;
118
+ /** Put one plugin on the shelf, as the Plugins page's "View in Plugin
119
+ * shop" asks (design 2026-10-16-shop-sidebar-page §7): the search text
120
+ * set to its name, every category, nothing subtracted, the first batch,
121
+ * the top of the page, and the one card `card` names opened. Whatever
122
+ * search the reader left is replaced: they asked to see this plugin. */
123
+ focus: (name: string, card: string | null) => void;
124
+ /** What one switch owes (`switchKey`), or undefined when it owes nothing.
125
+ * A channel of its own, apart from the page snapshot: one switch renders a
126
+ * notice, and the shelf, which re-renders whole whenever the snapshot is
127
+ * replaced, has no use for it. */
128
+ getOwed: (switchKey: string) => OwedNotice | undefined;
129
+ /** Be told when any switch's notice changes. */
130
+ subscribeOwed: (listener: () => void) => () => void;
131
+ /** Read when a switch's toggle starts, and handed back with its answer
132
+ * (`setOwed`): it counts the times this page saw an installation leave the
133
+ * profile. */
134
+ owedEpoch: () => number;
135
+ /** Sticky, and a restart outranks a reload. `since` is the `owedEpoch` the
136
+ * toggle started at. An answer that lands after its identity left the
137
+ * profile, by an uninstall that landed or from the installed list a page
138
+ * read, re-creates nothing: it describes the installation before, and a
139
+ * reinstall draws fresh switches under the same keys. A restart under the
140
+ * page leaves every answer standing: the page notes one only from the new
141
+ * process's version answer, the old process sent its answers before it
142
+ * exited, and an answer that lands after the restart is noted is the new
143
+ * process's, whose restart is owed. */
144
+ setOwed: (switchKey: string, owed: OwedNotice, since: number) => void;
145
+ /** The identities the installed list a page read holds. Every switch of an
146
+ * identity it no longer holds owes nothing: the plugin left the profile,
147
+ * removed outside the shop (dsh's Plugins page, `dsh plugin remove`) when
148
+ * no uninstall of this page settled for it. */
149
+ noteInstalled: (identities: Iterable<string>) => void;
150
+ /** The answer to a page's reverdict request landed, a failed one
151
+ * included; `asked` is the `reverdicts` count it was asked at. An answer to
152
+ * an earlier one clears nothing: a settle since raised another, which the
153
+ * page has yet to ask for or to hear back about. */
154
+ clearReverdict: (asked: number) => void;
155
+ /** dsh restarted under the page (index.ts `onRestarted`). */
156
+ noteRestarted: () => void;
157
+ /** Where the page was scrolled to, and the card at its top edge (design
158
+ * 2026-10-16-shop-sidebar-page §5.7). Written on every scroll without
159
+ * telling anyone, since nothing renders from it; read once, when the page
160
+ * comes back. A position written without a card has none. */
161
+ recordScroll: (top: number, anchor?: ScrollAnchor | null) => void;
162
+ readScroll: () => number;
163
+ /** The card written with the position, or null. A view change that
164
+ * refilters or reorders the shelf forgets it, as `focus` does: the card is
165
+ * somewhere else on the new shelf, or gone, and no scroll event need follow
166
+ * to write the card the reader sees now. */
167
+ readAnchor: () => ScrollAnchor | null;
168
+ /** ShopPage reports its own mount here. The page is the only surface that
169
+ * knows it is on screen, and it knows it without asking dsh anything:
170
+ * `main` mounts a key's panel only while it is selected. Not published
171
+ * either: what reads it is the hot-swap hand-over record, which this
172
+ * instance's `main` entry takes as it leaves and the next instance's
173
+ * `apply` reads (index.ts; design 2026-10-16-shop-sidebar-page §6). */
174
+ notePageMounted: (mounted: boolean) => void;
175
+ /** As this instance's `main` entry leaves, record whether the page showed
176
+ * (index.ts; design 2026-10-16-shop-sidebar-page §6). Reads the mount flag
177
+ * above — NEVER dsh's layout service, whose selection observable is not
178
+ * the same object on every leg (measured 2026-10-16,
179
+ * design 2026-10-16-shop-sidebar-page §3.1; see index.ts's layout-face
180
+ * comment). */
181
+ recordTeardown: () => void;
182
+ /** What a successor reads of this instance. */
183
+ handover: () => Handover;
184
+ /** Stop the flows' polling and drop every listener. A disposed store
185
+ * publishes nothing and registers no listener again, so a hot swap's
186
+ * window cannot change it. What it held stays readable, and the store
187
+ * stays under its page key until the next `apply` replaces it: a successor
188
+ * reads what this instance left from there (design
189
+ * 2026-10-16-shop-sidebar-page §5.1, §6). */
190
+ dispose: () => void;
191
+ }
192
+ /** The shelf a page opens on: the star order, every category, one batch. */
193
+ export declare const INITIAL_VIEW: ShelfView;
194
+ /**
195
+ * @param deps read on every call, so the owner can hand over its latest
196
+ * functions without rebuilding the store.
197
+ * @param options the page's own store polls unwatched; a store a tab builds
198
+ * for itself does not, and stops with the tab (ShopTab).
199
+ */
200
+ export declare function createPageStore(deps: () => PageStoreDeps, options: FlowRegistryOptions): ShopPageStore;
201
+ /** What sits at the page key. A later shop's store, met after a downgrade,
202
+ * need not share this one's shape, so only an object that says it is of
203
+ * this `schema` is handed out as this store; anything else is reported as
204
+ * another schema's and goes no further (design 2026-10-16-shop-sidebar-page
205
+ * §5.1). */
206
+ export type PageKeyOccupant = {
207
+ readonly kind: 'none';
208
+ } | {
209
+ readonly kind: 'store';
210
+ readonly store: ShopPageStore;
211
+ } | {
212
+ readonly kind: 'other';
213
+ readonly occupant: unknown;
214
+ };
215
+ /** What a shop applied earlier left on this page: nothing, a store of this
216
+ * schema, or another schema's object. */
217
+ export declare function readPageStore(): PageKeyOccupant;
218
+ /** Make `store` the page's. Returns what it replaces. */
219
+ export declare function installPageStore(store: ShopPageStore): PageKeyOccupant;
220
+ /** What a successor reads of the instance it replaces
221
+ * (design 2026-10-16-shop-sidebar-page §6). */
222
+ export interface Handover {
223
+ /** Whether the shop page was showing when the instance's teardown began. */
224
+ readonly showing: boolean;
225
+ }
226
+ /** What applied on this page before the instance reading it
227
+ * (design 2026-10-16-shop-sidebar-page §6). */
228
+ export type Predecessor = {
229
+ readonly kind: 'none';
230
+ }
231
+ /** A shop that still lived in Settings: a 0.8.4–0.8.7 shop, which left the
232
+ * restart monitor's store and no page store, or a build with the page store
233
+ * but not yet the page, whose schema-1 store cannot hand over. */
234
+ | {
235
+ readonly kind: 'section';
236
+ }
237
+ /** A 0.9.0-or-later shop, and whether its page showed as it was torn down.
238
+ * A later shop's store of another schema reads as not showing: its
239
+ * hand-over cannot be read. */
240
+ | {
241
+ readonly kind: 'page';
242
+ readonly showing: boolean;
243
+ };
244
+ /** Read at the very start of `apply`, before this instance puts its own
245
+ * stores on the page. */
246
+ export declare function readPredecessor(): Predecessor;
247
+ /** Stop what the page key held, through the `dispose` it has and no other:
248
+ * an object that claims this schema and lacks one is stopped no more than a
249
+ * schema-2 object is, and neither can throw the shop off the page. */
250
+ export declare function disposeOccupant(occupant: PageKeyOccupant): void;
251
+ /** Test-only: dispose the page's store and forget it, as a reload does, a
252
+ * store of another shape included (`disposeOccupant`): a case that left one
253
+ * must not have its cleanup throw over its own failure. */
254
+ export declare function resetPageStore(): void;
255
+ export {};