@khorsheed/dsh-reader 0.2.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.
Files changed (58) hide show
  1. package/README.en.md +90 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +90 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +15119 -0
  6. package/lib/index.js +3660 -0
  7. package/lib/invariant.js +38 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +1738 -0
  10. package/lib/typert.remote-client.d.ts +82 -0
  11. package/lib/typert.remote-client.js +1115 -0
  12. package/lib/types/arxiv.d.ts +47 -0
  13. package/lib/types/arxiv.js +76 -0
  14. package/lib/types/client/ReaderPane.d.ts +71 -0
  15. package/lib/types/client/ReaderPane.js +3135 -0
  16. package/lib/types/client/contract.d.ts +254 -0
  17. package/lib/types/client/contract.js +1 -0
  18. package/lib/types/client/definition.d.ts +27 -0
  19. package/lib/types/client/definition.js +32 -0
  20. package/lib/types/client/extract-article.d.ts +102 -0
  21. package/lib/types/client/extract-article.js +1024 -0
  22. package/lib/types/client/icons.d.ts +27 -0
  23. package/lib/types/client/icons.js +11 -0
  24. package/lib/types/client/index.d.ts +34 -0
  25. package/lib/types/client/index.js +222 -0
  26. package/lib/types/client/locales.d.ts +391 -0
  27. package/lib/types/client/locales.js +382 -0
  28. package/lib/types/client/parse-rss.d.ts +69 -0
  29. package/lib/types/client/parse-rss.js +656 -0
  30. package/lib/types/client/quote.d.ts +89 -0
  31. package/lib/types/client/quote.js +119 -0
  32. package/lib/types/client/selectors.d.ts +167 -0
  33. package/lib/types/client/selectors.js +413 -0
  34. package/lib/types/client/session.d.ts +171 -0
  35. package/lib/types/client/session.js +223 -0
  36. package/lib/types/client/store.d.ts +181 -0
  37. package/lib/types/client/store.js +226 -0
  38. package/lib/types/client/translate.d.ts +379 -0
  39. package/lib/types/client/translate.js +873 -0
  40. package/lib/types/index.d.ts +48 -0
  41. package/lib/types/index.js +25 -0
  42. package/lib/types/invariant.d.ts +29 -0
  43. package/lib/types/invariant.js +38 -0
  44. package/lib/types/link-resolvers.d.ts +107 -0
  45. package/lib/types/link-resolvers.js +299 -0
  46. package/lib/types/remote.d.ts +224 -0
  47. package/lib/types/remote.js +275 -0
  48. package/lib/types/schedule.d.ts +78 -0
  49. package/lib/types/schedule.js +114 -0
  50. package/lib/types/service.d.ts +633 -0
  51. package/lib/types/service.js +1697 -0
  52. package/lib/types/store.d.ts +224 -0
  53. package/lib/types/store.js +916 -0
  54. package/lib/types/types.d.ts +710 -0
  55. package/lib/types/types.js +182 -0
  56. package/locale/en.json +6 -0
  57. package/locale/zh.json +6 -0
  58. package/package.json +128 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
+ * (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
+ * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
+ * no host runtime identity, identical on every line. Re-run the generator
6
+ * after adding an icon import or bumping the harness checkout.
7
+ *
8
+ * @module @khorsheed/dsh-reader/client
9
+ */
10
+ /** Shared props for every product icon component (mirrors the upstream IconProps). */
11
+ export interface IconProps {
12
+ /** Square edge in px; defaults to the glyph's own drawn size. */
13
+ size?: number | undefined;
14
+ /** Extra class for layout placement; color rides currentColor. */
15
+ className?: string | undefined;
16
+ }
17
+ export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
18
+ export declare const IconChevronLeftOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
19
+ export declare const IconClockOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
20
+ export declare const IconCopyOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
21
+ export declare const IconGlobeOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
22
+ export declare const IconPlusOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
23
+ export declare const IconRefreshOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
24
+ export declare const IconRightUpOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
25
+ export declare const IconSettingsOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
26
+ export declare const IconTrashOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
27
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
3
+ export const IconChevronLeftOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M10 4L6.70711 7.29289C6.31658 7.68342 6.31658 8.31658 6.70711 8.70711L10 12", stroke: "currentColor" }) }));
4
+ export const IconClockOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5Z", stroke: "currentColor" }), _jsx("path", { d: "M8 4V8.5L11.25 10.25", stroke: "currentColor" })] }));
5
+ export const IconCopyOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("rect", { x: "1.52075", y: "4.07373", width: "10.3932", height: "10.3932", rx: "2", stroke: "currentColor" }), _jsx("path", { d: "M11.9792 1.53296C13.36 1.53296 14.4792 2.65225 14.4792 4.03296V9.42847C14.4792 10.3756 13.9521 11.1987 13.1755 11.6228V10.3298C13.3652 10.0787 13.4792 9.7674 13.4792 9.42847V4.03296C13.4792 3.20453 12.8077 2.53296 11.9792 2.53296H6.58374C6.27966 2.53301 5.99684 2.6235 5.7605 2.77905H4.42358C4.85652 2.03463 5.66056 1.53304 6.58374 1.53296H11.9792Z", fill: "currentColor" })] }));
6
+ export const IconGlobeOutlineMedium = ({ size = 14, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M7.99986 14.0887C11.3626 14.0887 14.0886 11.3627 14.0886 7.99998C14.0886 4.63727 11.3626 1.91125 7.99986 1.91125C4.63715 1.91125 1.91113 4.63727 1.91113 7.99998C1.91113 11.3627 4.63715 14.0887 7.99986 14.0887Z", stroke: "currentColor" }), _jsx("path", { d: "M2.34619 8H13.6538", stroke: "currentColor", strokeLinecap: "square" }), _jsx("path", { d: "M7.99976 14.0889C9.23509 14.0889 10.1743 11.3629 10.1743 8.00006C10.1743 4.63739 9.23509 1.91138 7.99976 1.91138", stroke: "currentColor" }), _jsx("path", { d: "M7.99973 14.0889C6.76445 14.0889 5.8252 11.3629 5.8252 8.00006C5.8252 4.63739 6.76445 1.91138 7.99973 1.91138", stroke: "currentColor" })] }));
7
+ export const IconPlusOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M8 2V14", stroke: "currentColor" }), _jsx("path", { d: "M2 8H14", stroke: "currentColor" })] }));
8
+ export const IconRefreshOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M14.5001 8C14.5 9.28552 14.1188 10.5422 13.4045 11.611C12.6903 12.6799 11.6752 13.5129 10.4875 14.0049C9.29982 14.4968 7.99295 14.6255 6.73212 14.3747C5.4713 14.124 4.31314 13.505 3.4041 12.596C2.49514 11.687 1.87614 10.5288 1.62537 9.26798C1.37459 8.00716 1.50331 6.70028 1.99525 5.51261C2.48719 4.32494 3.32025 3.30981 4.3891 2.59557C5.45795 1.88134 6.71458 1.50008 8.0001 1.5C9.9001 1.5 11.7001 2.3 13.0001 3.6L14.5001 5.1", stroke: "currentColor" }), _jsx("path", { d: "M14.4999 1.5V5.1H10.8999", stroke: "currentColor" })] }));
9
+ export const IconRightUpOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M11.7256 2.77441C12.5538 2.77469 13.2256 3.44616 13.2256 4.27441V10.1416H12.2256V4.27441C12.2256 3.99844 12.0015 3.77469 11.7256 3.77441H5.7207V2.77441H11.7256Z", fill: "currentColor" }), _jsx("path", { d: "M2.77441 13.2255L12.3756 3.62427", stroke: "currentColor" })] }));
10
+ export const IconSettingsOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M8 9.75012C8.9665 9.75012 9.75 8.96662 9.75 8.00012C9.75 7.03362 8.9665 6.25012 8 6.25012C7.0335 6.25012 6.25 7.03362 6.25 8.00012C6.25 8.96662 7.0335 9.75012 8 9.75012Z", stroke: "currentColor" }), _jsx("path", { d: "M13.0107 7.79377C12.9505 7.89401 12.9205 7.94413 12.9205 7.99951C12.9205 8.0549 12.9505 8.10502 13.0106 8.20528L13.9849 9.83006C14.045 9.93029 14.0751 9.9804 14.0751 10.0358C14.0751 10.0911 14.045 10.1413 13.9849 10.2415L13.0037 11.8777C12.9468 11.9726 12.9184 12.0201 12.8725 12.0461C12.8267 12.072 12.7713 12.072 12.6607 12.072H10.6704C10.5598 12.072 10.5045 12.072 10.4586 12.098C10.4128 12.1239 10.3843 12.1714 10.3274 12.2662L9.33825 13.9142C9.28133 14.009 9.25287 14.0564 9.20703 14.0823C9.16118 14.1083 9.10588 14.1083 8.99529 14.1083H7.00486C6.89426 14.1083 6.83896 14.1083 6.79312 14.0823C6.74727 14.0564 6.71881 14.009 6.6619 13.9142L5.67273 12.2662C5.61581 12.1714 5.58735 12.1239 5.54151 12.098C5.49566 12.072 5.44036 12.072 5.32977 12.072H3.33945C3.2288 12.072 3.17347 12.072 3.12761 12.0461C3.08176 12.0201 3.0533 11.9726 2.9964 11.8777L2.0152 10.2415C1.9551 10.1413 1.92505 10.0911 1.92505 10.0358C1.92505 9.9804 1.9551 9.93029 2.0152 9.83006L2.98951 8.20528C3.04963 8.10502 3.07969 8.0549 3.07969 7.99951C3.07968 7.94413 3.04961 7.89401 2.98946 7.79377L2.01529 6.17011C1.95514 6.06987 1.92507 6.01975 1.92507 5.96437C1.92506 5.90899 1.95512 5.85886 2.01524 5.7586L2.9964 4.1224C3.0533 4.0275 3.08176 3.98005 3.12761 3.95408C3.17347 3.92811 3.2288 3.92811 3.33945 3.92811H5.32977C5.44036 3.92811 5.49566 3.92811 5.54151 3.90216C5.58735 3.87621 5.61581 3.82879 5.67273 3.73397L6.6619 2.08599C6.71881 1.99116 6.74727 1.94375 6.79312 1.9178C6.83896 1.89185 6.89426 1.89185 7.00486 1.89185H8.99529C9.10588 1.89185 9.16118 1.89185 9.20703 1.9178C9.25287 1.94375 9.28133 1.99116 9.33825 2.08599L10.3274 3.73397C10.3843 3.82879 10.4128 3.87621 10.4586 3.90216C10.5045 3.92811 10.5598 3.92811 10.6704 3.92811H12.6607C12.7713 3.92811 12.8267 3.92811 12.8725 3.95408C12.9184 3.98005 12.9468 4.0275 13.0037 4.1224L13.9849 5.7586C14.045 5.85886 14.0751 5.90899 14.0751 5.96437C14.0751 6.01975 14.045 6.06987 13.9849 6.17011L13.0107 7.79377Z", stroke: "currentColor", strokeMiterlimit: "10" })] }));
11
+ export const IconTrashOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M1.28149 3.88831H14.7187", stroke: "currentColor" }), _jsx("path", { d: "M5.41602 3.88833V2.47962C5.41602 2.29282 5.52492 2.11366 5.71876 1.98157C5.9126 1.84948 6.17551 1.77527 6.44964 1.77527H9.55053C9.82466 1.77527 10.0876 1.84948 10.2814 1.98157C10.4753 2.11366 10.5842 2.29282 10.5842 2.47962V3.88833", stroke: "currentColor" }), _jsx("path", { d: "M2.57349 3.88831L3.19366 13.2943C3.21937 13.5502 3.33952 13.7872 3.53065 13.9593C3.72178 14.1313 3.97016 14.2259 4.22729 14.2246H11.7728C12.0299 14.2259 12.2783 14.1313 12.4694 13.9593C12.6605 13.7872 12.7807 13.5502 12.8064 13.2943L13.4266 3.88831", stroke: "currentColor" }), _jsx("path", { d: "M6.44946 6.98926V11.1238", stroke: "currentColor" }), _jsx("path", { d: "M9.55054 6.98926V11.1238", stroke: "currentColor" })] }));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The reader's browser half: mount the Remote, register the dictionaries and
3
+ * the right-Sidebar tab, and hand the pane the four gestures it cannot perform
4
+ * for itself (the conversation draft, the clipboard, the browser, and the
5
+ * side-chat hop).
6
+ *
7
+ * Failure policy is the repo's "degrade, don't explode": genuinely required
8
+ * wiring (`slots`, `remote`, `locale`, `sidebarRightTabs`) is declared in
9
+ * `inject` and logged when it fails, while every OPTIONAL capability is probed
10
+ * per gesture — no `sideChat` hides the side-chat action, no conversation
11
+ * surface makes the draft gestures no-ops, and an unavailable clipboard reports
12
+ * failure instead of pretending to have copied.
13
+ *
14
+ * @module @khorsheed/dsh-reader/client
15
+ */
16
+ import type { Context } from '@deepseek-ai/cordis';
17
+ import { ReaderPane } from './ReaderPane.tsx';
18
+ export { ReaderPane };
19
+ export { READER_KIND, READER_TAB_ID } from './definition.tsx';
20
+ /**
21
+ * Required services: the pane seat, the wire channel, the locale,
22
+ * the tab-type registry, and sessions (the conversation draft lives behind a
23
+ * session scope, and the session id alone is not enough to reach it).
24
+ */
25
+ export declare const inject: string[];
26
+ /**
27
+ * Client plugin body: mount the Remote, register the dictionaries and the
28
+ * sidebar tab, and return the disposer.
29
+ *
30
+ * @param ctx - client root context.
31
+ * @returns the teardown for everything this body registered.
32
+ */
33
+ export declare function apply(ctx: Context): Promise<() => Promise<void>>;
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,222 @@
1
+ import readerRemote from '@khorsheed/dsh-reader/remote';
2
+ import { extractArticle } from "./extract-article.js";
3
+ import { translationHash } from "./translate.js";
4
+ import { READER_TAB_ID, readerDefinition } from "./definition.js";
5
+ import { ReaderPane } from "./ReaderPane.js";
6
+ import { createReaderStore } from "./store.js";
7
+ import { en, NS, zh } from "./locales.js";
8
+ export { ReaderPane };
9
+ export { READER_KIND, READER_TAB_ID } from "./definition.js";
10
+ /**
11
+ * Required services: the pane seat, the wire channel, the locale,
12
+ * the tab-type registry, and sessions (the conversation draft lives behind a
13
+ * session scope, and the session id alone is not enough to reach it).
14
+ */
15
+ export const inject = ['slots', 'remote', 'locale', 'sidebarRightTabs', 'sessions'];
16
+ /**
17
+ * Client plugin body: mount the Remote, register the dictionaries and the
18
+ * sidebar tab, and return the disposer.
19
+ *
20
+ * @param ctx - client root context.
21
+ * @returns the teardown for everything this body registered.
22
+ */
23
+ export async function apply(ctx) {
24
+ const disposers = [];
25
+ try {
26
+ disposers.push(await ctx.remote.$mount(readerRemote));
27
+ }
28
+ catch (error) {
29
+ // A Remote already mounted by another composition fails loud; the rest of
30
+ // the plugin still registers, so the tab shows its empty state rather than
31
+ // disappearing.
32
+ ctx.logger.error(error);
33
+ }
34
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'reader: dictionaries');
35
+ const t = ctx.locale.bind(NS);
36
+ const remote = ctx.get('remote.reader');
37
+ /** Copy text through the clipboard API, reporting whether it worked. */
38
+ const copyText = async (text) => {
39
+ try {
40
+ await navigator.clipboard.writeText(text);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ };
47
+ /**
48
+ * Open a URL in the user's browser.
49
+ *
50
+ * The host exposes no plugin-facing "open this URL" service today (the
51
+ * open-in-app routes launch local files into an app), so this rides the
52
+ * browser's own user-invoked navigation: a click handler calling
53
+ * `window.open` with `noopener` is a user gesture, not a popup. The design
54
+ * records this as the one unverified seam, to be re-checked on the
55
+ * acceptance instance.
56
+ */
57
+ const openExternal = (url) => {
58
+ try {
59
+ const opened = window.open(url, '_blank', 'noopener,noreferrer');
60
+ if (opened !== null)
61
+ return true;
62
+ }
63
+ catch {
64
+ // fall through to the anchor path
65
+ }
66
+ try {
67
+ const anchor = document.createElement('a');
68
+ anchor.href = url;
69
+ anchor.target = '_blank';
70
+ anchor.rel = 'noopener noreferrer';
71
+ anchor.click();
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ };
78
+ /**
79
+ * The in-app Sidebar Browser, probed live (the canvas/quote pattern).
80
+ *
81
+ * Two services, both optional: the tab-type registry answers whether the
82
+ * `browser` kind is registered (host 0.1.6-alpha.2 mounts it; older hosts do
83
+ * not), and the navigation face performs the open. `openTab` throws for a
84
+ * kind nothing registered and for a missing session binding, so the call is
85
+ * wrapped — the pane's fallback is the external link.
86
+ */
87
+ const sidebarBrowser = () => {
88
+ const tabs = ctx.get('sidebarRightTabs');
89
+ if (tabs?.get('browser') === undefined)
90
+ return undefined;
91
+ return ctx.get('sidebarRight');
92
+ };
93
+ /**
94
+ * Fetch one entry's article and cache the extraction.
95
+ *
96
+ * The split is the host/browser boundary: the host owns the network (its
97
+ * sanctioned egress seam), this process owns the DOM, and neither can do the
98
+ * other's half — so the view calls this one function and both halves happen.
99
+ */
100
+ const fetchEntryBody = async (entryId, url) => {
101
+ const fetched = await remote.fetchEntryBody({ entryId, url });
102
+ if (!fetched.ok) {
103
+ return { entryId, cached: false, fresh: false, fromFeed: false, error: fetched.error.message };
104
+ }
105
+ const value = fetched.value;
106
+ if (value.raw === undefined || value.url === undefined) {
107
+ return { entryId, cached: false, fresh: false, fromFeed: false, error: value.error ?? 'fetch-failed' };
108
+ }
109
+ const extracted = extractArticle(value.raw, value.url);
110
+ if (!extracted.ok) {
111
+ return { entryId, cached: false, fresh: false, fromFeed: false, error: extracted.error };
112
+ }
113
+ const meta = {
114
+ ...(extracted.title === undefined ? {} : { title: extracted.title }),
115
+ ...(extracted.excerpt === undefined ? {} : { excerpt: extracted.excerpt }),
116
+ };
117
+ const stored = await remote.storeEntryBody({
118
+ entryId,
119
+ url: value.url,
120
+ html: extracted.html,
121
+ // The entry's translation map is keyed to the body's hash: a refetch that
122
+ // changes the body retires the map in the same commit.
123
+ bodyHash: translationHash(extracted.html),
124
+ ...meta,
125
+ ...(value.truncated === true ? { truncated: true } : {}),
126
+ ...(extracted.scriptFigures === undefined ? {} : { scriptFigures: extracted.scriptFigures }),
127
+ });
128
+ return stored.ok
129
+ ? { ...stored.value, ...meta }
130
+ : { entryId, cached: false, fresh: false, fromFeed: false, error: stored.error.message };
131
+ };
132
+ /** The injected business face, identical for whichever session mounts it. */
133
+ const face = (sessionId) => ({
134
+ capabilities: () => remote.capabilities(),
135
+ listSources: () => remote.listSources(),
136
+ addSource: url => remote.addSource({ url }),
137
+ updateSource: request => remote.updateSource(request),
138
+ removeSource: (id, entryIds) => remote.removeSource({ id, ...(entryIds === undefined ? {} : { entryIds }) }),
139
+ refresh: ids => remote.refresh(ids === undefined ? {} : { ids }),
140
+ getBodies: ids => remote.getBodies({ ids }),
141
+ quoteToSideChat: request => remote.quoteToSideChat(request),
142
+ entryFetchStates: entryIds => remote.entryFetchStates({ entryIds }),
143
+ getRawBody: entryId => remote.getRawBody({ entryId }),
144
+ storeEntryBody: request => remote.storeEntryBody(request),
145
+ listBackfillCandidates: entries => remote.listBackfillCandidates({ entries }),
146
+ getEntryBody: request => remote.getEntryBody(request),
147
+ fetchEntryBody,
148
+ getEntryTranslation: entryId => remote.getEntryTranslation({ entryId }),
149
+ getSentenceTranslations: request => remote.getSentenceTranslations(request),
150
+ rememberSentences: request => remote.rememberSentences(request),
151
+ entryTags: entryId => remote.entryTags({ entryId }),
152
+ listTags: () => remote.listTags(),
153
+ createTag: name => remote.createTag({ name }),
154
+ tagEntry: (entryId, tagId, on) => remote.tagEntry({ entryId, tagId, on }),
155
+ renameTag: (id, name) => remote.renameTag({ id, name }),
156
+ deleteTag: id => remote.deleteTag({ id }),
157
+ pruneTags: () => remote.pruneTags(),
158
+ recordRead: request => remote.recordRead(request),
159
+ listRecent: () => remote.listRecent(),
160
+ clearRecent: () => remote.clearRecent(),
161
+ getCachePolicy: () => remote.getCachePolicy(),
162
+ setCachePolicy: (ttlHours, maxEntries, translationBudgetChars) => remote.setCachePolicy({
163
+ ttlHours,
164
+ ...(maxEntries === undefined ? {} : { maxEntries }),
165
+ ...(translationBudgetChars === undefined ? {} : { translationBudgetChars }),
166
+ }),
167
+ getStorageStats: () => remote.getStorageStats(),
168
+ clearTranslations: () => remote.clearTranslations(),
169
+ readDraft: () => readDraft(sessionId),
170
+ setDraft: merged => { setDraft(sessionId, merged); },
171
+ copyText,
172
+ openExternal,
173
+ browserTabAvailable: () => sidebarBrowser() !== undefined,
174
+ openBrowserTab: url => {
175
+ const browser = sidebarBrowser();
176
+ if (browser === undefined)
177
+ return false;
178
+ try {
179
+ browser.openTab('browser', { params: { url } });
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ },
186
+ // The capture package (the ingest proposal's M1): probed live per gesture,
187
+ // absent today — the 「渲染抓取」 slot renders only while this answers.
188
+ captureRemote: () => {
189
+ const capture = ctx.get('remote.capture');
190
+ return typeof capture?.render === 'function' ? capture : undefined;
191
+ },
192
+ });
193
+ /** The conversation input for a session, when the composition has one. */
194
+ const inputFor = (sessionId) => {
195
+ const scope = ctx.get('sessions')?.scope(sessionId);
196
+ return scope?.get('conversation')?.input.for(scope);
197
+ };
198
+ /** Read the session's current draft; an empty string when there is none. */
199
+ const readDraft = (sessionId) => {
200
+ const input = inputFor(sessionId);
201
+ return input?.state.getSnapshot().draft ?? '';
202
+ };
203
+ /** Replace the session's draft; a silent no-op without a conversation surface. */
204
+ const setDraft = (sessionId, merged) => {
205
+ inputFor(sessionId)?.setDraft(merged);
206
+ };
207
+ // Two-stage right-Sidebar registration: the type into the registry, the body
208
+ // into the keyed pane seat under the type's id. Both ride `ctx.effect` so a
209
+ // reload or unload removes them.
210
+ ctx.effect(() => ctx.sidebarRightTabs.register(readerDefinition(t)), 'reader: tab type');
211
+ ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
212
+ name: 'sidebar.right.pane.tab',
213
+ key: READER_TAB_ID,
214
+ locale: NS,
215
+ store: createReaderStore,
216
+ inject: face,
217
+ }, ReaderPane)), 'reader: sidebar tab body');
218
+ return async () => {
219
+ for (const dispose of disposers.reverse())
220
+ await dispose();
221
+ };
222
+ }