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.
- package/lib/client.js +1529 -744
- package/lib/types/client/ShopDetailAction.d.ts +46 -0
- package/lib/types/client/ShopGlyph.d.ts +12 -0
- package/lib/types/client/ShopPage.d.ts +27 -0
- package/lib/types/client/ShopTab.d.ts +27 -10
- package/lib/types/client/flow-registry.d.ts +89 -0
- package/lib/types/client/index.d.ts +52 -7
- package/lib/types/client/listeners.d.ts +27 -0
- package/lib/types/client/locales.d.ts +15 -5
- package/lib/types/client/page-store.d.ts +255 -0
- package/lib/types/client/present.d.ts +26 -6
- package/lib/types/client/restart-monitor.d.ts +11 -0
- package/lib/types/client/useFlows.d.ts +34 -26
- package/lib/types/client/useUpdateSelf.d.ts +19 -16
- package/package.json +1 -1
- package/lib/types/client/ShopSection.d.ts +0 -10
- package/lib/types/client/useInstall.d.ts +0 -31
- package/lib/types/client/useUninstall.d.ts +0 -28
|
@@ -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
|
|
54
|
-
*
|
|
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
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
3
|
-
*
|
|
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 (
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
*
|
|
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
|
|
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 {};
|