@khorsheed/dsh-mobile 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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +132 -0
  4. package/README.zh.md +132 -0
  5. package/cordis.patch.yml +3 -0
  6. package/examples/https-ingress.mjs +84 -0
  7. package/examples/safari-stream-compat.mjs +98 -0
  8. package/lib/client.js +6138 -0
  9. package/lib/index.js +172 -0
  10. package/lib/invariant.js +20 -0
  11. package/lib/types/client/ConnectPhone.d.ts +9 -0
  12. package/lib/types/client/ConnectPhone.js +83 -0
  13. package/lib/types/client/DirectoryFlow.d.ts +14 -0
  14. package/lib/types/client/DirectoryFlow.js +75 -0
  15. package/lib/types/client/MobileChrome.d.ts +15 -0
  16. package/lib/types/client/MobileChrome.js +124 -0
  17. package/lib/types/client/MobileIcon.d.ts +6 -0
  18. package/lib/types/client/MobileIcon.js +16 -0
  19. package/lib/types/client/MobileLibrary.d.ts +13 -0
  20. package/lib/types/client/MobileLibrary.js +73 -0
  21. package/lib/types/client/MobileQueue.d.ts +9 -0
  22. package/lib/types/client/MobileQueue.js +90 -0
  23. package/lib/types/client/MobileRooms.d.ts +13 -0
  24. package/lib/types/client/MobileRooms.js +80 -0
  25. package/lib/types/client/MobileSeats.d.ts +4 -0
  26. package/lib/types/client/MobileSeats.js +5 -0
  27. package/lib/types/client/MobileTools.d.ts +7 -0
  28. package/lib/types/client/MobileTools.js +34 -0
  29. package/lib/types/client/SessionRename.d.ts +10 -0
  30. package/lib/types/client/SessionRename.js +43 -0
  31. package/lib/types/client/SubmissionFocus.d.ts +26 -0
  32. package/lib/types/client/SubmissionFocus.js +74 -0
  33. package/lib/types/client/composerActions.d.ts +24 -0
  34. package/lib/types/client/composerActions.js +66 -0
  35. package/lib/types/client/composerFocus.d.ts +15 -0
  36. package/lib/types/client/composerFocus.js +41 -0
  37. package/lib/types/client/directory.d.ts +17 -0
  38. package/lib/types/client/directory.js +54 -0
  39. package/lib/types/client/foreground.d.ts +4 -0
  40. package/lib/types/client/foreground.js +47 -0
  41. package/lib/types/client/gestures.d.ts +4 -0
  42. package/lib/types/client/gestures.js +118 -0
  43. package/lib/types/client/index.d.ts +20 -0
  44. package/lib/types/client/index.js +144 -0
  45. package/lib/types/client/locales.d.ts +145 -0
  46. package/lib/types/client/locales.js +80 -0
  47. package/lib/types/client/messageMenu.d.ts +19 -0
  48. package/lib/types/client/messageMenu.js +99 -0
  49. package/lib/types/client/native.d.ts +6 -0
  50. package/lib/types/client/native.js +15 -0
  51. package/lib/types/client/navigation.d.ts +41 -0
  52. package/lib/types/client/navigation.js +61 -0
  53. package/lib/types/client/pickers.d.ts +17 -0
  54. package/lib/types/client/pickers.js +109 -0
  55. package/lib/types/client/presentation.d.ts +38 -0
  56. package/lib/types/client/presentation.js +146 -0
  57. package/lib/types/client/queue.d.ts +47 -0
  58. package/lib/types/client/queue.js +26 -0
  59. package/lib/types/client/roomActions.d.ts +6 -0
  60. package/lib/types/client/roomActions.js +41 -0
  61. package/lib/types/client/rooms.d.ts +44 -0
  62. package/lib/types/client/rooms.js +75 -0
  63. package/lib/types/client/styles.d.ts +4 -0
  64. package/lib/types/client/styles.js +434 -0
  65. package/lib/types/client/surface.d.ts +17 -0
  66. package/lib/types/client/surface.js +112 -0
  67. package/lib/types/client/viewport.d.ts +13 -0
  68. package/lib/types/client/viewport.js +42 -0
  69. package/lib/types/connect.d.ts +7 -0
  70. package/lib/types/connect.js +61 -0
  71. package/lib/types/directories.d.ts +5 -0
  72. package/lib/types/directories.js +39 -0
  73. package/lib/types/index.d.ts +9 -0
  74. package/lib/types/index.js +21 -0
  75. package/lib/types/invariant.d.ts +16 -0
  76. package/lib/types/invariant.js +17 -0
  77. package/lib/types/protocol.d.ts +30 -0
  78. package/lib/types/protocol.js +17 -0
  79. package/locale/en.json +1 -0
  80. package/locale/zh.json +1 -0
  81. package/package.json +102 -0
package/lib/index.js ADDED
@@ -0,0 +1,172 @@
1
+ import { opendir, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ //#region lib/types/protocol.js
5
+ /** Identifiers shared by the plugin and its optional native shell. */
6
+ const MOBILE_VERSION = "0.1.0";
7
+ const HANDSHAKE_PATH = "/api/mobile/handshake";
8
+ const CONNECT_PATH = "/api/mobile/connect";
9
+ /** Read-only feature discovery; this does not create a login or grant access. */
10
+ function mobileHandshake() {
11
+ return {
12
+ mobileVersion: MOBILE_VERSION,
13
+ bridgeVersion: 1,
14
+ capabilities: [
15
+ "responsive-layout",
16
+ "official-composer",
17
+ "browser-session-auth"
18
+ ],
19
+ authentication: "official-browser-session",
20
+ devicePairing: false,
21
+ pushNotifications: false
22
+ };
23
+ }
24
+ const DIRECTORY_PATH = "/api/mobile/directories";
25
+ //#endregion
26
+ //#region lib/types/connect.js
27
+ const headers = {
28
+ "cache-control": "no-store",
29
+ "referrer-policy": "no-referrer"
30
+ };
31
+ /** Deployment-owned destination only: a browser cannot choose where a bearer is sent. */
32
+ function connectInfo(connection, configured) {
33
+ if (!configured) return {
34
+ state: "not-configured",
35
+ origin: null
36
+ };
37
+ let url;
38
+ try {
39
+ url = new URL(configured);
40
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/" || [
41
+ "localhost",
42
+ "127.0.0.1",
43
+ "[::1]"
44
+ ].includes(url.hostname)) throw new Error("invalid");
45
+ } catch {
46
+ return {
47
+ state: "invalid-origin",
48
+ origin: null
49
+ };
50
+ }
51
+ if (typeof connection.authenticatedUrl !== "function" || typeof connection.requestRejection !== "function") return {
52
+ state: "unsupported",
53
+ origin: url.origin
54
+ };
55
+ if (connection.requestRejection({ headers: {
56
+ host: url.host,
57
+ origin: url.origin
58
+ } }) === 403) return {
59
+ state: "untrusted-origin",
60
+ origin: url.origin
61
+ };
62
+ return {
63
+ state: "ready",
64
+ origin: url.origin
65
+ };
66
+ }
67
+ /** Called only behind the official authenticated Fetch registry. */
68
+ function connectResponse(connection, configured) {
69
+ return async (request) => {
70
+ const info = connectInfo(connection, configured);
71
+ if (request.method === "GET") return Response.json(info, { headers });
72
+ let sameOrigin = false;
73
+ try {
74
+ const origin = new URL(request.headers.get("origin") ?? "");
75
+ const host = request.headers.get("host");
76
+ sameOrigin = !!host && ["http:", "https:"].includes(origin.protocol) && origin.host === new URL(`http://${host}`).host;
77
+ } catch {}
78
+ if (!sameOrigin || request.headers.get("content-type") !== "application/json") return Response.json({ error: "forbidden" }, {
79
+ status: 403,
80
+ headers
81
+ });
82
+ if (info.state !== "ready" || !info.origin) return Response.json(info, {
83
+ status: 409,
84
+ headers
85
+ });
86
+ try {
87
+ const login = new URL(connection.authenticatedUrl(info.origin));
88
+ if (login.origin !== info.origin || login.pathname !== "/" || login.username || login.password || login.hash || !login.searchParams.get("token") || [...login.searchParams].length !== 1) throw new Error("unsupported");
89
+ return Response.json({
90
+ origin: info.origin,
91
+ loginUrl: login.href
92
+ }, { headers });
93
+ } catch {
94
+ return Response.json({ error: "unavailable" }, {
95
+ status: 503,
96
+ headers
97
+ });
98
+ }
99
+ };
100
+ }
101
+ //#endregion
102
+ //#region lib/types/directories.js
103
+ /** Read-only companion for native-picker hosts; never changes the shared picker backend. */
104
+ async function listMobileDirectory(path, signal) {
105
+ if (path !== void 0 && (!path || path.length > 4096 || path.includes("\0") || !isAbsolute(path) || process.platform === "win32" && !/^(?:[A-Za-z]:[\\/]|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)/.test(path))) throw new Error("invalid-path");
106
+ signal.throwIfAborted();
107
+ const target = resolve(path ?? homedir());
108
+ const entries = [];
109
+ let scanned = 0, truncated = false;
110
+ for await (const entry of await opendir(target)) {
111
+ signal.throwIfAborted();
112
+ if (++scanned > 1e4 || entries.length >= 1e3) {
113
+ truncated = true;
114
+ break;
115
+ }
116
+ const child = join(target, entry.name);
117
+ if (entry.isDirectory() || entry.isSymbolicLink() && await stat(child).then((s) => s.isDirectory(), () => false)) entries.push({
118
+ name: entry.name,
119
+ path: child,
120
+ hidden: entry.name.startsWith(".")
121
+ });
122
+ }
123
+ signal.throwIfAborted();
124
+ entries.sort((a, b) => a.name.localeCompare(b.name));
125
+ return {
126
+ path: target,
127
+ parent: dirname(target) === target ? null : dirname(target),
128
+ entries,
129
+ truncated
130
+ };
131
+ }
132
+ async function directoryResponse(request) {
133
+ try {
134
+ const path = new URL(request.url).searchParams.get("path") ?? void 0;
135
+ return Response.json(await listMobileDirectory(path, request.signal), { headers: { "cache-control": "no-store" } });
136
+ } catch (error) {
137
+ const invalid = error instanceof Error && error.message === "invalid-path";
138
+ return Response.json({ error: invalid ? "invalid-path" : "directory-unavailable" }, {
139
+ status: request.signal.aborted ? 499 : invalid ? 400 : 422,
140
+ headers: { "cache-control": "no-store" }
141
+ });
142
+ }
143
+ }
144
+ //#endregion
145
+ //#region lib/types/index.js
146
+ const name = "mobile";
147
+ const inject = [];
148
+ /** Attach only when the official Connection carrier is composed. */
149
+ function apply(ctx, config = {}) {
150
+ ctx.inject(["connection"], (web) => {
151
+ web.connection.fetch.register({
152
+ path: CONNECT_PATH,
153
+ methods: ["GET", "POST"],
154
+ requestBody: "buffered",
155
+ fetch: connectResponse(web.connection, config.publicOrigin ?? process.env.DSH_MOBILE_PUBLIC_ORIGIN)
156
+ });
157
+ web.connection.fetch.register({
158
+ path: DIRECTORY_PATH,
159
+ methods: ["GET"],
160
+ requestBody: "buffered",
161
+ fetch: directoryResponse
162
+ });
163
+ web.connection.fetch.register({
164
+ path: HANDSHAKE_PATH,
165
+ methods: ["GET"],
166
+ requestBody: "buffered",
167
+ fetch: async () => Response.json(mobileHandshake(), { headers: { "cache-control": "no-store" } })
168
+ });
169
+ });
170
+ }
171
+ //#endregion
172
+ export { apply, inject, name };
@@ -0,0 +1,20 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@khorsheed/dsh-mobile`.
4
+ * @module @khorsheed/dsh-mobile/invariant
5
+ */
6
+ const PACKAGE_NAME = "@khorsheed/dsh-mobile";
7
+ /** Cordis companion plugin name. */
8
+ const name = "mobile-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /** Mobile owns no session data relations; the host owns authentication and streams. */
12
+ const install = () => {};
13
+ /**
14
+ * Register this package's invariant companion.
15
+ * @param ctx - Cordis context carrying the invariant service.
16
+ * @returns the installed registration's disposer after setup succeeds.
17
+ */
18
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
19
+ //#endregion
20
+ export { apply, inject, name };
@@ -0,0 +1,9 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ /** Encode entirely in this browser; the login credential never visits a QR service. */
3
+ export declare function LoginQR({ value, label }: {
4
+ value: string;
5
+ label: string;
6
+ }): import("react").JSX.Element;
7
+ /** Settings owns this page's lifetime; no credential is persisted or put in the URL. */
8
+ export declare function ConnectPhone({ t }: PropsLocale<'mobile'>): import("react").JSX.Element;
9
+ //# sourceMappingURL=ConnectPhone.d.ts.map
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import qrcode from 'qrcode-generator';
4
+ import { CONNECT_PATH } from '../protocol.js';
5
+ /** Encode entirely in this browser; the login credential never visits a QR service. */
6
+ export function LoginQR({ value, label }) {
7
+ const { size, path } = useMemo(() => {
8
+ const code = qrcode(0, 'M');
9
+ code.addData(value);
10
+ code.make();
11
+ const n = code.getModuleCount(), cells = [];
12
+ for (let y = 0; y < n; y++)
13
+ for (let x = 0; x < n; x++)
14
+ if (code.isDark(y, x))
15
+ cells.push(`M${x + 4} ${y + 4}h1v1h-1z`);
16
+ return { size: n + 8, path: cells.join('') };
17
+ }, [value]);
18
+ return _jsxs("svg", { role: "img", "aria-label": label, viewBox: `0 0 ${size} ${size}`, width: "256", height: "256", shapeRendering: "crispEdges", children: [_jsx("rect", { width: size, height: size, fill: "white" }), _jsx("path", { d: path, fill: "black" })] });
19
+ }
20
+ /** Settings owns this page's lifetime; no credential is persisted or put in the URL. */
21
+ export function ConnectPhone({ t }) {
22
+ const [info, setInfo] = useState(), [login, setLogin] = useState();
23
+ const [busy, setBusy] = useState(false), [failed, setFailed] = useState(false);
24
+ const request = useRef(), timer = useRef();
25
+ const hide = () => { request.current?.abort(); clearTimeout(timer.current); setLogin(undefined); setBusy(false); };
26
+ const load = async (generate = false) => {
27
+ hide();
28
+ setFailed(false);
29
+ setBusy(true);
30
+ const controller = new AbortController();
31
+ request.current = controller;
32
+ try {
33
+ const response = await fetch(CONNECT_PATH, { method: generate ? 'POST' : 'GET', credentials: 'same-origin', cache: 'no-store', signal: controller.signal,
34
+ ...(generate ? { headers: { 'content-type': 'application/json' }, body: '{}' } : {}) });
35
+ if (!response.ok)
36
+ throw new Error('unavailable');
37
+ const result = await response.json();
38
+ if (controller.signal.aborted)
39
+ return;
40
+ if (generate) {
41
+ const url = new URL(result.loginUrl);
42
+ if (!info?.origin || url.origin !== info.origin || url.protocol !== 'https:' || !url.searchParams.get('token'))
43
+ throw new Error('invalid');
44
+ setLogin(url.href);
45
+ timer.current = setTimeout(hide, 120_000);
46
+ }
47
+ else
48
+ setInfo(result);
49
+ }
50
+ catch {
51
+ if (!controller.signal.aborted)
52
+ setFailed(true);
53
+ }
54
+ finally {
55
+ if (!controller.signal.aborted)
56
+ setBusy(false);
57
+ }
58
+ };
59
+ useEffect(() => {
60
+ void load();
61
+ const conceal = () => { if (document.visibilityState === 'hidden')
62
+ hide(); };
63
+ document.addEventListener('visibilitychange', conceal);
64
+ window.addEventListener('pagehide', hide);
65
+ return () => { request.current?.abort(); clearTimeout(timer.current); document.removeEventListener('visibilitychange', conceal); window.removeEventListener('pagehide', hide); };
66
+ }, []);
67
+ return _jsxs("section", { "data-mobile-connect": true, children: [_jsx("style", { children: styles }), _jsx("h2", { children: t('connectPhone') }), _jsx("p", { children: t('connectIntro') }), info?.origin && _jsxs("div", { "data-connect-host": true, children: [_jsx("span", { children: t('server') }), _jsx("strong", { children: new URL(info.origin).host })] }), info && info.state !== 'ready' && _jsx("div", { role: "status", children: _jsx("p", { children: t(info.state === 'untrusted-origin' ? 'connectUntrusted' : info.state === 'unsupported' ? 'connectUnsupported' : 'connectSetup') }) }), failed && _jsx("p", { role: "alert", children: t('connectError') }), busy && _jsx("p", { role: "status", children: t('connectLoading') }), login ? _jsxs("div", { "data-connect-code": true, children: [_jsx(LoginQR, { value: login, label: t('connectQR') }), _jsx("p", { children: t('connectConceal') }), _jsx("button", { type: "button", onClick: hide, children: t('connectHide') })] })
68
+ : _jsx("button", { type: "button", disabled: busy, onClick: () => { void load(info?.state === 'ready'); }, children: t(info?.state === 'ready' ? 'connectGenerate' : 'directoryRetry') }), _jsxs("ol", { children: [_jsx("li", { children: t('connectStepOpen') }), _jsx("li", { children: t('connectStepScan') }), _jsx("li", { children: t('connectStepConfirm') })] }), _jsx("p", { "data-connect-note": true, children: t('connectPrivacy') }), _jsx("p", { "data-connect-note": true, children: t('connectNetwork') })] });
69
+ }
70
+ const styles = `
71
+ [data-mobile-connect]{max-width:640px;color:var(--dsw-alias-label-primary);font-size:15px;line-height:1.6}
72
+ [data-mobile-connect] h2{font-size:20px;margin:0 0 12px}
73
+ [data-mobile-connect] p{margin:12px 0}
74
+ [data-connect-host]{display:flex;gap:16px;justify-content:space-between;flex-wrap:wrap;padding:16px 0;margin:16px 0;border-block:1px solid var(--dsw-alias-border-l2)}
75
+ [data-connect-host] strong{overflow-wrap:anywhere;font-weight:500}
76
+ [data-mobile-connect] button{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-fill-secondary,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 18px;min-height:44px;cursor:pointer}
77
+ [data-mobile-connect] button:disabled{opacity:.5;cursor:default}
78
+ [data-mobile-connect] button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#527cff);outline-offset:3px}
79
+ [data-connect-code]{display:flex;align-items:center;flex-direction:column;gap:8px;padding:24px 0}
80
+ [data-connect-code] svg{max-width:100%;height:auto;border:12px solid white;border-radius:12px;box-sizing:content-box}
81
+ [data-mobile-connect] ol{padding-left:24px;margin:24px 0}[data-mobile-connect] li{padding:5px 0}
82
+ [data-connect-note]{font-size:13px;color:var(--dsw-alias-label-secondary)}
83
+ `;
@@ -0,0 +1,14 @@
1
+ import type { DirectoryFlowOwnerProps } from '@deepseek-ai/dsh-client-ui-workspace/client';
2
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { MobileDirectory } from './directory.ts';
4
+ import type { MobileNavigation } from './navigation.ts';
5
+ export interface DirectoryInjected {
6
+ directory: MobileDirectory;
7
+ navigation: MobileNavigation;
8
+ }
9
+ /** Both workspace adoption and a pure member path pick use the same read-only browser. */
10
+ export declare function DirectoryFlow({ open, busy, onPicked, onCancel, directory, navigation, preferSaved, t }: DirectoryFlowOwnerProps & DirectoryInjected & PropsLocale<'mobile'> & {
11
+ preferSaved?: boolean;
12
+ }): import("react").ReactPortal;
13
+ export declare function MobileDirectoryOverlay({ directory, navigation, t }: DirectoryInjected & PropsLocale<'mobile'>): import("react").JSX.Element;
14
+ //# sourceMappingURL=DirectoryFlow.d.ts.map
@@ -0,0 +1,75 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { MobileIcon } from './MobileIcon.js';
5
+ const emptySubscribe = () => () => { };
6
+ const emptyWorkspaces = [];
7
+ /** Both workspace adoption and a pure member path pick use the same read-only browser. */
8
+ export function DirectoryFlow({ open, busy, onPicked, onCancel, directory, navigation, preferSaved = false, t }) {
9
+ const dialog = useRef(null);
10
+ const controller = useRef();
11
+ const [listing, setListing] = useState();
12
+ const [path, setPath] = useState(''), [loading, setLoading] = useState(false), [failed, setFailed] = useState(false);
13
+ const [tab, setTab] = useState('saved'), [hidden, setHidden] = useState(false);
14
+ const [query, setQuery] = useState('');
15
+ const available = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot);
16
+ const feed = available?.workspaces.list;
17
+ const saved = useSyncExternalStore(feed ? listener => feed.subscribe(listener) : emptySubscribe, () => feed?.getSnapshot().items ?? emptyWorkspaces);
18
+ const browse = async (target) => {
19
+ controller.current?.abort();
20
+ const request = new AbortController();
21
+ controller.current = request;
22
+ setLoading(true);
23
+ setFailed(false);
24
+ setListing(undefined);
25
+ setPath(target ?? '');
26
+ setTab('browse');
27
+ setQuery('');
28
+ try {
29
+ const next = await directory.list(target, request.signal);
30
+ if (request.signal.aborted)
31
+ return;
32
+ setListing(next);
33
+ setPath(next.path);
34
+ }
35
+ catch {
36
+ if (!request.signal.aborted)
37
+ setFailed(true);
38
+ }
39
+ finally {
40
+ if (!request.signal.aborted)
41
+ setLoading(false);
42
+ }
43
+ };
44
+ useEffect(() => {
45
+ if (open) {
46
+ dialog.current?.showModal();
47
+ if (!preferSaved)
48
+ void browse();
49
+ }
50
+ else {
51
+ dialog.current?.close();
52
+ controller.current?.abort();
53
+ setListing(undefined);
54
+ setPath('');
55
+ setTab('saved');
56
+ setFailed(false);
57
+ setLoading(false);
58
+ }
59
+ return () => { controller.current?.abort(); };
60
+ }, [open]);
61
+ const choose = () => { if (!busy && !loading && listing && path === listing.path)
62
+ onPicked(listing.path); };
63
+ return createPortal(_jsxs("dialog", { ref: dialog, "data-mobile-dialog": true, "data-mobile-directory": true, "aria-label": t('directoryTitle'), onKeyDown: event => { if (event.key === 'Escape')
64
+ event.stopPropagation(); }, onCancel: event => { event.preventDefault(); if (!busy)
65
+ onCancel(); }, children: [_jsxs("header", { children: [_jsx("h2", { children: t('directoryTitle') }), _jsx("button", { type: "button", "aria-label": t('cancel'), disabled: busy, onClick: onCancel, children: _jsx(MobileIcon, { name: "close" }) })] }), _jsx("p", { children: t('directoryHint') }), _jsxs("div", { "data-directory-tabs": true, children: [_jsx("button", { type: "button", "aria-pressed": tab === 'saved', onClick: () => setTab('saved'), children: t('workspaces') }), _jsx("button", { type: "button", "aria-pressed": tab === 'browse', onClick: () => { if (listing)
66
+ setTab('browse');
67
+ else
68
+ void browse(); }, children: t('directoryComputer') })] }), _jsx("div", { "data-directory-scroll": true, children: tab === 'saved' ? _jsx("div", { children: saved.length ? saved.map(item => _jsxs("button", { "data-directory-row": true, type: "button", onClick: () => { setPath(item.path); void browse(item.path); }, children: [_jsx(MobileIcon, { name: "folder" }), _jsxs("span", { children: [_jsx("strong", { children: item.title || item.path.split(/[\\/]/).filter(Boolean).at(-1) || item.path }), _jsx("small", { children: item.path })] }), _jsx(MobileIcon, { name: "right" })] }, item.path)) : _jsx("p", { children: t('directoryNoSaved') }) }) : _jsxs(_Fragment, { children: [_jsxs("div", { "data-directory-current": true, children: [_jsx(MobileIcon, { name: "folder" }), _jsx("span", { children: listing?.path || path || t('directoryLoading') })] }), _jsxs("details", { "data-directory-manual": true, children: [_jsx("summary", { children: t('directoryManual') }), _jsx("form", { onSubmit: event => { event.preventDefault(); event.currentTarget.querySelector('input')?.blur(); if (path.trim())
69
+ void browse(path.trim()); }, children: _jsxs("label", { children: [t('hostPath'), _jsxs("span", { "data-directory-path": true, children: [_jsx("input", { "aria-label": t('hostPath'), value: path, onChange: event => setPath(event.target.value), autoCapitalize: "none", autoCorrect: "off", spellCheck: false, placeholder: "/path/to/project" }), _jsx("button", { type: "submit", disabled: !path.trim(), children: t('directoryGo') })] })] }) })] }), _jsxs("div", { "data-directory-location": true, children: [_jsxs("button", { type: "button", disabled: loading || !listing?.parent, onClick: () => { if (listing?.parent)
70
+ void browse(listing.parent); }, children: [_jsx(MobileIcon, { name: "back" }), t('directoryParent')] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: hidden, onChange: event => setHidden(event.target.checked) }), t('directoryHidden')] })] }), loading && _jsx("p", { role: "status", children: t('directoryLoading') }), failed && _jsxs("p", { role: "alert", children: [t('directoryError'), " ", _jsx("button", { type: "button", onClick: () => { void browse(path || undefined); }, children: t('directoryRetry') })] }), listing && _jsxs(_Fragment, { children: [_jsx("input", { type: "search", "aria-label": t('directorySearch'), placeholder: t('directorySearch'), value: query, onChange: event => setQuery(event.target.value) }), listing.entries.filter(entry => (hidden || !entry.hidden) && entry.name.toLocaleLowerCase().includes(query.toLocaleLowerCase())).map(entry => _jsxs("button", { "data-directory-row": true, type: "button", onClick: () => { void browse(entry.path); }, children: [_jsx(MobileIcon, { name: "folder" }), _jsx("span", { children: entry.name }), _jsx(MobileIcon, { name: "right" })] }, entry.path)), !listing.entries.some(entry => hidden || !entry.hidden) && _jsx("p", { children: t('directoryEmpty') }), listing.truncated && _jsx("p", { children: t('directoryTruncated') })] })] }) }), _jsx("footer", { children: _jsx("button", { type: "button", "data-mobile-done": true, disabled: busy || loading || !listing || path !== listing.path || tab !== 'browse', onClick: choose, children: t('directorySelect') }) })] }), document.body);
71
+ }
72
+ export function MobileDirectoryOverlay({ directory, navigation, t }) {
73
+ const open = useSyncExternalStore(directory.subscribe, directory.getSnapshot);
74
+ return _jsx(DirectoryFlow, { preferSaved: true, open: open, busy: false, onPicked: directory.finish, onCancel: () => directory.finish(null), onError: () => { }, directory: directory, navigation: navigation, t: t });
75
+ }
@@ -0,0 +1,15 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client';
3
+ import type { MobileRooms } from './rooms.ts';
4
+ import type { MobileNavigation } from './navigation.ts';
5
+ import type { MobilePresentation } from './presentation.ts';
6
+ export interface MobileChromeInjected {
7
+ rooms?: MobileRooms;
8
+ navigation: MobileNavigation;
9
+ presentation: MobilePresentation;
10
+ toggleSidebar: () => void;
11
+ connection: ConnectionHandle;
12
+ }
13
+ /** One visible navigation bar, with native connection sheets when supported. */
14
+ export declare function MobileChrome({ presentation, toggleSidebar, connection, navigation, rooms, t }: MobileChromeInjected & PropsLocale<'mobile'>): import("react").JSX.Element | null;
15
+ //# sourceMappingURL=MobileChrome.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { installNavigationGestures } from './gestures.js';
4
+ import { MobileRoomNavigation } from './MobileRooms.js';
5
+ import { SessionRename } from './SessionRename.js';
6
+ import { MobileIcon } from './MobileIcon.js';
7
+ import { MobileLibrary } from './MobileLibrary.js';
8
+ import { mainSessionId } from './navigation.js';
9
+ import { hasNativeAction, reportChrome, requestNativeAction } from './native.js';
10
+ const emptySubscribe = () => () => { };
11
+ const emptyPreset = () => '';
12
+ function ConversationTitle({ navigation, fallback, prepareNavigation, surface, t }) {
13
+ const feed = navigation.sessions.list;
14
+ const sessions = useSyncExternalStore(useCallback(listener => feed.subscribe(listener), [feed]), useCallback(() => feed.getSnapshot(), [feed]));
15
+ const current = mainSessionId(sessions);
16
+ useLayoutEffect(() => prepareNavigation(), [current, prepareNavigation]);
17
+ const row = current ? sessions.byId[current] : undefined;
18
+ const preset = useSyncExternalStore(surface?.subscribe ?? emptySubscribe, surface?.getSnapshot ?? emptyPreset);
19
+ const [renaming, setRenaming] = useState(false);
20
+ useLayoutEffect(() => setRenaming(false), [current]);
21
+ const cwd = row?.cwd?.split(/[\\/]/).filter(Boolean).at(-1);
22
+ return _jsxs(_Fragment, { children: [_jsxs("span", { "data-mobile-title-line": true, children: [_jsx("strong", { "data-mobile-session-title": row && !row.blank ? '' : undefined, children: row && !row.blank ? row.title || row.displayTitle : fallback }), row && !row.blank && _jsx("button", { "data-mobile-rename": true, "aria-label": t('renameSession'), onClick: () => { prepareNavigation(); setRenaming(true); }, children: _jsx(MobileIcon, { name: "edit", size: 16 }) })] }), row && !row.blank && (cwd || preset) && _jsxs("span", { "data-mobile-subtitle": true, children: [_jsx("span", { title: row.cwd, children: cwd }), cwd && preset && _jsx("span", { "aria-hidden": true, children: "\u00B7" }), preset && _jsx("span", { children: preset })] }), renaming && row && !row.blank && _jsx(SessionRename, { navigation: navigation, sessionId: row.id, title: row.title || row.displayTitle, close: () => { prepareNavigation(); setRenaming(false); }, t: t }, row.id)] });
23
+ }
24
+ /** One visible navigation bar, with native connection sheets when supported. */
25
+ export function MobileChrome({ presentation, toggleSidebar, connection, navigation, rooms, t }) {
26
+ const state = useSyncExternalStore(presentation.subscribe, presentation.getSnapshot);
27
+ const wire = useSyncExternalStore(connection.state.subscribe, connection.state.getSnapshot);
28
+ const available = useSyncExternalStore(navigation.subscribe, navigation.getSnapshot);
29
+ const [library, setLibrary] = useState(false), [settings, setSettings] = useState(false);
30
+ const [wide, setWide] = useState(window.innerWidth >= 960);
31
+ useEffect(() => { const resize = () => setWide(window.innerWidth >= 960); window.addEventListener('resize', resize); return () => window.removeEventListener('resize', resize); }, []);
32
+ const [connectionExpanded, setConnectionExpanded] = useState(false);
33
+ useEffect(() => {
34
+ setConnectionExpanded(false);
35
+ if (wire === 'connected')
36
+ return;
37
+ const timer = window.setTimeout(() => setConnectionExpanded(true), 4000);
38
+ return () => window.clearTimeout(timer);
39
+ }, [wire]);
40
+ useEffect(() => {
41
+ if (state.active && wire === 'connected' && available?.sessions.list.getSnapshot().phase === 'pending')
42
+ void available.sessions.refresh().catch(() => { });
43
+ }, [state.active, wire, available]);
44
+ const activated = useRef(false), dialog = useRef(null);
45
+ useEffect(() => {
46
+ if (!state.active) {
47
+ activated.current = false;
48
+ setLibrary(false);
49
+ return;
50
+ }
51
+ if (available && !activated.current) {
52
+ activated.current = true;
53
+ setLibrary(true);
54
+ if (state.drawer)
55
+ toggleSidebar();
56
+ }
57
+ }, [state.active, state.drawer, available, toggleSidebar]);
58
+ useEffect(() => { reportChrome(state.active); return () => reportChrome(false); }, [state.active]);
59
+ useEffect(() => {
60
+ if (!state.active || !library || !available || wide)
61
+ return;
62
+ const main = document.querySelector('[data-mobile-frame] > div:has(> [data-slot="main"])');
63
+ if (!main)
64
+ return;
65
+ const previous = main.inert;
66
+ main.inert = true;
67
+ return () => { main.inert = previous; };
68
+ }, [state.active, library, available, wide]);
69
+ useEffect(() => {
70
+ document.documentElement.toggleAttribute('data-mobile-library-open', state.active && library && !!available);
71
+ return () => document.documentElement.removeAttribute('data-mobile-library-open');
72
+ }, [state.active, library, available]);
73
+ useEffect(() => {
74
+ if (!state.active || settings)
75
+ return;
76
+ return installNavigationGestures(document, open => { if (available)
77
+ setLibrary(open);
78
+ else if (open !== state.drawer)
79
+ toggleSidebar(); }, toggleSidebar);
80
+ }, [state.active, available, settings, state.drawer, toggleSidebar]);
81
+ const openLibrary = () => {
82
+ if (!available) {
83
+ toggleSidebar();
84
+ return;
85
+ }
86
+ if (state.drawer)
87
+ toggleSidebar();
88
+ setLibrary(true);
89
+ };
90
+ const startSession = () => {
91
+ presentation.prepareNavigation();
92
+ available?.workspace.startSession();
93
+ if (state.drawer)
94
+ toggleSidebar();
95
+ setSettings(false);
96
+ setLibrary(false);
97
+ };
98
+ const openSettings = () => {
99
+ if (hasNativeAction('settings'))
100
+ requestNativeAction('settings');
101
+ else
102
+ setSettings(true);
103
+ };
104
+ useEffect(() => { if (settings)
105
+ dialog.current?.showModal();
106
+ else
107
+ dialog.current?.close(); }, [settings]);
108
+ useEffect(() => {
109
+ const escape = (e) => {
110
+ if (e.key !== 'Escape' || settings)
111
+ return;
112
+ if (library)
113
+ setLibrary(false);
114
+ else if (state.drawer)
115
+ toggleSidebar();
116
+ };
117
+ window.addEventListener('keydown', escape);
118
+ return () => window.removeEventListener('keydown', escape);
119
+ }, [state.drawer, settings, library, toggleSidebar]);
120
+ if (!state.active && !settings && state.mode !== 'desktop')
121
+ return null;
122
+ return _jsxs(_Fragment, { children: [!state.active && state.mode === 'desktop' && _jsx("button", { "data-mobile-restore": true, onClick: () => presentation.setMode('mobile'), children: t('restoreMobile') }), state.active && _jsxs(_Fragment, { children: [available && _jsxs("aside", { "data-mobile-library-pane": true, hidden: !library && !wide, children: [_jsxs("nav", { "data-mobile-library-toolbar": true, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('settings'), onClick: openSettings, children: _jsx(MobileIcon, { name: "settings" }) }), _jsx("button", { "aria-label": t('newSession'), onClick: startSession, children: _jsx(MobileIcon, { name: "compose" }) })] }), _jsx(MobileLibrary, { ...(rooms ? { rooms } : {}), navigation: available, t: t, onBeforeOpen: presentation.prepareNavigation, onOpen: () => setLibrary(false) })] }), library && available && !wide && _jsx("button", { type: "button", "data-mobile-peek-close": true, "aria-label": t('returnToConversation'), onClick: () => { presentation.prepareNavigation(); setLibrary(false); } }), state.drawer && _jsx("button", { "data-mobile-shade": true, "aria-label": t('close'), onClick: toggleSidebar }), _jsxs("nav", { "data-mobile-toolbar": true, "data-library": false, "aria-label": t('menu'), children: [_jsx("button", { "aria-label": t('menu'), onClick: openLibrary, children: _jsx(MobileIcon, { name: "back" }) }), _jsxs("div", { "data-mobile-nav-title": true, children: [(available ? _jsx(ConversationTitle, { navigation: available, prepareNavigation: presentation.prepareNavigation, surface: presentation.surface, fallback: t('newSession'), t: t }) : _jsx("strong", { children: t('brand') })), wire !== 'connected' && !connectionExpanded && _jsx("small", { role: "status", children: t(wire ?? 'connecting') })] }), _jsx("button", { "data-mobile-new": !!available, "aria-label": available ? t('newSession') : t('settings'), onClick: available ? startSession : openSettings, children: _jsx(MobileIcon, { name: available ? "compose" : "settings" }) }), available && rooms && _jsx(MobileRoomNavigation, { rooms: rooms, navigation: available, prepareNavigation: presentation.prepareNavigation, t: t })] }), wire !== 'connected' && connectionExpanded && _jsxs("div", { "data-mobile-connection-status": true, role: "status", children: [_jsx("span", { children: t(wire ?? 'connecting') }), _jsx("button", { onClick: () => connection.reconnect(), children: t('reconnect') })] })] }), _jsxs("dialog", { ref: dialog, "data-mobile-dialog": true, onClose: () => setSettings(false), "aria-label": t('settings'), children: [_jsx("h2", { children: t('settings') }), available && _jsx("button", { "data-mobile-option": true, onClick: startSession, children: t('newSession') }), _jsx("button", { "data-mobile-option": true, onClick: () => { setSettings(false); setLibrary(false); if (!state.drawer)
123
+ toggleSidebar(); }, children: t('workspaces') }), hasNativeAction('settings') ? _jsx("button", { "data-mobile-option": true, onClick: () => { setSettings(false); requestNativeAction('settings'); }, children: t('connectionSettings') }) : _jsxs(_Fragment, { children: [_jsx("h3", { children: t('connection') }), _jsx("p", { children: window.location.host }), _jsx("p", { children: t(wire ?? 'connecting') }), _jsx("button", { onClick: () => connection.reconnect(), children: t('reconnect') })] }), _jsx("h3", { children: t('display') }), _jsx("div", { role: "group", "aria-label": t('display'), children: ['auto', 'mobile', 'desktop'].map((mode) => _jsx("button", { "aria-pressed": state.mode === mode, onClick: () => presentation.setMode(mode), children: t(mode === 'auto' ? 'automatic' : mode) }, mode)) }), _jsx("p", { children: t('appearance') }), _jsx("button", { "data-mobile-done": true, onClick: () => setSettings(false), children: t('done') })] })] });
124
+ }
@@ -0,0 +1,6 @@
1
+ export type MobileIconName = 'edit' | 'members' | 'back' | 'settings' | 'compose' | 'folder' | 'search' | 'scan' | 'close' | 'down' | 'right' | 'plus' | 'attachment' | 'commands' | 'shield';
2
+ export declare function MobileIcon({ name, size }: {
3
+ name: MobileIconName;
4
+ size?: number;
5
+ }): import("react").JSX.Element;
6
+ //# sourceMappingURL=MobileIcon.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const paths = {
3
+ edit: 'm15 5 4 4M5 19l4-1L20 7a2 2 0 0 0-4-4L5 14l-1 6Z',
4
+ members: 'M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M22 21v-2a4 4 0 0 0-3-3.87M9 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8m8 .13a4 4 0 0 1 0 7.75',
5
+ plus: 'M12 5v14M5 12h14', commands: 'm16 4-8 16', shield: 'm12 3 8 3v6c0 5-8 9-8 9s-8-4-8-9V6l8-3Zm-4 9 3 3 5-6',
6
+ attachment: 'm9 12 6-6a3 3 0 0 1 4 4l-8 8a5 5 0 0 1-7-7l8-8M7 14l7-7a1 1 0 0 1 2 2l-7 7',
7
+ back: 'm15 18-6-6 6-6', settings: 'M4 7h6m4 0h6M4 17h10m4 0h2M10 4v6m4 4v6',
8
+ compose: 'M12 4H5a1 1 0 0 0-1 1v14a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7M10 14l1-4 8-8 3 3-8 8-4 1Z',
9
+ folder: 'M3 7a1 1 0 0 1 1-1h6l2 3h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7Z',
10
+ search: 'm16 16 5 5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0',
11
+ scan: 'M8 4H5a1 1 0 0 0-1 1v3m12-4h3a1 1 0 0 1 1 1v3M4 16v3a1 1 0 0 0 1 1h3m8 0h3a1 1 0 0 0 1-1v-3M5 12h14',
12
+ close: 'm6 6 12 12M6 18 18 6', down: 'm6 9 6 6 6-6', right: 'm9 6 6 6-6 6',
13
+ };
14
+ export function MobileIcon({ name, size = 22 }) {
15
+ return _jsx("svg", { "data-mobile-icon": name, width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: paths[name] }) });
16
+ }
@@ -0,0 +1,13 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { NavigationCapabilities } from './navigation.ts';
3
+ import type { MobileRooms } from './rooms.ts';
4
+ type Props = PropsLocale<'mobile'> & {
5
+ rooms?: MobileRooms;
6
+ navigation: NavigationCapabilities;
7
+ onOpen: () => void;
8
+ onBeforeOpen?: () => void;
9
+ };
10
+ /** Browse official metadata; the official workspace service remains the only navigation writer. */
11
+ export declare function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }: Props): import("react").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=MobileLibrary.d.ts.map