@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
@@ -0,0 +1,41 @@
1
+ /** rc1 has no composer autofocus option. Guard navigation focus through public
2
+ * DOM events; never replace Lexical, its focus method, selection, or draft. */
3
+ export class ComposerFocus {
4
+ doc;
5
+ active;
6
+ waiting = true;
7
+ constructor(doc, active) {
8
+ this.doc = doc;
9
+ this.active = active;
10
+ doc.addEventListener('focusin', this.focus, true);
11
+ doc.addEventListener('pointerdown', this.pointer, true);
12
+ doc.addEventListener('keydown', this.key, true);
13
+ }
14
+ arm = () => {
15
+ this.waiting = true;
16
+ const el = this.doc.activeElement;
17
+ if (this.active() && el instanceof HTMLElement && this.isEditor(el))
18
+ el.blur();
19
+ };
20
+ isEditor(el) {
21
+ return !!el.closest('[data-mobile-frame] [data-composer-card] [contenteditable="true"]');
22
+ }
23
+ focus = (event) => {
24
+ if (this.active() && this.waiting && event.target instanceof HTMLElement && this.isEditor(event.target))
25
+ event.target.blur();
26
+ };
27
+ pointer = (event) => {
28
+ // Explicit composer taps and message actions (e.g. edit) may focus input.
29
+ if (event.target instanceof Element && event.target.closest('[data-mobile-frame] :is([data-composer-card], [data-conversation-scroll])'))
30
+ this.waiting = false;
31
+ };
32
+ key = (event) => {
33
+ if (event.key === 'Tab')
34
+ this.waiting = false;
35
+ };
36
+ dispose() {
37
+ this.doc.removeEventListener('focusin', this.focus, true);
38
+ this.doc.removeEventListener('pointerdown', this.pointer, true);
39
+ this.doc.removeEventListener('keydown', this.key, true);
40
+ }
41
+ }
@@ -0,0 +1,17 @@
1
+ import { type MobileDirectoryListing } from '../protocol.ts';
2
+ /** One pending path choice shared by every mobile caller of the Host picker. */
3
+ export declare class MobileDirectory {
4
+ private pending;
5
+ private listeners;
6
+ readonly getSnapshot: () => boolean;
7
+ readonly subscribe: (listener: () => void) => () => void;
8
+ readonly pick: () => Promise<string | null>;
9
+ readonly finish: (path: string | null) => void;
10
+ readonly list: (path: string | undefined, signal: AbortSignal) => Promise<MobileDirectoryListing>;
11
+ }
12
+ /** Checked public-method fallback until Host offers a pure path-selection UI slot.
13
+ * Scoped to this browser; preserve desktop dispatch and restore only our own wrapper. */
14
+ export declare function installMobileDirectoryPicker(owner: {
15
+ pickDirectory(): Promise<string | null>;
16
+ }, directory: MobileDirectory, active: () => boolean): () => void;
17
+ //# sourceMappingURL=directory.d.ts.map
@@ -0,0 +1,54 @@
1
+ import { DIRECTORY_PATH } from '../protocol.js';
2
+ /** One pending path choice shared by every mobile caller of the Host picker. */
3
+ export class MobileDirectory {
4
+ pending;
5
+ listeners = new Set();
6
+ getSnapshot = () => !!this.pending;
7
+ subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
8
+ pick = () => {
9
+ if (!this.pending) {
10
+ let resolve;
11
+ const promise = new Promise(done => { resolve = done; });
12
+ this.pending = { promise, resolve };
13
+ for (const listener of this.listeners)
14
+ listener();
15
+ }
16
+ return this.pending.promise;
17
+ };
18
+ finish = (path) => {
19
+ const pending = this.pending;
20
+ this.pending = undefined;
21
+ pending?.resolve(path);
22
+ for (const listener of this.listeners)
23
+ listener();
24
+ };
25
+ list = async (path, signal) => {
26
+ const response = await fetch(`${DIRECTORY_PATH}${path === undefined ? '' : `?path=${encodeURIComponent(path)}`}`, { signal, credentials: 'same-origin', cache: 'no-store' });
27
+ if (!response.ok)
28
+ throw new Error('directory-unavailable');
29
+ return response.json();
30
+ };
31
+ }
32
+ /** Checked public-method fallback until Host offers a pure path-selection UI slot.
33
+ * Scoped to this browser; preserve desktop dispatch and restore only our own wrapper. */
34
+ export function installMobileDirectoryPicker(owner, directory, active) {
35
+ const descriptor = Object.getOwnPropertyDescriptor(owner, 'pickDirectory');
36
+ const original = owner.pickDirectory;
37
+ const wrapped = () => active() ? directory.pick() : original.call(owner);
38
+ // A future read-only facade must not prevent the rest of mobile from loading.
39
+ try {
40
+ owner.pickDirectory = wrapped;
41
+ }
42
+ catch {
43
+ return () => { directory.finish(null); };
44
+ }
45
+ return () => {
46
+ directory.finish(null);
47
+ if (owner.pickDirectory !== wrapped)
48
+ return;
49
+ if (descriptor)
50
+ Object.defineProperty(owner, 'pickDirectory', descriptor);
51
+ else
52
+ Reflect.deleteProperty(owner, 'pickDirectory');
53
+ };
54
+ }
@@ -0,0 +1,4 @@
1
+ /** Resume a stale official follow generation, preserving live reply baselines.
2
+ * Brief interruptions and duplicate native/browser signals leave healthy streams alone. */
3
+ export declare function installForegroundRecovery(win: Window, reconnect: () => void, getState?: () => string | undefined): () => void;
4
+ //# sourceMappingURL=foreground.d.ts.map
@@ -0,0 +1,47 @@
1
+ /** Resume a stale official follow generation, preserving live reply baselines.
2
+ * Brief interruptions and duplicate native/browser signals leave healthy streams alone. */
3
+ export function installForegroundRecovery(win, reconnect, getState = () => undefined) {
4
+ let hiddenAt = win.document.visibilityState === 'hidden' ? Date.now() : undefined;
5
+ let lastSignalAt = Date.now(), lastReconnectAt = -Infinity;
6
+ let timer, needsRecovery = false;
7
+ const resume = (awayMs) => {
8
+ if (win.document.visibilityState === 'hidden' || win.navigator.onLine === false)
9
+ return;
10
+ const state = getState();
11
+ needsRecovery ||= awayMs >= 5000 || (state !== 'connected' && state !== 'connecting');
12
+ lastSignalAt = Date.now();
13
+ if (timer !== undefined)
14
+ win.clearTimeout(timer);
15
+ timer = win.setTimeout(() => {
16
+ timer = undefined;
17
+ if (needsRecovery && Date.now() - lastReconnectAt >= 1000) {
18
+ lastReconnectAt = Date.now();
19
+ reconnect();
20
+ }
21
+ needsRecovery = false;
22
+ }, 150);
23
+ };
24
+ const native = () => resume(Date.now() - (hiddenAt ?? lastSignalAt));
25
+ const visibility = () => {
26
+ if (win.document.visibilityState === 'hidden') {
27
+ hiddenAt = Date.now();
28
+ needsRecovery = false;
29
+ if (timer !== undefined)
30
+ win.clearTimeout(timer);
31
+ timer = undefined;
32
+ return;
33
+ }
34
+ if (hiddenAt !== undefined) {
35
+ const elapsed = Date.now() - hiddenAt;
36
+ hiddenAt = undefined;
37
+ resume(elapsed);
38
+ }
39
+ };
40
+ const page = (event) => { if (event.persisted)
41
+ resume(Infinity); };
42
+ win.document.addEventListener('visibilitychange', visibility);
43
+ win.addEventListener('pageshow', page);
44
+ win.addEventListener('dsh-mobile-foreground', native);
45
+ return () => { if (timer !== undefined)
46
+ win.clearTimeout(timer); win.document.removeEventListener('visibilitychange', visibility); win.removeEventListener('pageshow', page); win.removeEventListener('dsh-mobile-foreground', native); };
47
+ }
@@ -0,0 +1,4 @@
1
+ /** Horizontal navigation never claims text selection, editors or nested horizontal content. */
2
+ export declare function navigationSwipe(dx: number, dy: number, width: number): 'open' | 'close' | undefined;
3
+ export declare function installNavigationGestures(doc: Document, change: (open: boolean) => void, closeSidebar?: () => void): () => void;
4
+ //# sourceMappingURL=gestures.d.ts.map
@@ -0,0 +1,118 @@
1
+ /** Horizontal navigation never claims text selection, editors or nested horizontal content. */
2
+ export function navigationSwipe(dx, dy, width) {
3
+ if (Math.abs(dx) < Math.min(72, width * .18) || Math.abs(dx) < Math.abs(dy) * 1.6)
4
+ return;
5
+ return dx > 0 ? 'open' : 'close';
6
+ }
7
+ export function installNavigationGestures(doc, change, closeSidebar) {
8
+ let start, suppressClickUntil = 0, suppressClickTarget, animation;
9
+ const resetDrag = () => {
10
+ start?.panel?.element.style.removeProperty('transform');
11
+ doc.documentElement.removeAttribute('data-mobile-gesture');
12
+ doc.documentElement.style.removeProperty('--mobile-drag-offset');
13
+ };
14
+ const blocked = 'input,textarea,[contenteditable],select,pre,code,[role=dialog],[role=menu],[role=slider],[role=switch],[role=checkbox],[data-mobile-tools-dialog],[data-mobile-dialog]';
15
+ const down = (event) => {
16
+ resetDrag();
17
+ start = undefined;
18
+ suppressClickUntil = 0;
19
+ suppressClickTarget = undefined;
20
+ if (animation || event.touches.length !== 1 || doc.querySelector('dialog[open],[role=dialog],[role=menu]'))
21
+ return;
22
+ const target = event.target;
23
+ if (target.closest(blocked) || doc.getSelection()?.toString())
24
+ return;
25
+ // List rows are navigation surfaces: a tap opens, a horizontal drag dismisses.
26
+ // All other buttons keep their existing touch behavior.
27
+ if (target.closest('button,a') && !target.closest('[data-mobile-session],[data-mobile-workspace-group],[data-mobile-peek-close],[data-slot=sidebar]'))
28
+ return;
29
+ for (let node = target; node && node !== doc.body; node = node.parentElement) {
30
+ if (node.scrollWidth > node.clientWidth + 4 && /auto|scroll/.test(doc.defaultView.getComputedStyle(node).overflowX))
31
+ return;
32
+ }
33
+ const viewport = doc.documentElement.clientWidth || doc.defaultView.innerWidth;
34
+ if (viewport >= 960)
35
+ return; // A pinned two-column layout is not a drawer.
36
+ const right = target.closest('[data-sidebar-right-panel][data-sidebar-right-open]') ?? undefined;
37
+ if (right && !right.querySelector('[data-sidebar-right-toggle]'))
38
+ return;
39
+ const sidebar = closeSidebar ? target.closest('[data-slot=sidebar]')?.parentElement : undefined;
40
+ const panel = right ? { element: right, side: 'right', close: () => right.querySelector('[data-sidebar-right-toggle]')?.click() } : sidebar ? { element: sidebar, side: 'left', close: closeSidebar } : undefined;
41
+ const width = panel ? panel.element.getBoundingClientRect().width || viewport : Math.min(viewport * .84, 380);
42
+ start = { target: target.closest('button,a') ?? target, x: event.touches[0].clientX, y: event.touches[0].clientY, width, initial: doc.documentElement.hasAttribute('data-mobile-library-open') ? width : 0, ...(panel ? { panel } : {}), dragged: false };
43
+ };
44
+ const move = (event) => {
45
+ if (!start || event.touches.length !== 1) {
46
+ resetDrag();
47
+ start = undefined;
48
+ return;
49
+ }
50
+ const dx = event.touches[0].clientX - start.x, dy = event.touches[0].clientY - start.y;
51
+ if (!start.dragged && Math.abs(dy) > 12 && Math.abs(dy) > Math.abs(dx)) {
52
+ resetDrag();
53
+ start = undefined;
54
+ return;
55
+ }
56
+ if (!start.dragged && (Math.abs(dx) <= 12 || Math.abs(dx) <= Math.abs(dy) * 1.6))
57
+ return;
58
+ if (start.panel) {
59
+ const direction = start.panel.side === 'right' ? 1 : -1;
60
+ if (!start.dragged && dx * direction < 0)
61
+ return;
62
+ start.panel.element.style.transform = `translate3d(${direction * Math.max(0, dx * direction)}px,0,0)`;
63
+ }
64
+ else {
65
+ if (!start.dragged && (start.initial === 0 && dx < 0 || start.initial > 0 && dx > 0))
66
+ return;
67
+ doc.documentElement.setAttribute('data-mobile-gesture', '');
68
+ doc.documentElement.style.setProperty('--mobile-drag-offset', `${Math.max(0, Math.min(start.width, start.initial + dx))}px`);
69
+ }
70
+ start.dragged = true;
71
+ if (event.cancelable)
72
+ event.preventDefault();
73
+ };
74
+ const up = (event) => {
75
+ const from = start, point = event.changedTouches[0];
76
+ if (!from || !point) {
77
+ resetDrag();
78
+ start = undefined;
79
+ return;
80
+ }
81
+ const dx = point.clientX - from.x, dy = point.clientY - from.y;
82
+ const result = from.dragged ? navigationSwipe(dx, dy, from.width) : undefined;
83
+ if (from.dragged) {
84
+ suppressClickUntil = Date.now() + 500;
85
+ suppressClickTarget = from.target;
86
+ }
87
+ if (from.panel && from.dragged) {
88
+ const owner = from.panel, panel = owner.element, direction = owner.side === 'right' ? 1 : -1;
89
+ const close = result === (owner.side === 'right' ? 'open' : 'close');
90
+ const finish = () => { animation = undefined; panel.style.removeProperty('transform'); if (close && panel.isConnected)
91
+ owner.close(); };
92
+ if (panel.animate && !doc.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches) {
93
+ animation = panel.animate([{ transform: `translateX(${direction * Math.max(0, dx * direction)}px)` }, { transform: `translateX(${close ? direction * from.width : 0}px)` }], { duration: 180, easing: 'cubic-bezier(.2,.8,.2,1)' });
94
+ animation.onfinish = finish;
95
+ }
96
+ else
97
+ finish();
98
+ }
99
+ resetDrag();
100
+ start = undefined;
101
+ if (!from.panel && result)
102
+ change(result === 'open');
103
+ };
104
+ const cancel = () => { resetDrag(); start = undefined; };
105
+ const click = (event) => {
106
+ if (event.detail !== 0 && Date.now() < suppressClickUntil && event.target instanceof Node && (suppressClickTarget?.contains(event.target) || event.target.contains(suppressClickTarget ?? null))) {
107
+ event.preventDefault();
108
+ event.stopImmediatePropagation();
109
+ suppressClickUntil = 0;
110
+ }
111
+ };
112
+ doc.addEventListener('touchstart', down, { passive: true });
113
+ doc.addEventListener('touchmove', move, { passive: false });
114
+ doc.addEventListener('touchend', up);
115
+ doc.addEventListener('touchcancel', cancel);
116
+ doc.addEventListener('click', click, true);
117
+ return () => { cancel(); animation?.cancel(); doc.removeEventListener('touchstart', down); doc.removeEventListener('touchmove', move); doc.removeEventListener('touchend', up); doc.removeEventListener('touchcancel', cancel); doc.removeEventListener('click', click, true); };
118
+ }
@@ -0,0 +1,20 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ declare global {
3
+ interface Window {
4
+ __DSH_MOBILE_SHELL__?: {
5
+ bridgeVersion: number;
6
+ capabilities?: readonly string[];
7
+ };
8
+ webkit?: {
9
+ messageHandlers?: {
10
+ dshMobile?: {
11
+ postMessage(value: unknown): void;
12
+ };
13
+ };
14
+ };
15
+ }
16
+ }
17
+ export declare const inject: string[];
18
+ /** One browser-owned presentation; no Host preference or sibling plugin is changed. */
19
+ export declare function apply(ctx: Context): void;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { ConnectPhone } from './ConnectPhone.js';
2
+ import { BRIDGE_VERSION, MOBILE_VERSION } from '../protocol.js';
3
+ import { MobilePresentation } from './presentation.js';
4
+ import { DirectoryFlow, MobileDirectoryOverlay } from './DirectoryFlow.js';
5
+ import { MobileDirectory, installMobileDirectoryPicker } from './directory.js';
6
+ import { MobileNavigation, mainSessionId } from './navigation.js';
7
+ import { MobileWelcome } from './MobileSeats.js';
8
+ import { MobileSubmissionFocus } from './SubmissionFocus.js';
9
+ import { MobileTools } from './MobileTools.js';
10
+ import { MobileChrome } from './MobileChrome.js';
11
+ import { MobileQueue } from './MobileQueue.js';
12
+ import { MobileRooms } from './rooms.js';
13
+ import { MessageMenu } from './messageMenu.js';
14
+ import { installForegroundRecovery } from './foreground.js';
15
+ import { NS, en, zh } from './locales.js';
16
+ export const inject = ['slots', 'locale', 'layout', 'connection'];
17
+ /** One browser-owned presentation; no Host preference or sibling plugin is changed. */
18
+ export function apply(ctx) {
19
+ ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'mobile: dictionaries');
20
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
21
+ name: 'settings.section', id: 'mobile-connect', order: 25, label: () => ctx.locale.bind(NS)('connectPhone'), locale: NS,
22
+ }, ConnectPhone));
23
+ const connection = ctx.get('connection');
24
+ const rooms = new MobileRooms(() => ctx.get('remote.room'));
25
+ ctx.effect(() => () => rooms.dispose(), 'mobile: room presentation');
26
+ const messages = new MessageMenu(document);
27
+ ctx.effect(() => () => messages.dispose(), 'mobile: message actions');
28
+ const navigation = new MobileNavigation();
29
+ const directory = new MobileDirectory();
30
+ const presentation = new MobilePresentation(window, window.__DSH_MOBILE_SHELL__?.bridgeVersion === BRIDGE_VERSION);
31
+ ctx.inject(['sessions', 'workspaces', 'uiWorkspace'], scoped => {
32
+ navigation.set({ sessions: scoped.sessions, workspaces: scoped.workspaces, workspace: scoped.uiWorkspace });
33
+ scoped.effect(() => installMobileDirectoryPicker(scoped.uiWorkspace, directory, () => presentation.getSnapshot().active), 'mobile: path picker');
34
+ scoped.effect(() => () => { navigation.set(undefined); }, 'mobile: optional navigation');
35
+ });
36
+ ctx.effect(() => { const off = presentation.subscribe(() => { if (!presentation.getSnapshot().active)
37
+ directory.finish(null); }); return () => { off(); directory.finish(null); }; }, 'mobile: pending directory');
38
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({ name: 'shell.overlay', id: 'mobile-directory', locale: NS, inject: () => ({ directory, navigation }) }, MobileDirectoryOverlay));
39
+ ctx.effect(() => () => { presentation.dispose(); }, 'mobile: presentation');
40
+ ctx.effect(() => {
41
+ let remove;
42
+ const sync = () => {
43
+ if (presentation.getSnapshot().active)
44
+ remove ??= installForegroundRecovery(window, () => connection.reconnect(), () => connection.state.getSnapshot());
45
+ else {
46
+ remove?.();
47
+ remove = undefined;
48
+ }
49
+ };
50
+ sync();
51
+ const unsubscribe = presentation.subscribe(sync);
52
+ return () => { unsubscribe(); remove?.(); };
53
+ }, 'mobile: foreground reconnect');
54
+ ctx.slots.inject('shell.overlay', () => ctx.slots.register({
55
+ name: 'shell.overlay', id: 'mobile-navigation', locale: NS,
56
+ inject: () => ({ presentation, connection, navigation, rooms, toggleSidebar: () => { ctx.layout.toggleSidebar(); } }),
57
+ }, MobileChrome));
58
+ for (const name of ['conversation.input.dock', 'conversation.session.header.actions']) {
59
+ ctx.slots.inject(name, () => ctx.slots.register({ name, id: 'mobile-send-focus' }, MobileSubmissionFocus));
60
+ }
61
+ // Room's first asynchronous cache fill does not bump its composer slot in
62
+ // this revision. A declining mobile entry refreshes the public election;
63
+ // the original Room/approval/local-agent entries still decide the winner.
64
+ ctx.slots.inject('conversation.composer', () => {
65
+ let remove;
66
+ let timer;
67
+ const refresh = () => {
68
+ const list = navigation.getSnapshot()?.sessions.list.getSnapshot();
69
+ const current = list === undefined ? undefined : mainSessionId(list);
70
+ if (!presentation.getSnapshot().active || !current || !rooms.get(current))
71
+ return;
72
+ clearTimeout(timer);
73
+ timer = setTimeout(() => {
74
+ remove?.();
75
+ remove = ctx.slots.register({ name: 'conversation.composer', priority: 10000, select: () => null }, () => null);
76
+ }, 300);
77
+ };
78
+ const unsubscribe = rooms.subscribe(refresh);
79
+ return () => { unsubscribe(); clearTimeout(timer); remove?.(); };
80
+ });
81
+ ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
82
+ name: 'conversation.session.header.actions', id: 'mobile-room-queue', locale: NS,
83
+ inject: sessionId => {
84
+ const conversation = ctx.get('sessions')?.scope(sessionId)?.get('conversation');
85
+ return conversation ? { updateQueue: conversation.updateQueue.bind(conversation) } : {};
86
+ },
87
+ }, MobileQueue));
88
+ // Contribute through existing seats; no foreign child-slot ownership is claimed.
89
+ ctx.slots.inject('conversation.hero.brand.mark', () => {
90
+ let remove;
91
+ const sync = () => {
92
+ if (presentation.getSnapshot().active)
93
+ remove ??= ctx.slots.register({ name: 'conversation.hero.brand.mark', priority: -100, locale: NS }, MobileWelcome);
94
+ else {
95
+ remove?.();
96
+ remove = undefined;
97
+ }
98
+ };
99
+ sync();
100
+ const unsubscribe = presentation.subscribe(sync);
101
+ return () => { unsubscribe(); remove?.(); };
102
+ });
103
+ ctx.slots.inject('conversation.input.left', () => {
104
+ let remove;
105
+ const sync = () => {
106
+ if (presentation.getSnapshot().active)
107
+ remove ??= ctx.slots.register({ name: 'conversation.input.left', id: 'mobile-input-tools', order: -100, locale: NS, inject: () => ({ rooms }) }, MobileTools);
108
+ else {
109
+ remove?.();
110
+ remove = undefined;
111
+ }
112
+ };
113
+ sync();
114
+ const unsubscribe = presentation.subscribe(sync);
115
+ return () => { unsubscribe(); remove?.(); };
116
+ });
117
+ // Shadow only these public flow slots while this client is in mobile mode.
118
+ for (const name of ['conversation.hero.workspace.directoryFlow', 'sidebar.workspaces.directoryFlow']) {
119
+ ctx.slots.inject(name, () => {
120
+ let remove;
121
+ const sync = () => {
122
+ if (presentation.getSnapshot().active) {
123
+ remove ??= ctx.slots.register({ name, priority: -100, locale: NS, inject: () => ({ directory, navigation }) }, DirectoryFlow);
124
+ }
125
+ else {
126
+ remove?.();
127
+ remove = undefined;
128
+ }
129
+ };
130
+ sync();
131
+ const unsubscribe = presentation.subscribe(sync);
132
+ return () => { unsubscribe(); remove?.(); };
133
+ });
134
+ }
135
+ const post = (type) => {
136
+ if (window.__DSH_MOBILE_SHELL__?.bridgeVersion !== BRIDGE_VERSION)
137
+ return;
138
+ window.webkit?.messageHandlers?.dshMobile?.postMessage({ type, bridgeVersion: BRIDGE_VERSION, mobileVersion: MOBILE_VERSION, layout: presentation.getSnapshot(), navigation: !!navigation.getSnapshot(), anchors: { root: document.querySelectorAll('[data-slot="root"]').length, overlay: document.querySelectorAll('[data-shell-overlay]').length, main: document.querySelectorAll('[data-slot="main"]').length, sidebar: document.querySelectorAll('[data-slot="sidebar"]').length } });
139
+ };
140
+ post('ready');
141
+ ctx.effect(() => presentation.subscribe(() => post('ready')), 'mobile: layout availability');
142
+ ctx.effect(() => navigation.subscribe(() => post('ready')), 'mobile: navigation availability');
143
+ ctx.effect(() => () => { post('unloaded'); }, 'mobile: shell availability');
144
+ }
@@ -0,0 +1,145 @@
1
+ /** Copy for controls owned by mobile; all conversation copy stays with its owner. */
2
+ export declare const NS = "mobile";
3
+ export declare const en: {
4
+ readonly connectPhone: "Connect phone";
5
+ readonly connectIntro: "Continue your conversations in DSH Mobile.";
6
+ readonly connectQR: "Scan with DSH Mobile to sign in";
7
+ readonly connectGenerate: "Show login QR code";
8
+ readonly connectHide: "Hide QR code";
9
+ readonly connectConceal: "Hidden automatically after 2 minutes or when you leave this page.";
10
+ readonly connectStepOpen: "Open DSH Mobile on your iPhone.";
11
+ readonly connectStepScan: "Go to Connection Settings → Scan to connect.";
12
+ readonly connectStepConfirm: "Scan this code, check the host address, then confirm the connection.";
13
+ readonly connectPrivacy: "This QR code grants access to this host. Keep it private. Hiding the code does not revoke the official login link; it remains valid until the host process restarts.";
14
+ readonly connectNetwork: "Keep the computer and its HTTPS entry online. Scanning reconnects the App; it cannot repair a disconnected tunnel.";
15
+ readonly connectSetup: "The operator must configure a reachable HTTPS origin using mobile.publicOrigin or DSH_MOBILE_PUBLIC_ORIGIN, and add its hostname to the host trusted hosts.";
16
+ readonly connectUntrusted: "The configured address is not trusted by this host. Update --trusted-host and restart the host before generating a code.";
17
+ readonly connectUnsupported: "This host version cannot provide official login links. Update the supported host/plugin combination.";
18
+ readonly connectError: "Could not load the connection information. Check the connection and retry.";
19
+ readonly connectLoading: "Loading connection information…";
20
+ readonly directoryManual: 'Enter a path';
21
+ readonly directoryTitle: 'Choose working directory';
22
+ readonly directoryHint: 'Select a folder on your computer.';
23
+ readonly directoryComputer: 'Browse computer';
24
+ readonly directoryNoSaved: 'No saved workspaces yet. Browse your computer to choose one.';
25
+ readonly directoryGo: 'Go';
26
+ readonly directoryParent: 'Parent folder';
27
+ readonly directoryHidden: 'Hidden folders';
28
+ readonly directoryLoading: 'Loading folders…';
29
+ readonly directoryError: 'Could not read this folder. Check the path, permissions and connection.';
30
+ readonly directoryRetry: 'Retry';
31
+ readonly directorySearch: 'Filter folders';
32
+ readonly directoryEmpty: 'No subfolders.';
33
+ readonly directoryTruncated: 'This listing is limited. Enter a full path to open another folder.';
34
+ readonly directorySelect: 'Use this directory';
35
+ readonly renameSession: 'Rename session';
36
+ readonly sessionTitle: 'Session title';
37
+ readonly renameUnavailable: 'Could not rename this conversation. Please try again.';
38
+ readonly roomActionUnavailable: 'Open Members to manage this room; its shortcut is unavailable.';
39
+ readonly memberFailed: 'Task failed';
40
+ readonly currentConversation: 'Current conversation';
41
+ readonly loadingAgents: 'Checking available agents…';
42
+ readonly noAgents: 'No local agents are available on this host.';
43
+ readonly loginAgents: 'Sign in to an agent on the computer before inviting it.';
44
+ readonly queued: 'Queued messages';
45
+ readonly queueError: 'Could not update the queue. Please try again.';
46
+ readonly editQueued: 'Edit';
47
+ readonly removeQueued: 'Remove';
48
+ readonly sendNow: 'Send now';
49
+ readonly sendingQueued: 'Waiting for host admission…';
50
+ readonly members: 'Members';
51
+ readonly membersHint: 'Open a member conversation, or adjust its role.';
52
+ readonly inviteMember: 'Invite member';
53
+ readonly memberSettings: 'Member settings';
54
+ readonly duplicateMember: 'This member name is already in use.';
55
+ readonly invalidMember: 'Use a name without spaces or @.';
56
+ readonly roomError: 'Could not complete this request. Check the connection and try again.';
57
+ readonly mainMember: 'Main agent';
58
+ readonly openMemberChat: 'Open conversation';
59
+ readonly awaitingMember: 'Waiting for a first task';
60
+ readonly agentProvider: 'Agent';
61
+ readonly chooseProvider: 'Choose an available agent';
62
+ readonly needsLogin: 'Sign in on computer';
63
+ readonly memberName: 'Name';
64
+ readonly memberRole: 'Role instructions';
65
+ readonly memberRoleHint: 'What should this member focus on?';
66
+ readonly inviteHint: 'Invitation adds the member. Send an @message in the room to start a task.';
67
+ readonly roleChangeHint: 'Role changes apply to subsequent dispatches.';
68
+ readonly advanced: 'Advanced';
69
+ readonly inheritWorkspace: 'Inherit room workspace';
70
+ readonly saving: 'Saving…';
71
+ readonly save: 'Save';
72
+ readonly stopMemberConfirm: 'Stop this member’s current task?';
73
+ readonly removeMemberConfirm: 'Remove this member from the room? Its conversation history is retained.';
74
+ readonly confirm: 'Confirm';
75
+ readonly stopMember: 'Stop task';
76
+ readonly removeMember: 'Remove member';
77
+ readonly backMembers: 'Back to members';
78
+ readonly memberUnit: 'members';
79
+ readonly inputTools: 'Add and configure';
80
+ readonly closeTools: 'Close input tools';
81
+ readonly attachments: 'Add attachment';
82
+ readonly commands: 'Commands';
83
+ readonly permissions: 'Permissions';
84
+ readonly welcome: 'What would you like to do?';
85
+ readonly welcomeHint: 'Continue an idea, or start a new task.';
86
+ readonly menu: 'Conversations';
87
+ readonly close: 'Close navigation';
88
+ readonly returnToConversation: 'Return to full conversation';
89
+ readonly title: 'Your workspace, with you';
90
+ readonly settings: 'Mobile settings';
91
+ readonly back: 'Back';
92
+ readonly display: 'Display';
93
+ readonly automatic: 'Follow screen size';
94
+ readonly mobile: 'Mobile';
95
+ readonly desktop: 'Desktop';
96
+ readonly restoreMobile: 'Return to mobile layout';
97
+ readonly appearance: 'Layout applies only to this browser.';
98
+ readonly connection: 'Connection';
99
+ readonly reconnect: 'Reconnect';
100
+ readonly connected: 'Connected';
101
+ readonly connecting: 'Connecting…';
102
+ readonly disconnected: 'Connection interrupted';
103
+ readonly server: 'Host';
104
+ readonly privacy: 'Models, tools and files run on your host.';
105
+ readonly done: 'Done';
106
+ readonly brand: 'DSH';
107
+ readonly grouping: 'Conversation view';
108
+ readonly byTime: 'Time';
109
+ readonly byWorkspace: 'Workspace';
110
+ readonly today: 'Today';
111
+ readonly yesterday: 'Yesterday';
112
+ readonly earlier: 'Earlier';
113
+ readonly unassignedWorkspace: 'No workspace';
114
+ readonly scan: 'Scan to connect';
115
+ readonly conversation: 'Conversation';
116
+ readonly options: 'Conversation options';
117
+ readonly connectionSettings: 'Connection settings';
118
+ readonly libraryEyebrow: 'YOUR WORKSPACE';
119
+ readonly libraryTitle: 'Pick up where you left off.';
120
+ readonly librarySubtitle: 'A thought, a conversation, a little progress.';
121
+ readonly newSession: 'New conversation';
122
+ readonly searchTitles: 'Search';
123
+ readonly searchResults: 'Results';
124
+ readonly recent: 'Recent conversations';
125
+ readonly workspaces: 'Workspaces';
126
+ readonly loadingSessions: 'Loading conversations…';
127
+ readonly noMatches: 'No matching conversations.';
128
+ readonly noSessions: 'Your conversations will appear here.';
129
+ readonly navigationError: 'Could not open this conversation. Please try again.';
130
+ readonly running: 'Working';
131
+ readonly completed: 'Completed';
132
+ readonly libraryFooter: 'A little closer, wherever you are.';
133
+ readonly workspace: 'Open workspace';
134
+ readonly hostPath: 'Computer directory';
135
+ readonly hostPathHelp: 'Enter an existing absolute directory on your computer. This does not select a folder on your phone.';
136
+ readonly openWorkspace: 'Open';
137
+ readonly cancel: 'Cancel';
138
+ };
139
+ export declare const zh: Record<keyof typeof en, string>;
140
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
141
+ interface LocaleNamespaceMap {
142
+ mobile: keyof typeof en;
143
+ }
144
+ }
145
+ //# sourceMappingURL=locales.d.ts.map