@sleetdrop/dsh-plugin-topology 0.1.0

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,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Stroke-free minus glyph, matching the plus geometry. */
3
+ export function IconMinusOutline16({ size = 16, className }) {
4
+ return (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M1.5 7.34961H14.5V8.65039H1.5Z", fill: "currentColor" }) }));
5
+ }
6
+ /** Stroke-free plus glyph. */
7
+ export function IconPlusOutline16({ size = 16, className }) {
8
+ return (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z", fill: "currentColor" }) }));
9
+ }
10
+ /** Close cross glyph. */
11
+ export function IconCloseOutline16({ size = 16, className }) {
12
+ return (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M8 6.93934L14.4697 0.46967L15.5303 1.53033L9.06066 8L15.5303 14.4697L14.4697 15.5303L8 9.06066L1.53033 15.5303L0.46967 14.4697L6.93934 8L0.46967 1.53033L1.53033 0.46967L8 6.93934Z", fill: "currentColor" }) }));
13
+ }
14
+ /** Down chevron for expandable stats. */
15
+ export function IconChevronDownOutline14({ size = 14, className }) {
16
+ return (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z", fill: "currentColor" }) }));
17
+ }
18
+ /** Download arrow glyph. */
19
+ export function IconDownloadOutline16({ size = 16, className }) {
20
+ return (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M7.29414 1.5H8.70586V9.88477L12.1963 6.39429L13.1963 7.39429L8 12.5906L2.80371 7.39429L3.80371 6.39429L7.29414 9.88477V1.5Z", fill: "currentColor" }), _jsx("path", { d: "M1.5 13.5V14.8H14.5V13.5H1.5Z", fill: "currentColor" })] }));
21
+ }
22
+ /** Fullscreen / fit glyph. */
23
+ export function IconFullscreenOutline16({ size = 16, className }) {
24
+ return (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M1.5 1.5H6.5V2.8H2.8V6.5H1.5V1.5Z", fill: "currentColor" }), _jsx("path", { d: "M14.5 1.5H9.5V2.8H13.2V6.5H14.5V1.5Z", fill: "currentColor" }), _jsx("path", { d: "M1.5 14.5V9.5H2.8V13.2H6.5V14.5H1.5Z", fill: "currentColor" }), _jsx("path", { d: "M13.2 9.5H14.5V14.5H9.5V13.2H13.2V9.5Z", fill: "currentColor" })] }));
25
+ }
26
+ /** Plugin-mark glyph for the footer trigger. */
27
+ export function IconPluginOutline16({ size = 16, className }) {
28
+ return (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.3" }), _jsx("circle", { cx: "5.6", cy: "5.6", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "10.4", cy: "5.6", r: "1.1", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "9.4", r: "1.1", fill: "currentColor" }), _jsx("path", { d: "M5.6 5.6L8 9.4M10.4 5.6L8 9.4", stroke: "currentColor", strokeWidth: "1.1" })] }));
29
+ }
30
+ /** Minimal outline button (the rc.2 harness ships no shared Button). */
31
+ export function OutlineButton({ children, onClick, title, icon, }) {
32
+ const handle = (event) => {
33
+ event.preventDefault();
34
+ onClick();
35
+ };
36
+ return (_jsxs("button", { type: "button", className: "pluginTopologyButton", onClick: handle, title: title, children: [icon, _jsx("span", { children: children })] }));
37
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Browser half: a global plugin-topology panel reachable from the sidebar
3
+ * footer. A `sidebar.footer.action` occupant renders a trigger button and a
4
+ * fixed, centered panel that shows the live Host plugin dependency graph
5
+ * (metrics, legend, unresolved-dependency log, zoom/pan and format downloads).
6
+ *
7
+ * The Host service is consumed through the generated `remote` namespace, whose
8
+ * contribution this plugin mounts itself (`ctx.remote.$mount`). The mounted
9
+ * namespace service is read through `ctx.get('remote.pluginTopology')` — the
10
+ * Cordis no-inject read — because declaring it in `inject` would deadlock:
11
+ * the service does not exist until this plugin's own apply mounts it.
12
+ * @module @sleetdrop/dsh-plugin-topology/client
13
+ */
14
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
15
+ import { type PluginTopologyLocaleKey } from './locales.ts';
16
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
17
+ interface LocaleNamespaceMap {
18
+ /** Plugin dependency graph view copy. */
19
+ 'pluginTopology': PluginTopologyLocaleKey;
20
+ }
21
+ }
22
+ export type { TopologyViewInjected, TopologyViewProps } from './TopologyView.tsx';
23
+ export type { PluginTopologyLocaleKey } from './locales.ts';
24
+ export { TopologyPanel, type TopologyPanelProps } from './panel.tsx';
25
+ /** Services required by the panel. The topology namespace is self-mounted and
26
+ * read through ctx.get — see the module doc for the deadlock rationale. */
27
+ export declare const inject: string[];
28
+ /** Contribute the global topology panel trigger + panel to the sidebar footer. */
29
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Browser half: a global plugin-topology panel reachable from the sidebar
3
+ * footer. A `sidebar.footer.action` occupant renders a trigger button and a
4
+ * fixed, centered panel that shows the live Host plugin dependency graph
5
+ * (metrics, legend, unresolved-dependency log, zoom/pan and format downloads).
6
+ *
7
+ * The Host service is consumed through the generated `remote` namespace, whose
8
+ * contribution this plugin mounts itself (`ctx.remote.$mount`). The mounted
9
+ * namespace service is read through `ctx.get('remote.pluginTopology')` — the
10
+ * Cordis no-inject read — because declaring it in `inject` would deadlock:
11
+ * the service does not exist until this plugin's own apply mounts it.
12
+ * @module @sleetdrop/dsh-plugin-topology/client
13
+ */
14
+ // The generated Host Remote-descriptor contribution for this package.
15
+ import topologyRemote from "./remote-client.js";
16
+ import { createTopologyViewStore } from "./stores.js";
17
+ import { TopologyPanel } from "./panel.js";
18
+ import { en, NS, zh } from "./locales.js";
19
+ export { TopologyPanel } from "./panel.js";
20
+ /** Services required by the panel. The topology namespace is self-mounted and
21
+ * read through ctx.get — see the module doc for the deadlock rationale. */
22
+ export const inject = ['slots', 'locale', 'remote'];
23
+ /** Contribute the global topology panel trigger + panel to the sidebar footer. */
24
+ export function apply(ctx) {
25
+ // rc.2 locale API: per-locale registration (the typed dicts form needs a
26
+ // LocaleNamespaceMap merge this package deliberately avoids).
27
+ ctx.effect(() => {
28
+ const unregisterEn = ctx.locale.register(NS, 'en', en);
29
+ const unregisterZh = ctx.locale.register(NS, 'zh', zh);
30
+ return () => {
31
+ unregisterEn();
32
+ unregisterZh();
33
+ };
34
+ }, 'ui-plugin-topology: dictionaries');
35
+ // Mount the package's own Remote contribution so `remote.pluginTopology`
36
+ // becomes a live namespace service. The mounted service is read via
37
+ // ctx.get (no-inject) inside the panel callbacks, which run only after the
38
+ // user opens the panel — long after this mount has settled.
39
+ ctx.effect(() => ctx.remote.$mount(topologyRemote).then(dispose => dispose, (error) => {
40
+ console.error('[plugin-topology] remote contribution mount failed:', error);
41
+ return () => { };
42
+ }), 'ui-plugin-topology: mount remote contribution');
43
+ const t = ctx.locale.bind(NS);
44
+ const viewerStore = createTopologyViewStore();
45
+ const namespaceOf = () => {
46
+ const namespace = ctx.get('remote.pluginTopology');
47
+ if (namespace === undefined) {
48
+ throw new Error('pluginTopology remote namespace is not mounted yet — reopen the panel');
49
+ }
50
+ return namespace;
51
+ };
52
+ ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
53
+ name: 'sidebar.footer.action',
54
+ id: 'plugin-topology',
55
+ order: 100,
56
+ locale: NS,
57
+ label: () => t('title'),
58
+ store: viewerStore,
59
+ inject: () => ({
60
+ analyze: async () => {
61
+ const result = await namespaceOf().analyze();
62
+ if (!result.ok) {
63
+ throw new Error(`pluginTopology.analyze failed: ${result.error.code}: ${result.error.message}`);
64
+ }
65
+ return result.value;
66
+ },
67
+ render: async (format, rankdir) => {
68
+ const result = await namespaceOf().render(format, rankdir);
69
+ if (!result.ok) {
70
+ throw new Error(`pluginTopology.render failed: ${result.error.code}: ${result.error.message}`);
71
+ }
72
+ return result.value;
73
+ },
74
+ }),
75
+ }, TopologyPanel));
76
+ }
@@ -0,0 +1,4 @@
1
+ export declare const NS = "pluginTopology";
2
+ export type PluginTopologyLocaleKey = 'title' | 'loading' | 'error' | 'retry' | 'zoomIn' | 'zoomOut' | 'resetView' | 'zoomLevel' | 'downloadSvg' | 'downloadDot' | 'downloadJson' | 'metricPlugins' | 'metricDependencies' | 'metricUnresolved' | 'metricDensity' | 'metricWeakComponents' | 'metricDiameter' | 'rankdirToggle' | 'unresolvedPlugin' | 'unresolvedService' | 'unresolvedState' | 'legendLabel' | 'legendUnresolved' | 'legendIsolated' | 'legendId';
3
+ export declare const en: Record<PluginTopologyLocaleKey, string>;
4
+ export declare const zh: Record<PluginTopologyLocaleKey, string>;
@@ -0,0 +1,55 @@
1
+ export const NS = 'pluginTopology';
2
+ export const en = {
3
+ title: 'Plugin topology',
4
+ loading: 'Loading…',
5
+ error: 'Failed to load the plugin topology',
6
+ retry: 'Retry',
7
+ zoomIn: 'Zoom in',
8
+ zoomOut: 'Zoom out',
9
+ resetView: 'Reset view',
10
+ zoomLevel: 'Zoom level',
11
+ downloadSvg: 'SVG',
12
+ downloadDot: 'DOT',
13
+ downloadJson: 'JSON',
14
+ metricPlugins: 'Plugins',
15
+ metricDependencies: 'Dependencies',
16
+ metricUnresolved: 'Unresolved dependencies',
17
+ metricDensity: 'Coupling density',
18
+ metricWeakComponents: 'Disconnected parts',
19
+ metricDiameter: 'Deepest dependency chain',
20
+ rankdirToggle: 'Layout direction',
21
+ unresolvedPlugin: 'Plugin',
22
+ unresolvedService: 'Missing service',
23
+ unresolvedState: 'State',
24
+ legendLabel: 'Legend',
25
+ legendUnresolved: 'Unresolved dependency',
26
+ legendIsolated: 'Isolated plugin',
27
+ legendId: 'Instance creation ordinal within this run',
28
+ };
29
+ export const zh = {
30
+ title: '插件拓扑',
31
+ loading: '加载中…',
32
+ error: '加载插件拓扑失败',
33
+ retry: '重试',
34
+ zoomIn: '放大',
35
+ zoomOut: '缩小',
36
+ resetView: '复位视图',
37
+ zoomLevel: '缩放比例',
38
+ downloadSvg: 'SVG',
39
+ downloadDot: 'DOT',
40
+ downloadJson: 'JSON',
41
+ metricPlugins: '插件数',
42
+ metricDependencies: '依赖数',
43
+ metricUnresolved: '未解析依赖',
44
+ metricDensity: '耦合密度',
45
+ metricWeakComponents: '不相连的部分',
46
+ metricDiameter: '最深依赖链',
47
+ rankdirToggle: '布局方向',
48
+ unresolvedPlugin: '插件',
49
+ unresolvedService: '缺失服务',
50
+ unresolvedState: '状态',
51
+ legendLabel: '图例',
52
+ legendUnresolved: '存在未解析依赖',
53
+ legendIsolated: '孤立插件(无依赖边)',
54
+ legendId: '本次运行内的创建序号',
55
+ };
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import { type TopologyViewInjected, type ViewerStateSelector } from './TopologyView.tsx';
3
+ import type { PluginTopologyLocaleKey } from './locales.ts';
4
+ /** Props assembled by the slot renderer: store seat, injected face, and locale t. */
5
+ export type TopologyPanelProps = {
6
+ /** Wide/rail layout flag supplied by the sidebar footer-action slot. */
7
+ wide?: boolean;
8
+ useStore: ViewerStateSelector;
9
+ actions: {
10
+ setTransform: (transform: {
11
+ x: number;
12
+ y: number;
13
+ k: number;
14
+ } | null) => void;
15
+ };
16
+ t: (key: PluginTopologyLocaleKey) => string;
17
+ } & TopologyViewInjected;
18
+ /** The plugin-topology panel content wrapper and its footer trigger. */
19
+ export declare function TopologyPanel({ wide, useStore, actions, analyze, render, t, }: TopologyPanelProps): ReactNode;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { IconCloseOutline16, IconFullscreenOutline16, IconPluginOutline16 } from "./icons.js";
4
+ import { TopologyView } from "./TopologyView.js";
5
+ import css from './panel.module.css';
6
+ /** The plugin-topology panel content wrapper and its footer trigger. */
7
+ export function TopologyPanel({ wide, useStore, actions, analyze, render, t, }) {
8
+ const rootRef = useRef(null);
9
+ const [open, setOpen] = useState(false);
10
+ // Open maximized by default: the Graphviz canvas needs the space.
11
+ const [maximized, setMaximized] = useState(true);
12
+ // Close on outside pointer; mounted only while open, so the listener
13
+ // lifetime is the panel's.
14
+ useEffect(() => {
15
+ if (!open)
16
+ return;
17
+ const onDown = (event) => {
18
+ const root = rootRef.current;
19
+ if (root === null)
20
+ return;
21
+ const panel = root.querySelector('[data-plugin-topology-panel]');
22
+ if (event.target instanceof Node && panel !== null && !panel.contains(event.target)) {
23
+ setOpen(false);
24
+ }
25
+ };
26
+ document.addEventListener('pointerdown', onDown);
27
+ return () => { document.removeEventListener('pointerdown', onDown); };
28
+ }, [open]);
29
+ // Escape closes the panel.
30
+ useEffect(() => {
31
+ if (!open)
32
+ return;
33
+ const onKey = (event) => {
34
+ if (event.key === 'Escape')
35
+ setOpen(false);
36
+ };
37
+ window.addEventListener('keydown', onKey);
38
+ return () => { window.removeEventListener('keydown', onKey); };
39
+ }, [open]);
40
+ return (_jsxs("div", { ref: rootRef, className: css.layer, children: [_jsxs("button", { type: "button", className: css.trigger, "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => { setOpen(value => !value); }, children: [_jsx(IconPluginOutline16, { size: wide === true ? 16 : 18 }), wide === true && _jsx("span", { className: css.triggerLabel, children: t('title') })] }), open && (_jsxs("section", { className: maximized ? `${css.panel} ${css.maximized}` : css.panel, "data-plugin-topology-panel": "", role: "dialog", "aria-modal": "true", "aria-label": t('title'), children: [_jsxs("header", { className: css.header, children: [_jsx("span", { className: css.title, children: t('title') }), _jsxs("div", { className: css.headerActions, children: [_jsx("button", { type: "button", className: css.close, "aria-label": t('resetView'), title: t('resetView'), onClick: () => { setMaximized(value => !value); }, children: _jsx(IconFullscreenOutline16, { size: 14 }) }), _jsx("button", { type: "button", className: css.close, "aria-label": t('title'), onClick: () => { setOpen(false); }, children: _jsx(IconCloseOutline16, { size: 14 }) })] })] }), _jsx("div", { className: css.body, children: _jsx(TopologyView, { useStore: useStore, actions: actions, analyze: analyze, render: render, t: t }) })] }))] }));
41
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host Remote-descriptor contribution consumed by the unary gateway client.
3
+ *
4
+ * This mirrors the artifact the `@deepseek-ai/dsh-typert-generator` would emit
5
+ * from the Host FaceModel. It is checked in verbatim rather than regenerated at
6
+ * build time so the standalone repo builds without the generator's workspace
7
+ * scan; when the generator is wired, replace this module with its output.
8
+ * @module @sleetdrop/dsh-plugin-topology/client/remote-client
9
+ */
10
+ import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
11
+ export declare const TYPERT_REMOTE: TypertRemoteContribution;
12
+ export default TYPERT_REMOTE;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Host Remote-descriptor contribution consumed by the unary gateway client.
3
+ *
4
+ * This mirrors the artifact the `@deepseek-ai/dsh-typert-generator` would emit
5
+ * from the Host FaceModel. It is checked in verbatim rather than regenerated at
6
+ * build time so the standalone repo builds without the generator's workspace
7
+ * scan; when the generator is wired, replace this module with its output.
8
+ * @module @sleetdrop/dsh-plugin-topology/client/remote-client
9
+ */
10
+ import { z } from 'zod';
11
+ const analyze_result$schema = z.object({
12
+ 'graph': z.object({
13
+ 'kind': z.literal('bipartite').readonly(),
14
+ 'generatedAt': z.string().readonly(),
15
+ 'nodes': z.array(z.union([
16
+ z.object({
17
+ 'id': z.string().readonly(),
18
+ 'kind': z.literal('plugin').readonly(),
19
+ 'label': z.string().readonly(),
20
+ 'state': z.union([
21
+ z.literal('PENDING'), z.literal('LOADING'), z.literal('ACTIVE'),
22
+ z.literal('FAILED'), z.literal('DISPOSED'), z.literal('UNLOADING'),
23
+ ]).readonly(),
24
+ 'parent': z.union([z.literal(null), z.string()]).readonly(),
25
+ 'inject': z.array(z.string()).readonly(),
26
+ 'source': z.string().readonly().optional(),
27
+ }),
28
+ z.object({
29
+ 'id': z.string().readonly(),
30
+ 'kind': z.literal('service').readonly(),
31
+ 'label': z.string().readonly(),
32
+ 'scope': z.string().readonly(),
33
+ }),
34
+ ])).readonly(),
35
+ 'edges': z.array(z.object({
36
+ 'source': z.string().readonly(),
37
+ 'target': z.string().readonly(),
38
+ 'kind': z.union([z.literal('provides'), z.literal('injects')]).readonly(),
39
+ 'service': z.string().readonly(),
40
+ })).readonly(),
41
+ 'unresolved': z.array(z.object({
42
+ 'plugin': z.string().readonly(),
43
+ 'service': z.string().readonly(),
44
+ 'state': z.union([
45
+ z.literal('PENDING'), z.literal('LOADING'), z.literal('ACTIVE'),
46
+ z.literal('FAILED'), z.literal('DISPOSED'), z.literal('UNLOADING'),
47
+ ]).readonly(),
48
+ })).readonly(),
49
+ }).readonly(),
50
+ 'collapsed': z.object({
51
+ 'kind': z.literal('collapsed').readonly(),
52
+ 'generatedAt': z.string().readonly(),
53
+ 'nodes': z.array(z.object({
54
+ 'id': z.string().readonly(),
55
+ 'kind': z.literal('plugin').readonly(),
56
+ 'label': z.string().readonly(),
57
+ 'state': z.union([
58
+ z.literal('PENDING'), z.literal('LOADING'), z.literal('ACTIVE'),
59
+ z.literal('FAILED'), z.literal('DISPOSED'), z.literal('UNLOADING'),
60
+ ]).readonly(),
61
+ 'parent': z.union([z.literal(null), z.string()]).readonly(),
62
+ 'inject': z.array(z.string()).readonly(),
63
+ 'source': z.string().readonly().optional(),
64
+ })).readonly(),
65
+ 'edges': z.array(z.object({
66
+ 'source': z.string().readonly(),
67
+ 'target': z.string().readonly(),
68
+ 'service': z.string().readonly(),
69
+ })).readonly(),
70
+ }).readonly(),
71
+ 'nodeMetrics': z.record(z.string(), z.object({
72
+ 'inDegree': z.number().readonly(),
73
+ 'outDegree': z.number().readonly(),
74
+ 'degreeCentrality': z.number().readonly(),
75
+ 'betweennessCentrality': z.number().readonly(),
76
+ 'eigenvectorCentrality': z.number().readonly(),
77
+ 'pagerank': z.number().readonly(),
78
+ })).readonly(),
79
+ 'graphMetrics': z.object({
80
+ 'nodeCount': z.number().readonly(),
81
+ 'edgeCount': z.number().readonly(),
82
+ 'density': z.number().readonly(),
83
+ 'stronglyConnectedComponents': z.number().readonly(),
84
+ 'largestSccSize': z.number().readonly(),
85
+ 'isDag': z.boolean().readonly(),
86
+ 'weaklyConnectedComponents': z.number().readonly(),
87
+ 'diameter': z.number().readonly(),
88
+ 'averagePathLength': z.number().readonly(),
89
+ 'sourceCount': z.number().readonly(),
90
+ 'sinkCount': z.number().readonly(),
91
+ }).readonly(),
92
+ });
93
+ const render_parameter_0$schema = z.union([z.literal('json'), z.literal('dot'), z.literal('svg')]);
94
+ const render_parameter_1$schema = z.union([z.literal('TB'), z.literal('LR')]);
95
+ const render_result$schema = z.string();
96
+ export const TYPERT_REMOTE = {
97
+ package: '@sleetdrop/dsh-plugin-topology',
98
+ descriptors: [
99
+ {
100
+ id: '@sleetdrop/dsh-plugin-topology#pluginTopology/analyze',
101
+ service: 'pluginTopology',
102
+ namespace: 'pluginTopology',
103
+ method: 'analyze',
104
+ invocation: { kind: 'direct' },
105
+ parameters: [],
106
+ result: {
107
+ mode: 'strict',
108
+ typeSymbol: '@sleetdrop/dsh-plugin-topology/types#TopologyAnalysis',
109
+ schema: analyze_result$schema,
110
+ },
111
+ },
112
+ {
113
+ id: '@sleetdrop/dsh-plugin-topology#pluginTopology/render',
114
+ service: 'pluginTopology',
115
+ namespace: 'pluginTopology',
116
+ method: 'render',
117
+ invocation: { kind: 'direct' },
118
+ parameters: [
119
+ {
120
+ name: 'format',
121
+ wire: 'format',
122
+ source: 'json',
123
+ codec: {
124
+ mode: 'strict',
125
+ typeSymbol: '@sleetdrop/dsh-plugin-topology/types#RenderFormat',
126
+ schema: render_parameter_0$schema,
127
+ },
128
+ },
129
+ {
130
+ name: 'rankdir',
131
+ wire: 'rankdir',
132
+ source: 'json',
133
+ codec: {
134
+ mode: 'strict',
135
+ typeSymbol: '@sleetdrop/dsh-plugin-topology/types#Rankdir',
136
+ schema: render_parameter_1$schema,
137
+ },
138
+ },
139
+ ],
140
+ result: {
141
+ mode: 'strict',
142
+ typeSymbol: '@sleetdrop/dsh-plugin-topology#pluginTopology/render:result',
143
+ schema: render_result$schema,
144
+ },
145
+ },
146
+ ],
147
+ };
148
+ export default TYPERT_REMOTE;
@@ -0,0 +1,25 @@
1
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
2
+ /** The pan/zoom transform of the graph viewport. */
3
+ export interface TopologyTransform {
4
+ /** Horizontal translate in px. */
5
+ x: number;
6
+ /** Vertical translate in px. */
7
+ y: number;
8
+ /** Scale factor (1 = fit-to-viewport). */
9
+ k: number;
10
+ }
11
+ /** Viewer state: the remembered transform, or null until the first fit lands. */
12
+ type ViewerState = {
13
+ transform: TopologyTransform | null;
14
+ };
15
+ type ViewerActions = {
16
+ setTransform: (draft: ViewerState, transform: TopologyTransform | null) => void;
17
+ };
18
+ /**
19
+ * Create the shared root-scope viewer handle. The transform is written here so
20
+ * it survives the panel's unmount/remount when the user closes and reopens the
21
+ * global panel.
22
+ * @returns the store handle (spec + type + identity + factory in one).
23
+ */
24
+ export declare function createTopologyViewStore(): EngineStoreHandle<ViewerState, ViewerActions>;
25
+ export {};
@@ -0,0 +1,15 @@
1
+ import { defineStore } from '@deepseek-ai/dsh-client-runtime/client';
2
+ /**
3
+ * Create the shared root-scope viewer handle. The transform is written here so
4
+ * it survives the panel's unmount/remount when the user closes and reopens the
5
+ * global panel.
6
+ * @returns the store handle (spec + type + identity + factory in one).
7
+ */
8
+ export function createTopologyViewStore() {
9
+ return defineStore({
10
+ init: () => ({ transform: null }),
11
+ actions: {
12
+ setTransform: (d, transform) => { d.transform = transform; },
13
+ },
14
+ });
15
+ }
@@ -0,0 +1,9 @@
1
+ import type { RefObject } from 'react';
2
+ /**
3
+ * Close a popover when the pointer presses outside its root element.
4
+ * Local implementation: the 0.1.1-rc.2 harness ships no shared dismiss hook.
5
+ * @param ref - root element the interaction belongs to.
6
+ * @param open - whether the popover is currently open.
7
+ * @param setOpen - close state setter.
8
+ */
9
+ export declare function useDismissOnOutsidePointer(ref: RefObject<HTMLElement | null>, open: boolean, setOpen: (open: boolean) => void): void;
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'react';
2
+ /**
3
+ * Close a popover when the pointer presses outside its root element.
4
+ * Local implementation: the 0.1.1-rc.2 harness ships no shared dismiss hook.
5
+ * @param ref - root element the interaction belongs to.
6
+ * @param open - whether the popover is currently open.
7
+ * @param setOpen - close state setter.
8
+ */
9
+ export function useDismissOnOutsidePointer(ref, open, setOpen) {
10
+ useEffect(() => {
11
+ if (!open)
12
+ return;
13
+ const onDown = (event) => {
14
+ const root = ref.current;
15
+ if (root === null)
16
+ return;
17
+ if (event.target instanceof Node && !root.contains(event.target))
18
+ setOpen(false);
19
+ };
20
+ document.addEventListener('pointerdown', onDown);
21
+ return () => { document.removeEventListener('pointerdown', onDown); };
22
+ }, [ref, open, setOpen]);
23
+ }