@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,73 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { GROUPING_KEY, groupSessions, mainSessionId, recentSessions } from './navigation.js';
4
+ import { MobileIcon } from './MobileIcon.js';
5
+ import { hasNativeAction, requestNativeAction } from './native.js';
6
+ /** Browse official metadata; the official workspace service remains the only navigation writer. */
7
+ export function MobileLibrary({ rooms, navigation, onOpen, onBeforeOpen, t }) {
8
+ const sessionFeed = navigation.sessions.list, workspaceFeed = navigation.workspaces.list;
9
+ // Official feeds may expose prototype methods. Keep their receiver and stable subscriptions.
10
+ const sessions = useSyncExternalStore(useCallback(listener => sessionFeed.subscribe(listener), [sessionFeed]), useCallback(() => sessionFeed.getSnapshot(), [sessionFeed]));
11
+ const workspaces = useSyncExternalStore(useCallback(listener => workspaceFeed.subscribe(listener), [workspaceFeed]), useCallback(() => workspaceFeed.getSnapshot(), [workspaceFeed]));
12
+ const [query, setQuery] = useState(''), [searching, setSearching] = useState(false), [error, setError] = useState(false);
13
+ const [grouping, setGrouping] = useState(() => {
14
+ try {
15
+ return localStorage.getItem(GROUPING_KEY) === 'workspace' ? 'workspace' : 'time';
16
+ }
17
+ catch {
18
+ return 'time';
19
+ }
20
+ });
21
+ const [collapsed, setCollapsed] = useState(() => new Set());
22
+ const input = useRef(null);
23
+ const rows = recentSessions(sessions, workspaces.archivedSessionIds, query);
24
+ const current = mainSessionId(sessions);
25
+ const groups = groupSessions(rows, grouping, { today: t('today'), yesterday: t('yesterday'), earlier: t('earlier'), workspace: t('unassignedWorkspace') });
26
+ const chooseGrouping = (value) => {
27
+ setGrouping(value);
28
+ try {
29
+ localStorage.setItem(GROUPING_KEY, value);
30
+ }
31
+ catch { /* Restricted storage keeps this preference in memory. */ }
32
+ };
33
+ return _jsxs("section", { "data-mobile-library": true, "aria-label": t('menu'), children: [_jsxs("header", { "data-mobile-library-header": true, children: [_jsx("h1", { children: t('menu') }), _jsx("div", { "data-mobile-grouping": true, role: "group", "aria-label": t('grouping'), children: ['time', 'workspace'].map(value => _jsx("button", { "aria-pressed": grouping === value, onClick: () => chooseGrouping(value), children: t(value === 'time' ? 'byTime' : 'byWorkspace') }, value)) })] }), _jsxs("div", { "data-mobile-recents": true, children: [error && _jsx("p", { role: "alert", children: t('navigationError') }), sessions.phase !== 'ready' && !rows.length ? _jsx("p", { role: "status", children: t('loadingSessions') }) : rows.length === 0 ? _jsx("p", { "data-mobile-empty": true, role: "status", children: query ? t('noMatches') : t('noSessions') }) : groups.map(group => {
34
+ const open = grouping === 'time' || !!query.trim() || !collapsed.has(group.key);
35
+ return _jsxs("section", { "data-mobile-session-group": true, children: [grouping === 'workspace' ? _jsxs("button", { "data-mobile-workspace-group": true, "aria-expanded": open, onClick: () => setCollapsed(previous => {
36
+ const next = new Set(previous);
37
+ next.has(group.key) ? next.delete(group.key) : next.add(group.key);
38
+ return next;
39
+ }), children: [_jsx(MobileIcon, { name: "folder", size: 18 }), _jsx("strong", { children: group.label }), _jsx("small", { children: group.rows.length }), _jsx(MobileIcon, { name: open ? "down" : "right", size: 18 })] }) : _jsx("h2", { children: group.label }), open && _jsx("ul", { children: group.rows.map(row => _jsx("li", { children: _jsxs("button", { "data-mobile-session": true, "data-mobile-session-id": row.id, "aria-current": current === row.id ? 'page' : undefined, onClick: () => {
40
+ try {
41
+ onBeforeOpen?.();
42
+ navigation.workspace.openSession(row.id);
43
+ setError(false);
44
+ onOpen();
45
+ }
46
+ catch {
47
+ setError(true);
48
+ }
49
+ }, children: [_jsxs("span", { "data-mobile-session-copy": true, children: [_jsx("strong", { children: row.title || (row.blank ? t('newSession') : row.displayTitle) }), _jsx(SessionMetadata, { id: row.id, cwd: row.cwd ?? '', ...(rooms ? { rooms } : {}), t: t })] }), row.running ? _jsx("span", { "data-mobile-activity": true, role: "status", children: t('running') }) : _jsx("time", { dateTime: new Date(row.updatedAt).toISOString(), children: new Date(row.updatedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) })] }) }, row.id)) })] }, group.key);
50
+ })] }), _jsxs("footer", { "data-mobile-search-dock": true, children: [_jsxs("label", { "data-mobile-search": true, children: [_jsx(MobileIcon, { name: "search", size: 20 }), _jsx("input", { ref: input, type: "search", enterKeyHint: "search", autoComplete: "off", value: query, onFocus: () => setSearching(true), onChange: e => setQuery(e.target.value), placeholder: t('searchTitles'), "aria-label": t('searchTitles') })] }), searching ? _jsx("button", { "data-mobile-search-cancel": true, "aria-label": t('cancel'), onClick: () => { setQuery(''); setSearching(false); input.current?.blur(); }, children: _jsx(MobileIcon, { name: "close" }) }) : hasNativeAction('scan') && _jsx("button", { "data-mobile-scan": true, "aria-label": t('scan'), onClick: () => requestNativeAction('scan'), children: _jsx(MobileIcon, { name: "scan" }) })] })] });
51
+ }
52
+ const emptyRoomSubscribe = () => () => { };
53
+ const emptyRoomSnapshot = () => 0;
54
+ function SessionMetadata({ id, cwd, rooms, t }) {
55
+ const ref = useRef(null);
56
+ useSyncExternalStore(rooms?.subscribe ?? emptyRoomSubscribe, rooms?.getSnapshot ?? emptyRoomSnapshot);
57
+ useEffect(() => {
58
+ if (!rooms?.available() || !ref.current)
59
+ return;
60
+ if (typeof IntersectionObserver === 'undefined') {
61
+ void rooms.refresh(id);
62
+ return;
63
+ }
64
+ const observer = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) {
65
+ void rooms.refresh(id);
66
+ observer.disconnect();
67
+ } });
68
+ observer.observe(ref.current);
69
+ return () => observer.disconnect();
70
+ }, [rooms, id]);
71
+ const state = rooms?.get(id), count = state ? state.members.length : state === null || !rooms?.available() ? 1 : '…';
72
+ return _jsxs("small", { ref: ref, children: [cwd.split(/[\\/]/).filter(Boolean).at(-1) || t('unassignedWorkspace'), " \u00B7 ", count, " ", t('memberUnit')] });
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { IConversation } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ export interface MobileQueueInjected {
4
+ updateQueue?: IConversation['updateQueue'];
5
+ }
6
+ /** Room's takeover hides the official QueueDock. Restore its mutation verbs
7
+ * through the same scoped Conversation API, without touching the Room composer. */
8
+ export declare function MobileQueue({ useSession, useProjection, updateQueue, t }: PropsRuntime<'conversation.session.header.actions'> & PropsLocale<'mobile'> & MobileQueueInjected): import("react").ReactPortal | null;
9
+ //# sourceMappingURL=MobileQueue.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { MobileIcon } from './MobileIcon.js';
4
+ import { createPortal } from 'react-dom';
5
+ import { queuedRows } from './queue.js';
6
+ /** Room's takeover hides the official QueueDock. Restore its mutation verbs
7
+ * through the same scoped Conversation API, without touching the Room composer. */
8
+ export function MobileQueue({ useSession, useProjection, updateQueue, t }) {
9
+ // alpha.2 serves the queue as the `inbox` projection (its declared value is
10
+ // wire JSON; the official QueueDock narrows it the same way); the 0.1.5
11
+ // standard kit has no projection hook and its host serves no `inbox` key —
12
+ // both absence paths fall back to the legacy snapshot queue inside queuedRows.
13
+ const legacyQueue = useSession(s => s.queue);
14
+ const inbox = (useProjection === undefined ? undefined : useProjection('inbox'));
15
+ const rows = queuedRows(legacyQueue, inbox);
16
+ const running = useSession(s => s.running);
17
+ const pending = useSession(s => s.pendingSubmissions);
18
+ const mutable = useSession(s => s.subagent === null || s.subagent.address.mode === 'continuable');
19
+ const [seat, setSeat] = useState(null), [open, setOpen] = useState(false);
20
+ const [busy, setBusy] = useState(false), [error, setError] = useState(false), [edit, setEdit] = useState(null);
21
+ const dialog = useRef(null), alive = useRef(true);
22
+ useEffect(() => {
23
+ alive.current = true;
24
+ if (!updateQueue)
25
+ return () => { alive.current = false; };
26
+ let owned;
27
+ const sync = () => {
28
+ const fallback = document.querySelector('[data-mobile-frame] [data-chain-overlay-fallback="conversation.composer"]');
29
+ const root = fallback?.closest('[data-composer-seat]');
30
+ const roomQueue = root?.querySelector('[data-testid="room-queue-strip"]');
31
+ if (document.documentElement.hasAttribute('data-dsh-mobile') && fallback?.style.display === 'none' && roomQueue && root) {
32
+ if (owned && owned.parentElement !== root) {
33
+ owned.remove();
34
+ owned = undefined;
35
+ }
36
+ if (!owned) {
37
+ owned = document.createElement('div');
38
+ owned.dataset.mobileRoomQueue = '';
39
+ root.prepend(owned);
40
+ setSeat(owned);
41
+ }
42
+ }
43
+ else if (owned) {
44
+ owned.remove();
45
+ owned = undefined;
46
+ setSeat(null);
47
+ }
48
+ };
49
+ const observer = new MutationObserver(sync);
50
+ observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ['style', 'data-dsh-mobile'] });
51
+ sync();
52
+ return () => { alive.current = false; observer.disconnect(); owned?.remove(); };
53
+ }, [updateQueue]);
54
+ useEffect(() => { if (open && seat)
55
+ dialog.current?.showModal();
56
+ else
57
+ dialog.current?.close(); }, [open, seat]);
58
+ const admitted = new Set(rows.flatMap(row => row.rpcId === undefined ? [] : [row.rpcId]));
59
+ const pendingQueue = pending.filter(row => row.placement === 'queued' && !admitted.has(row.requestId));
60
+ const count = rows.length + pendingQueue.length;
61
+ useEffect(() => {
62
+ if (!count)
63
+ setOpen(false);
64
+ if (edit && (!mutable || !rows.some(row => row.id === edit.id)))
65
+ setEdit(null);
66
+ }, [count, mutable, rows, edit]);
67
+ const apply = async (id, action) => {
68
+ if (!updateQueue || busy || !mutable || !rows.some(row => row.id === id))
69
+ return;
70
+ setBusy(true);
71
+ setError(false);
72
+ try {
73
+ await updateQueue(id, action);
74
+ if (alive.current)
75
+ setEdit(null);
76
+ }
77
+ catch {
78
+ if (alive.current)
79
+ setError(true);
80
+ }
81
+ finally {
82
+ if (alive.current)
83
+ setBusy(false);
84
+ }
85
+ };
86
+ if (!seat || !updateQueue || !count)
87
+ return null;
88
+ return createPortal(_jsxs(_Fragment, { children: [_jsxs("button", { "data-mobile-queue-pill": true, onClick: () => setOpen(true), children: [t('queued'), " \u00B7 ", count] }), _jsxs("dialog", { ref: dialog, "data-mobile-tools-dialog": true, "aria-label": t('queued'), onClose: () => setOpen(false), children: [_jsx("div", { "data-mobile-tools-handle": true }), _jsxs("header", { children: [_jsx("strong", { children: t('queued') }), _jsx("button", { "aria-label": t('done'), onClick: () => dialog.current?.close(), children: _jsx(MobileIcon, { name: "close" }) })] }), error && _jsx("p", { role: "alert", children: t('queueError') }), rows.map(row => _jsxs("section", { "data-mobile-queue-row": true, children: [edit?.id === row.id ? _jsx("textarea", { "aria-label": t('editQueued'), value: edit.text, onChange: e => setEdit({ id: row.id, text: e.target.value }) }) : _jsx("p", { children: row.content.map(b => b.type === 'text' ? b.text : `[${t('attachments')}]`).join('') }), _jsx("div", { children: edit?.id === row.id ? _jsxs(_Fragment, { children: [_jsx("button", { disabled: busy || !edit.text.trim(), onClick: () => void apply(row.id, { kind: 'edit', content: [{ type: 'text', text: edit.text }] }), children: t('save') }), _jsx("button", { disabled: busy, onClick: () => setEdit(null), children: t('cancel') })] }) : _jsxs(_Fragment, { children: [_jsx("button", { disabled: !mutable || busy || !running, onClick: () => void apply(row.id, { kind: 'steer' }), children: t('sendNow') }), _jsx("button", { disabled: !mutable || busy || row.text === null, onClick: () => { if (row.text !== null)
89
+ setEdit({ id: row.id, text: row.text }); }, children: t('editQueued') }), _jsx("button", { disabled: !mutable || busy, onClick: () => void apply(row.id, { kind: 'remove' }), children: t('removeQueued') })] }) })] }, row.id)), pendingQueue.length > 0 && _jsx("p", { role: "status", children: t('sendingQueued') })] })] }), seat);
90
+ }
@@ -0,0 +1,13 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { NavigationCapabilities } from './navigation.ts';
3
+ import { MobileRooms } from './rooms.ts';
4
+ export declare const openMobileMembers: (sessionId: string, invite?: boolean) => boolean;
5
+ type Props = PropsLocale<'mobile'> & {
6
+ rooms: MobileRooms;
7
+ navigation: NavigationCapabilities;
8
+ prepareNavigation: () => void;
9
+ };
10
+ /** A read-only roster shortcut. All mutations open the installed Room UI. */
11
+ export declare function MobileRoomNavigation({ rooms, navigation, prepareNavigation, t }: Props): import("react").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=MobileRooms.d.ts.map
@@ -0,0 +1,80 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { mainSessionId } from './navigation.js';
5
+ import { MobileRooms } from './rooms.js';
6
+ import { MobileIcon } from './MobileIcon.js';
7
+ import { openRoomInvite, openRoomMemberSettings } from './roomActions.js';
8
+ export const openMobileMembers = (sessionId, invite = false) => window.dispatchEvent(new CustomEvent('dsh-mobile-members', { detail: { sessionId, invite } }));
9
+ /** A read-only roster shortcut. All mutations open the installed Room UI. */
10
+ export function MobileRoomNavigation({ rooms, navigation, prepareNavigation, t }) {
11
+ const feed = navigation.sessions.list;
12
+ const current = useSyncExternalStore(useCallback(fn => feed.subscribe(fn), [feed]), useCallback(() => mainSessionId(feed.getSnapshot()), [feed]));
13
+ useSyncExternalStore(rooms.subscribe, rooms.getSnapshot);
14
+ const room = current ? rooms.get(current) : undefined;
15
+ const [open, setOpen] = useState(false), [error, setError] = useState(false);
16
+ const dialog = useRef(null), epoch = useRef(0);
17
+ useEffect(() => {
18
+ epoch.current++;
19
+ setOpen(false);
20
+ setError(false);
21
+ if (!current)
22
+ return;
23
+ const refresh = () => { if (!document.hidden)
24
+ void rooms.refresh(current, true); };
25
+ refresh();
26
+ const timer = window.setInterval(refresh, 7000);
27
+ return () => { epoch.current++; window.clearInterval(timer); };
28
+ }, [rooms, current]);
29
+ const close = () => { dialog.current?.close(); setOpen(false); };
30
+ const invite = () => { close(); if (!openRoomInvite(document)) {
31
+ setError(true);
32
+ setOpen(true);
33
+ } };
34
+ useEffect(() => {
35
+ const show = (event) => {
36
+ const detail = event.detail;
37
+ if (!current || detail.sessionId !== current || !rooms.available())
38
+ return;
39
+ if (detail.invite)
40
+ invite();
41
+ else {
42
+ setOpen(true);
43
+ setError(false);
44
+ }
45
+ };
46
+ window.addEventListener('dsh-mobile-members', show);
47
+ return () => window.removeEventListener('dsh-mobile-members', show);
48
+ }, [current, rooms]);
49
+ useEffect(() => { if (open)
50
+ dialog.current?.showModal();
51
+ else
52
+ dialog.current?.close(); }, [open]);
53
+ const edit = async (name) => {
54
+ const generation = epoch.current;
55
+ close();
56
+ const ok = await openRoomMemberSettings(document, name, () => epoch.current === generation);
57
+ if (!ok && epoch.current === generation) {
58
+ setError(true);
59
+ setOpen(true);
60
+ }
61
+ };
62
+ // The official workspace service is the only navigation writer on both host
63
+ // lines (alpha.2 deleted sessions.open); it throws synchronously on failure.
64
+ const openChat = (childSessionId) => {
65
+ prepareNavigation();
66
+ try {
67
+ navigation.workspace.openSession(childSessionId);
68
+ close();
69
+ }
70
+ catch {
71
+ setError(true);
72
+ setOpen(true);
73
+ }
74
+ };
75
+ return _jsxs(_Fragment, { children: [room && _jsxs("button", { "data-mobile-members-open": true, "aria-label": t('members'), onClick: () => { setOpen(true); setError(false); }, children: [_jsx(MobileIcon, { name: "members" }), _jsx("small", { children: room.members.length })] }), createPortal(_jsxs("dialog", { ref: dialog, "data-mobile-room-dialog": true, "data-mobile-tools-dialog": true, "aria-label": t('members'), onClose: () => setOpen(false), children: [_jsx("div", { "data-mobile-tools-handle": true }), _jsxs("header", { children: [_jsx("strong", { children: t('members') }), _jsx("button", { "aria-label": t('done'), onClick: close, children: _jsx(MobileIcon, { name: "close" }) })] }), error && _jsx("p", { role: "alert", children: t('roomActionUnavailable') }), _jsx("p", { "data-mobile-room-hint": true, children: t('membersHint') }), room?.members.map(m => {
76
+ const run = room.runs.find(r => r.member === m.name);
77
+ return _jsxs("div", { "data-mobile-member-row": true, children: [_jsxs("button", { "data-mobile-member-chat": true, disabled: !m.childSessionId, onClick: () => { if (m.childSessionId)
78
+ openChat(m.childSessionId); }, children: [_jsx("span", { "data-mobile-avatar": true, children: _jsx(MobileIcon, { name: m.kind === 'main-agent' ? 'compose' : 'members', size: 20 }) }), _jsxs("span", { children: [_jsx("strong", { children: m.name }), _jsxs("small", { children: [m.provider ?? t('mainMember'), " \u00B7 ", m.kind === 'main-agent' ? t('currentConversation') : run?.state === 'running' ? t('running') : run?.state === 'failed' ? t('memberFailed') : m.childSessionId ? t('openMemberChat') : t('awaitingMember')] })] })] }), m.kind !== 'main-agent' && _jsx("button", { "aria-label": `${t('memberSettings')} ${m.name}`, onClick: () => void edit(m.name), children: _jsx(MobileIcon, { name: "settings", size: 20 }) })] }, m.name);
79
+ }), _jsxs("button", { "data-mobile-room-primary": true, onClick: invite, children: [_jsx(MobileIcon, { name: "plus", size: 18 }), t('inviteMember')] })] }), document.body)] });
80
+ }
@@ -0,0 +1,4 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ /** Brand-seat replacement only. The official hero picker and editor retain ownership. */
3
+ export declare function MobileWelcome({ t }: PropsLocale<'mobile'>): import("react").JSX.Element;
4
+ //# sourceMappingURL=MobileSeats.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Brand-seat replacement only. The official hero picker and editor retain ownership. */
3
+ export function MobileWelcome({ t }) {
4
+ return _jsxs("div", { "data-mobile-welcome": true, children: [_jsx("h2", { children: t('welcome') }), _jsx("p", { children: t('welcomeHint') })] });
5
+ }
@@ -0,0 +1,7 @@
1
+ import type { PropsRuntime, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { MobileRooms } from './rooms.ts';
3
+ /** Own only the launcher and grouped navigation; action targets stay host-owned. */
4
+ export declare function MobileTools({ t, useSession, rooms }: PropsLocale<'mobile'> & PropsRuntime<'conversation.input.left'> & {
5
+ rooms: MobileRooms;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=MobileTools.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { ComposerActions } from './composerActions.js';
4
+ import { openMobileMembers } from './MobileRooms.js';
5
+ import { MobileIcon } from './MobileIcon.js';
6
+ /** Own only the launcher and grouped navigation; action targets stay host-owned. */
7
+ export function MobileTools({ t, useSession, rooms }) {
8
+ const sessionId = useSession(s => s.sessionId);
9
+ const seat = useRef(null);
10
+ const [actions, setActions] = useState();
11
+ useLayoutEffect(() => {
12
+ if (!seat.current)
13
+ return;
14
+ const controller = new ComposerActions(seat.current);
15
+ setActions(controller);
16
+ return () => controller.dispose();
17
+ }, []);
18
+ return _jsx("span", { ref: seat, "data-mobile-tools-seat": true, children: actions && _jsx(Tools, { actions: actions, t: t, invite: rooms.available() ? () => openMobileMembers(sessionId, true) : undefined }) });
19
+ }
20
+ function Tools({ actions, t, invite }) {
21
+ const targets = useSyncExternalStore(actions.subscribe, actions.getSnapshot);
22
+ const dialog = useRef(null), launcher = useRef(null);
23
+ const close = () => { dialog.current?.close(); launcher.current?.setAttribute('aria-expanded', 'false'); };
24
+ useLayoutEffect(() => { if (!targets.commands || !targets.attachments)
25
+ close(); }, [targets.commands, targets.attachments]);
26
+ if (!targets.commands || !targets.attachments)
27
+ return null;
28
+ const invoke = (action) => { close(); actions.invoke(action); };
29
+ return _jsxs(_Fragment, { children: [_jsx("button", { ref: launcher, "data-mobile-tools-open": true, "aria-label": t('inputTools'), "aria-haspopup": "dialog", "aria-expanded": "false", onClick: () => { dialog.current?.showModal(); launcher.current?.setAttribute('aria-expanded', 'true'); }, children: _jsx(MobileIcon, { name: "plus" }) }), _jsxs("dialog", { ref: dialog, "data-mobile-tools-dialog": true, "aria-label": t('inputTools'), onClose: () => launcher.current?.setAttribute('aria-expanded', 'false'), onClick: event => { if (event.target === dialog.current) {
30
+ const r = dialog.current.getBoundingClientRect();
31
+ if (event.clientY < r.top || event.clientX < r.left || event.clientX > r.right || event.clientY > r.bottom)
32
+ close();
33
+ } }, children: [_jsx("div", { "data-mobile-tools-handle": true }), _jsxs("header", { children: [_jsx("strong", { children: t('inputTools') }), _jsx("button", { "aria-label": t('closeTools'), onClick: close, children: _jsx(MobileIcon, { name: "close" }) })] }), _jsxs("div", { "data-mobile-tools-grid": true, children: [_jsxs("button", { disabled: targets.attachments.disabled, onClick: () => invoke('attachments'), children: [_jsx(MobileIcon, { name: "attachment" }), t('attachments')] }), _jsxs("button", { disabled: targets.commands.disabled, onClick: () => invoke('commands'), children: [_jsx(MobileIcon, { name: "commands" }), t('commands')] })] }), targets.permissions && _jsxs("button", { "data-mobile-tools-permission": true, disabled: targets.permissions.disabled, onClick: () => invoke('permissions'), children: [_jsx(MobileIcon, { name: "shield" }), _jsx("span", { children: t('permissions') }), _jsx("small", { children: targets.permissions.label }), _jsx(MobileIcon, { name: "right", size: 16 })] }), invite && _jsxs("button", { "data-mobile-tools-permission": true, onClick: () => { close(); invite(); }, children: [_jsx(MobileIcon, { name: "members" }), _jsx("span", { children: t('inviteMember') }), _jsx("small", {}), _jsx(MobileIcon, { name: "right", size: 16 })] })] })] });
34
+ }
@@ -0,0 +1,10 @@
1
+ import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { NavigationCapabilities } from './navigation.ts';
3
+ /** Same per-session command as the Web workspace dialog; no local title cache. */
4
+ export declare function SessionRename({ navigation, sessionId, title, close, t }: {
5
+ navigation: NavigationCapabilities;
6
+ sessionId: Parameters<NavigationCapabilities['sessions']['binding']>[0];
7
+ title: string;
8
+ close: () => void;
9
+ } & PropsLocale<'mobile'>): import("react").ReactPortal;
10
+ //# sourceMappingURL=SessionRename.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ /** Same per-session command as the Web workspace dialog; no local title cache. */
5
+ export function SessionRename({ navigation, sessionId, title, close, t }) {
6
+ const dialog = useRef(null);
7
+ const [draft, setDraft] = useState(title), [busy, setBusy] = useState(false), [error, setError] = useState('');
8
+ const alive = useRef(true), submitting = useRef(false);
9
+ useLayoutEffect(() => {
10
+ alive.current = true;
11
+ dialog.current?.showModal();
12
+ return () => { alive.current = false; dialog.current?.close(); };
13
+ }, []);
14
+ const save = async () => {
15
+ if (submitting.current || !draft.trim())
16
+ return;
17
+ submitting.current = true;
18
+ setBusy(true);
19
+ setError('');
20
+ try {
21
+ const session = navigation.sessions.binding(sessionId)?.session;
22
+ if (!session)
23
+ throw new Error(t('renameUnavailable'));
24
+ const result = await session.rename(draft.trim());
25
+ if (!result.ok)
26
+ throw new Error(result.error.message);
27
+ if (alive.current)
28
+ close();
29
+ }
30
+ catch (reason) {
31
+ if (alive.current)
32
+ setError(reason instanceof Error ? reason.message : t('renameUnavailable'));
33
+ }
34
+ finally {
35
+ submitting.current = false;
36
+ if (alive.current)
37
+ setBusy(false);
38
+ }
39
+ };
40
+ return createPortal(_jsx("dialog", { ref: dialog, "data-mobile-dialog": true, "data-mobile-rename-dialog": true, "aria-labelledby": "mobile-rename-heading", onCancel: event => { event.preventDefault(); if (!busy)
41
+ close(); }, children: _jsxs("form", { onSubmit: event => { event.preventDefault(); void save(); }, children: [_jsx("h2", { id: "mobile-rename-heading", children: t('renameSession') }), _jsxs("label", { children: [t('sessionTitle'), _jsx("input", { value: draft, onChange: event => setDraft(event.target.value), autoComplete: "off", enterKeyHint: "done", disabled: busy, onKeyDown: event => { if (event.key === 'Enter' && event.nativeEvent.isComposing)
42
+ event.preventDefault(); } })] }), error && _jsx("p", { role: "alert", children: error }), _jsxs("footer", { children: [_jsx("button", { type: "button", disabled: busy, onClick: close, children: t('cancel') }), _jsx("button", { type: "submit", "data-mobile-primary": true, disabled: busy || !draft.trim(), children: t(busy ? 'saving' : 'save') })] })] }) }), document.body);
43
+ }
@@ -0,0 +1,26 @@
1
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ type Receipt = {
3
+ id: string;
4
+ text: string;
5
+ };
6
+ /** Read durable public chat records, never local optimistic submission echoes. */
7
+ export declare function acceptedMessages(chat: unknown): Receipt[];
8
+ /** A successful admission may hide the keyboard only for this exact local send.
9
+ * Failed sends, history loads, remote sends and newly typed drafts keep focus. */
10
+ export declare class SubmissionFocus {
11
+ private doc;
12
+ private receipts;
13
+ private highWater;
14
+ private attempt;
15
+ constructor(doc: Document);
16
+ private editor;
17
+ private value;
18
+ private arm;
19
+ private click;
20
+ private key;
21
+ update(receipts: Receipt[]): void;
22
+ dispose(): void;
23
+ }
24
+ export declare function MobileSubmissionFocus({ useConversation, useSession, useProjection }: PropsRuntime<'conversation.session.header.actions'>): null;
25
+ export {};
26
+ //# sourceMappingURL=SubmissionFocus.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { queuedRows } from './queue.js';
3
+ const textOf = (content) => Array.isArray(content) ? content.map(b => b?.type === 'text' ? b.text : '').join('') : '';
4
+ /** Read durable public chat records, never local optimistic submission echoes. */
5
+ export function acceptedMessages(chat) {
6
+ const value = chat;
7
+ const nodes = value?.nodes?.values?.();
8
+ if (!Array.isArray(nodes))
9
+ return [];
10
+ return nodes.flatMap(node => {
11
+ const data = node?.data ?? node;
12
+ return (node?.kind === 'user' || node?.kind === 'steering') && typeof data?.seq === 'number'
13
+ ? [{ id: `message:${data.seq}`, text: textOf(data.content) }] : [];
14
+ });
15
+ }
16
+ /** A successful admission may hide the keyboard only for this exact local send.
17
+ * Failed sends, history loads, remote sends and newly typed drafts keep focus. */
18
+ export class SubmissionFocus {
19
+ doc;
20
+ receipts = new Set();
21
+ highWater = -1;
22
+ attempt;
23
+ constructor(doc) {
24
+ this.doc = doc;
25
+ doc.addEventListener('pointerdown', this.click, true);
26
+ doc.addEventListener('keydown', this.key, true);
27
+ }
28
+ editor() {
29
+ if (!this.doc.documentElement.hasAttribute('data-dsh-mobile'))
30
+ return;
31
+ const el = this.doc.activeElement;
32
+ return el instanceof HTMLElement && el.matches('[contenteditable="true"], textarea') && el.closest('[data-composer-seat]') ? el : undefined;
33
+ }
34
+ value(el) { return el instanceof HTMLTextAreaElement ? el.value : el.textContent ?? ''; }
35
+ arm() { const editor = this.editor(); if (editor)
36
+ this.attempt = { editor, text: this.value(editor).trim() }; }
37
+ click = (e) => {
38
+ const button = e.target instanceof Element ? e.target.closest('button') : null;
39
+ if (button && !button.disabled && button.closest('[data-composer-seat]') && /发送|Send|Queue|Steer/i.test(button.getAttribute('aria-label') ?? ''))
40
+ this.arm();
41
+ };
42
+ key = (e) => {
43
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && this.editor())
44
+ this.arm();
45
+ };
46
+ update(receipts) {
47
+ const fresh = receipts.filter(r => !this.receipts.has(r.id) && (!r.id.startsWith('message:') || Number(r.id.slice(8)) > this.highWater));
48
+ for (const row of receipts)
49
+ if (row.id.startsWith('message:'))
50
+ this.highWater = Math.max(this.highWater, Number(row.id.slice(8)));
51
+ this.receipts = new Set(receipts.map(r => r.id));
52
+ const attempt = this.attempt;
53
+ if (!attempt || !fresh.some(r => r.text.trim() === attempt.text))
54
+ return;
55
+ this.attempt = undefined;
56
+ // Lexical clears optimistically; sibling composers clear after their RPC.
57
+ requestAnimationFrame(() => {
58
+ if (this.doc.documentElement.hasAttribute('data-dsh-mobile') && this.doc.activeElement === attempt.editor && !this.value(attempt.editor).trim())
59
+ attempt.editor.blur();
60
+ });
61
+ }
62
+ dispose() { this.attempt = undefined; this.doc.removeEventListener('pointerdown', this.click, true); this.doc.removeEventListener('keydown', this.key, true); }
63
+ }
64
+ export function MobileSubmissionFocus({ useConversation, useSession, useProjection }) {
65
+ const receipts = useConversation(s => JSON.stringify(acceptedMessages(s.views.get('chat'))));
66
+ // Same dual-line queue read as MobileQueue: alpha.2's inbox projection, else the 0.1.5 snapshot queue.
67
+ const legacyQueue = useSession(s => s.queue);
68
+ const inbox = (useProjection === undefined ? undefined : useProjection('inbox'));
69
+ const queue = useMemo(() => queuedRows(legacyQueue, inbox), [legacyQueue, inbox]);
70
+ const controller = useRef();
71
+ useEffect(() => { const focus = new SubmissionFocus(document); controller.current = focus; return () => focus.dispose(); }, []);
72
+ useEffect(() => { controller.current?.update([...JSON.parse(receipts), ...queue.map(row => ({ id: `queue:${row.id}`, text: textOf(row.content) }))]); }, [receipts, queue]);
73
+ return null;
74
+ }
@@ -0,0 +1,24 @@
1
+ /** Checked rc1 button bridge. The official editor, chooser, permission menu and
2
+ * confirmation remain mounted and retain their callbacks. Unknown markup falls back. */
3
+ export type ComposerAction = 'commands' | 'attachments' | 'permissions';
4
+ export interface ActionTarget {
5
+ button: HTMLButtonElement;
6
+ label: string;
7
+ disabled: boolean;
8
+ }
9
+ export type ComposerActionsSnapshot = Partial<Record<ComposerAction, ActionTarget>>;
10
+ export declare class ComposerActions {
11
+ private readonly seat;
12
+ private snapshot;
13
+ private listeners;
14
+ private marked;
15
+ private observer;
16
+ constructor(seat: HTMLElement);
17
+ readonly getSnapshot: () => Partial<Record<ComposerAction, ActionTarget>>;
18
+ readonly subscribe: (listener: () => void) => () => void;
19
+ private sync;
20
+ private target;
21
+ invoke(action: ComposerAction): boolean;
22
+ dispose(): void;
23
+ }
24
+ //# sourceMappingURL=composerActions.d.ts.map
@@ -0,0 +1,66 @@
1
+ export class ComposerActions {
2
+ seat;
3
+ snapshot = {};
4
+ listeners = new Set();
5
+ marked = new Set();
6
+ observer;
7
+ constructor(seat) {
8
+ this.seat = seat;
9
+ this.observer = new MutationObserver(() => this.sync());
10
+ const card = seat.closest('[data-composer-card]');
11
+ if (card)
12
+ this.observer.observe(card, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['disabled', 'aria-label'] });
13
+ this.sync();
14
+ }
15
+ getSnapshot = () => this.snapshot;
16
+ subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
17
+ sync() {
18
+ const card = this.seat.closest('[data-composer-card]');
19
+ const file = card?.querySelector(':scope > div:last-child > div:first-child > input[type=file][hidden]');
20
+ const attachments = file?.previousElementSibling;
21
+ const commands = attachments?.previousElementSibling;
22
+ const next = {};
23
+ if (commands instanceof HTMLButtonElement && commands.getAttribute('aria-haspopup') === 'listbox' && attachments instanceof HTMLButtonElement) {
24
+ next.commands = this.target(commands);
25
+ next.attachments = this.target(attachments);
26
+ const modes = file?.nextElementSibling;
27
+ const permission = modes?.querySelector(':scope > span > button[aria-label]');
28
+ // PermissionSelect has a text label between optional glyph and chevron spans.
29
+ if (permission?.querySelector(':scope > span:not([aria-hidden])') && permission.querySelector(':scope > span[aria-hidden] svg'))
30
+ next.permissions = this.target(permission);
31
+ }
32
+ for (const button of this.marked)
33
+ if (!Object.values(next).some(value => value.button === button)) {
34
+ button.removeAttribute('data-mobile-folded-action');
35
+ this.marked.delete(button);
36
+ }
37
+ for (const value of Object.values(next))
38
+ if (!this.marked.has(value.button)) {
39
+ value.button.setAttribute('data-mobile-folded-action', '');
40
+ this.marked.add(value.button);
41
+ }
42
+ if (['commands', 'attachments', 'permissions'].some(key => next[key]?.button !== this.snapshot[key]?.button || next[key]?.label !== this.snapshot[key]?.label || next[key]?.disabled !== this.snapshot[key]?.disabled)) {
43
+ this.snapshot = next;
44
+ for (const listener of this.listeners)
45
+ listener();
46
+ }
47
+ }
48
+ target(button) {
49
+ return { button, label: button.textContent?.trim() || button.getAttribute('aria-label') || '', disabled: button.disabled };
50
+ }
51
+ invoke(action) {
52
+ this.sync();
53
+ const target = this.snapshot[action];
54
+ if (!target || target.disabled || !target.button.isConnected)
55
+ return false;
56
+ // The official command list is a combobox whose keyboard handling stays
57
+ // with Lexical. Only an explicit command-menu choice may focus the editor.
58
+ if (action === 'commands')
59
+ this.seat.closest('[data-composer-card]')?.querySelector('[contenteditable="true"]')?.focus({ preventScroll: true });
60
+ // Synchronous with the user's click, including the native file-picker gesture.
61
+ target.button.click();
62
+ return true;
63
+ }
64
+ dispose() { this.observer.disconnect(); for (const button of this.marked)
65
+ button.removeAttribute('data-mobile-folded-action'); this.marked.clear(); this.listeners.clear(); }
66
+ }
@@ -0,0 +1,15 @@
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 declare class ComposerFocus {
4
+ private readonly doc;
5
+ private readonly active;
6
+ private waiting;
7
+ constructor(doc: Document, active: () => boolean);
8
+ readonly arm: () => void;
9
+ private isEditor;
10
+ private readonly focus;
11
+ private readonly pointer;
12
+ private readonly key;
13
+ dispose(): void;
14
+ }
15
+ //# sourceMappingURL=composerFocus.d.ts.map