@linxin666/dsh-remote-web-ui 0.3.6 → 0.3.9
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.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17658
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -149
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -561
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -63
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -821
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -236
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -685
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -268
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1342
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Landing level: the workspace roster. The mobile surface opens straight
|
|
3
|
-
* here — no new-session homepage — and every workspace row is a thin
|
|
4
|
-
* fetch from workspace.list (the roster is small; sessions are not loaded
|
|
5
|
-
* until a workspace is opened).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useEffect, useState } from 'react'
|
|
9
|
-
import type { WorkspaceView as WorkspaceRow } from '@deepseek-ai/dsh-host-apiproxy/api/workspace'
|
|
10
|
-
import { listWorkspaces, listDirectory, createWorkspace, type DirectoryListing, type DirectoryEntry } from '../api.ts'
|
|
11
|
-
import { errorText } from './App.tsx'
|
|
12
|
-
import { ThemeToggle } from '../theme-toggle.tsx'
|
|
13
|
-
|
|
14
|
-
/** Props for the directory browser. */
|
|
15
|
-
interface DirectoryBrowserProps {
|
|
16
|
-
onCancel(): void
|
|
17
|
-
onPick(workspace: WorkspaceRow): void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Render the directory browser for creating a new workspace.
|
|
22
|
-
*/
|
|
23
|
-
function DirectoryBrowser({ onCancel, onPick }: DirectoryBrowserProps) {
|
|
24
|
-
const [listing, setListing] = useState<DirectoryListing | undefined>(undefined)
|
|
25
|
-
const [error, setError] = useState<string | undefined>(undefined)
|
|
26
|
-
const [creating, setCreating] = useState(false)
|
|
27
|
-
const [path, setPath] = useState<string | undefined>(undefined)
|
|
28
|
-
const [reload, setReload] = useState(0)
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
let cancelled = false
|
|
32
|
-
setError(undefined)
|
|
33
|
-
setListing(undefined)
|
|
34
|
-
void listDirectory(path).then(
|
|
35
|
-
(res) => {
|
|
36
|
-
if (cancelled) return
|
|
37
|
-
setListing(res)
|
|
38
|
-
},
|
|
39
|
-
(reason: unknown) => {
|
|
40
|
-
if (cancelled) return
|
|
41
|
-
setError(errorText(reason))
|
|
42
|
-
},
|
|
43
|
-
)
|
|
44
|
-
return () => { cancelled = true }
|
|
45
|
-
}, [path, reload])
|
|
46
|
-
|
|
47
|
-
const handleCreate = async () => {
|
|
48
|
-
if (listing === undefined || creating) return
|
|
49
|
-
setCreating(true)
|
|
50
|
-
setError(undefined)
|
|
51
|
-
try {
|
|
52
|
-
const result = await createWorkspace(listing.path)
|
|
53
|
-
onPick(result.workspace)
|
|
54
|
-
} catch (reason: unknown) {
|
|
55
|
-
setCreating(false)
|
|
56
|
-
setError(errorText(reason))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div className="mobile dir-browser">
|
|
62
|
-
<header className="mobile-header">
|
|
63
|
-
<button type="button" className="mobile-headerAction" onClick={onCancel}>返回</button>
|
|
64
|
-
<h1 className="mobile-title">选择目录</h1>
|
|
65
|
-
</header>
|
|
66
|
-
|
|
67
|
-
{listing !== undefined && (
|
|
68
|
-
<div className="dir-crumbs">
|
|
69
|
-
{listing.crumbs.map((crumb, idx) => (
|
|
70
|
-
<span key={crumb.path}>
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
className="dir-crumb"
|
|
74
|
-
onClick={() => setPath(crumb.path)}
|
|
75
|
-
>
|
|
76
|
-
{crumb.name || '/'}
|
|
77
|
-
</button>
|
|
78
|
-
{idx < listing.crumbs.length - 1 && <span className="dir-crumb-separator">/</span>}
|
|
79
|
-
</span>
|
|
80
|
-
))}
|
|
81
|
-
</div>
|
|
82
|
-
)}
|
|
83
|
-
|
|
84
|
-
{error !== undefined ? (
|
|
85
|
-
<div className="mobile-empty">
|
|
86
|
-
<p className="mobile-error">{error}</p>
|
|
87
|
-
<button type="button" className="mobile-button" onClick={() => setReload(n => n + 1)}>重试</button>
|
|
88
|
-
</div>
|
|
89
|
-
) : listing === undefined ? (
|
|
90
|
-
<div className="mobile-empty">
|
|
91
|
-
<p className="mobile-muted">加载中…</p>
|
|
92
|
-
</div>
|
|
93
|
-
) : (
|
|
94
|
-
<ul className="mobile-list">
|
|
95
|
-
{listing.entries.length === 0 ? (
|
|
96
|
-
<div className="mobile-empty dir-empty">
|
|
97
|
-
<p className="mobile-muted">空目录</p>
|
|
98
|
-
</div>
|
|
99
|
-
) : (
|
|
100
|
-
listing.entries.map((entry: DirectoryEntry) => (
|
|
101
|
-
<li key={entry.path}>
|
|
102
|
-
<button
|
|
103
|
-
type="button"
|
|
104
|
-
className={`mobile-row dir-entry ${entry.hidden ? 'dir-entry-hidden' : ''}`}
|
|
105
|
-
onClick={() => setPath(entry.path)}
|
|
106
|
-
>
|
|
107
|
-
<span className="mobile-rowTitle">{entry.name}</span>
|
|
108
|
-
</button>
|
|
109
|
-
</li>
|
|
110
|
-
))
|
|
111
|
-
)}
|
|
112
|
-
</ul>
|
|
113
|
-
)}
|
|
114
|
-
|
|
115
|
-
<div className="dir-select">
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
className="mobile-button"
|
|
119
|
-
disabled={listing === undefined || creating}
|
|
120
|
-
onClick={() => void handleCreate()}
|
|
121
|
-
>
|
|
122
|
-
{creating ? '创建中…' : '选择此目录'}
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Props for the workspace roster. */
|
|
130
|
-
export interface WorkspaceViewProps {
|
|
131
|
-
/** Workspace carried by the pairing link; opened after the roster loads. */
|
|
132
|
-
initialWorkspaceId?: string
|
|
133
|
-
/** Open one workspace's session list. */
|
|
134
|
-
onPick(workspace: WorkspaceRow): void
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Render the workspace roster.
|
|
139
|
-
* @param props - the pick action.
|
|
140
|
-
* @returns the roster.
|
|
141
|
-
*/
|
|
142
|
-
export function WorkspaceView({ initialWorkspaceId, onPick }: WorkspaceViewProps) {
|
|
143
|
-
const [items, setItems] = useState<WorkspaceRow[] | undefined>(undefined)
|
|
144
|
-
const [error, setError] = useState<string | undefined>(undefined)
|
|
145
|
-
// Bumped by the retry button to re-run the roster fetch effect.
|
|
146
|
-
const [reload, setReload] = useState(0)
|
|
147
|
-
const [isCreating, setIsCreating] = useState(false)
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
let cancelled = false
|
|
151
|
-
void listWorkspaces().then(
|
|
152
|
-
(rows) => {
|
|
153
|
-
if (cancelled) return
|
|
154
|
-
const target = initialWorkspaceId === undefined
|
|
155
|
-
? undefined
|
|
156
|
-
: rows.find(workspace => workspace.workspaceId === initialWorkspaceId)
|
|
157
|
-
if (target !== undefined) {
|
|
158
|
-
onPick(target)
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
setItems(rows)
|
|
162
|
-
},
|
|
163
|
-
(reason: unknown) => {
|
|
164
|
-
if (cancelled) return
|
|
165
|
-
setError(errorText(reason))
|
|
166
|
-
},
|
|
167
|
-
)
|
|
168
|
-
return () => { cancelled = true }
|
|
169
|
-
}, [initialWorkspaceId, onPick, reload])
|
|
170
|
-
|
|
171
|
-
if (isCreating) {
|
|
172
|
-
return (
|
|
173
|
-
<DirectoryBrowser
|
|
174
|
-
onCancel={() => setIsCreating(false)}
|
|
175
|
-
onPick={onPick}
|
|
176
|
-
/>
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (error !== undefined) {
|
|
181
|
-
return (
|
|
182
|
-
<div className="mobile">
|
|
183
|
-
<header className="mobile-header">
|
|
184
|
-
<h1 className="mobile-title">工作区</h1>
|
|
185
|
-
<ThemeToggle />
|
|
186
|
-
</header>
|
|
187
|
-
<div className="mobile-empty">
|
|
188
|
-
<p className="mobile-error">加载失败:{error}</p>
|
|
189
|
-
<button type="button" className="mobile-button" onClick={() => { setError(undefined); setItems(undefined); setReload(n => n + 1) }}>
|
|
190
|
-
重试
|
|
191
|
-
</button>
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (items === undefined) {
|
|
198
|
-
return (
|
|
199
|
-
<div className="mobile">
|
|
200
|
-
<header className="mobile-header">
|
|
201
|
-
<h1 className="mobile-title">工作区</h1>
|
|
202
|
-
<ThemeToggle />
|
|
203
|
-
</header>
|
|
204
|
-
<div className="mobile-empty">
|
|
205
|
-
<p className="mobile-muted">加载中…</p>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return (
|
|
212
|
-
<div className="mobile">
|
|
213
|
-
<header className="mobile-header">
|
|
214
|
-
<h1 className="mobile-title">工作区</h1>
|
|
215
|
-
<ThemeToggle />
|
|
216
|
-
</header>
|
|
217
|
-
{items.length === 0 ? (
|
|
218
|
-
<div className="mobile-empty">
|
|
219
|
-
<p className="mobile-muted">暂无工作区</p>
|
|
220
|
-
</div>
|
|
221
|
-
) : (
|
|
222
|
-
<ul className="mobile-list">
|
|
223
|
-
{items.map(workspace => (
|
|
224
|
-
<li key={workspace.workspaceId}>
|
|
225
|
-
<button type="button" className="mobile-row" onClick={() => { onPick(workspace) }}>
|
|
226
|
-
<span className="mobile-rowTitle">{workspace.title}</span>
|
|
227
|
-
<span className="mobile-rowMeta">{workspace.path}</span>
|
|
228
|
-
<span className="mobile-chevron">›</span>
|
|
229
|
-
</button>
|
|
230
|
-
</li>
|
|
231
|
-
))}
|
|
232
|
-
</ul>
|
|
233
|
-
)}
|
|
234
|
-
<div style={{ padding: '16px' }}>
|
|
235
|
-
<button
|
|
236
|
-
type="button"
|
|
237
|
-
className="mobile-button"
|
|
238
|
-
onClick={() => setIsCreating(true)}
|
|
239
|
-
>
|
|
240
|
-
+ 新建工作区
|
|
241
|
-
</button>
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
)
|
|
245
|
-
}
|
package/src/mobile-api.ts
DELETED
|
@@ -1,418 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The mobile surface's data channel: `/m/api` proxies the host ApiProxy
|
|
3
|
-
* service for the standalone phone page. The phone's RPC calls ride THIS
|
|
4
|
-
* prefix instead of the connection plugin's `/api` — so the tunneled Host
|
|
5
|
-
* never needs to enter the connection trust fence (a distributable plugin
|
|
6
|
-
* cannot change that fence), and this plugin's own pairing gate is the
|
|
7
|
-
* access control instead.
|
|
8
|
-
*
|
|
9
|
-
* Security model:
|
|
10
|
-
* - Every request must carry a live paired-device cookie (the same gate
|
|
11
|
-
* semantic as the LAN fence), enforced before any host call.
|
|
12
|
-
* - Only an explicit allowlist of methods is proxied ON THIS PREFIX. The
|
|
13
|
-
* allowlist constrains the /m/api proxy alone: the same paired-device
|
|
14
|
-
* cookie also passes the global api/gate, so a paired device is a
|
|
15
|
-
* full-control credential for the host /api surface outside the SDK's
|
|
16
|
-
* loopback-pinned privileged set (settings/credentials/agentPreset/host
|
|
17
|
-
* actions/llm.discoverModels). Pairing is full device trust.
|
|
18
|
-
* - `session.list` is paged here (the host API returns everything; this
|
|
19
|
-
* layer slices stable pages) so the phone never transfers the whole list.
|
|
20
|
-
* - The live mux stream is bridged over Server-Sent Events on the same
|
|
21
|
-
* prefix (one-directional push; answers to questions/approvals ride the
|
|
22
|
-
* unary channel), gated identically.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
26
|
-
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
27
|
-
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy'
|
|
28
|
-
import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
29
|
-
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
|
|
30
|
-
import type { PendingTracker } from './mobile-pending.ts'
|
|
31
|
-
import type { PairingService } from './pairing.ts'
|
|
32
|
-
import { readBoundedJson, writeJson } from './http.ts'
|
|
33
|
-
import { readCookie } from './gate.ts'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Methods the phone surface may call. Everything else is refused HERE — but
|
|
37
|
-
* note the paired-device cookie also passes the global api/gate for the full
|
|
38
|
-
* ApiProxy surface (gate.ts), so a paired phone is a full-control credential:
|
|
39
|
-
* the allowlist only constrains this /m/api proxy, not the cookie's reach.
|
|
40
|
-
* stop() revokes every device; the loopback panel can also revoke one
|
|
41
|
-
* device at a time.
|
|
42
|
-
*/
|
|
43
|
-
const MOBILE_ALLOWLIST = new Set([
|
|
44
|
-
'host.listDirectory',
|
|
45
|
-
'workspace.create',
|
|
46
|
-
'workspace.list',
|
|
47
|
-
'agentPreset.list',
|
|
48
|
-
'session.create',
|
|
49
|
-
'session.list',
|
|
50
|
-
'session.history',
|
|
51
|
-
'session.search',
|
|
52
|
-
'session.prompt',
|
|
53
|
-
'session.models',
|
|
54
|
-
'session.selectModel',
|
|
55
|
-
'session.rename',
|
|
56
|
-
'session.cancel',
|
|
57
|
-
])
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Locally answered display-preference method (the phone's read-only
|
|
61
|
-
* surface preferences; never proxied to the host ApiProxy and never a
|
|
62
|
-
* settings-domain write).
|
|
63
|
-
*/
|
|
64
|
-
const MOBILE_PREFERENCES_METHOD = 'mobile.preferences'
|
|
65
|
-
const MOBILE_PENDING_METHOD = 'mobile.pending'
|
|
66
|
-
const MOBILE_RESPOND_METHOD = 'mobile.respond'
|
|
67
|
-
/** Execute a slash command through the host command registry (#1125). */
|
|
68
|
-
const MOBILE_COMMAND_METHOD = 'mobile.command'
|
|
69
|
-
|
|
70
|
-
/** One session.list page (thin phones load incrementally). */
|
|
71
|
-
const SESSION_PAGE_SIZE = 20
|
|
72
|
-
/** SSE keep-alive ping cadence for the live mux stream (single connection). */
|
|
73
|
-
const DEFAULT_EVENTS_HEARTBEAT_MS = 15_000
|
|
74
|
-
|
|
75
|
-
/** Encode one list position as an opaque continuation cursor. */
|
|
76
|
-
function sessionListCursor(updatedAt: number, sessionId: string): string {
|
|
77
|
-
return `${updatedAt}:${sessionId}`
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Parse a cursor; malformed cursors mean "start over" (safe failure mode). */
|
|
81
|
-
function parseSessionListCursor(cursor: string): { updatedAt: number; sessionId: string } | undefined {
|
|
82
|
-
const separator = cursor.indexOf(':')
|
|
83
|
-
if (separator < 0) return undefined
|
|
84
|
-
const updatedAt = Number(cursor.slice(0, separator))
|
|
85
|
-
if (!Number.isFinite(updatedAt)) return undefined
|
|
86
|
-
return { updatedAt, sessionId: cursor.slice(separator + 1) }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** Whether a row comes strictly after the cursor position. */
|
|
90
|
-
function afterCursor(row: { updatedAt: number; sessionId: string }, position: { updatedAt: number; sessionId: string }): boolean {
|
|
91
|
-
return row.updatedAt < position.updatedAt
|
|
92
|
-
|| (row.updatedAt === position.updatedAt && row.sessionId > position.sessionId)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Route-family dependencies. */
|
|
96
|
-
export interface MobileApiDeps {
|
|
97
|
-
/** The pairing service (device gate + cookie name). */
|
|
98
|
-
service: PairingService
|
|
99
|
-
/** The host ApiProxy service (injected by the plugin). */
|
|
100
|
-
apiProxy: ApiProxy
|
|
101
|
-
/** The pending tracker. */
|
|
102
|
-
pendingTracker: PendingTracker
|
|
103
|
-
/** The resolved mobile composer preference (live per request). */
|
|
104
|
-
mobileEnterToSend: () => boolean
|
|
105
|
-
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
106
|
-
eventsHeartbeatMs?: number
|
|
107
|
-
/**
|
|
108
|
-
* Optional command dispatcher for executing slash commands on the mobile
|
|
109
|
-
* surface. When absent, `mobile.command` returns an error; the mobile UI
|
|
110
|
-
* can still fall back to `session.prompt` (which sends the line to the
|
|
111
|
-
* model, not the command registry).
|
|
112
|
-
*/
|
|
113
|
-
commandDispatcher?: {
|
|
114
|
-
execute(
|
|
115
|
-
sessionId: string,
|
|
116
|
-
line: string,
|
|
117
|
-
signal: AbortSignal,
|
|
118
|
-
): Promise<{ ok: true; result: unknown } | { error: string }>
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Mobile API route paths. */
|
|
123
|
-
export const MOBILE_API_PATHS = {
|
|
124
|
-
events: '/m/api/events.mux',
|
|
125
|
-
} as const
|
|
126
|
-
|
|
127
|
-
/** The mobile-api prefix (every other path under it is a method name). */
|
|
128
|
-
const MOBILE_API_PREFIX = '/m/api'
|
|
129
|
-
/** Method extraction: the prefix plus one slash. */
|
|
130
|
-
const MOBILE_API_METHOD_PREFIX = `${MOBILE_API_PREFIX}/`
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Build the mobile data-channel routes.
|
|
134
|
-
* @param deps - pairing service + apiProxy.
|
|
135
|
-
* @returns the routes to register on webServer.
|
|
136
|
-
*/
|
|
137
|
-
export function makeMobileApiRoutes(deps: MobileApiDeps): WebRoute[] {
|
|
138
|
-
const { service, apiProxy, mobileEnterToSend } = deps
|
|
139
|
-
const eventsHeartbeatMs = deps.eventsHeartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Refresh the paired device's presence and report whether it is live.
|
|
143
|
-
* The mobile surface (unlike the desktop Web UI) has no `/api/pair/heartbeat`
|
|
144
|
-
* sender, so any activity on the mobile channel — a gated RPC, or the live
|
|
145
|
-
* SSE stream staying open — must count as presence. Without this, an
|
|
146
|
-
* idle-but-connected phone ages past `offlineAfterMs` and the desktop panel
|
|
147
|
-
* wrongly reports it as disconnected.
|
|
148
|
-
*/
|
|
149
|
-
const touchDeviceFor = (req: IncomingMessage): boolean => {
|
|
150
|
-
const deviceId = readCookie(req.headers.cookie, service.config.cookieName)
|
|
151
|
-
if (deviceId === undefined) return false
|
|
152
|
-
return service.touchDevice(deviceId)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** The phone gate: a live paired-device cookie, or nothing else proceeds. */
|
|
156
|
-
const gateOk = (req: IncomingMessage): boolean => {
|
|
157
|
-
return touchDeviceFor(req)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const handleMethod = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
161
|
-
if (req.method !== 'POST') {
|
|
162
|
-
res.writeHead(405)
|
|
163
|
-
res.end()
|
|
164
|
-
return
|
|
165
|
-
}
|
|
166
|
-
if (!gateOk(req)) {
|
|
167
|
-
writeJson(res, 403, { ok: false, error: { code: 'unpaired', message: 'mobile session is not paired' } })
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
const pathname = new URL(req.url ?? '/', 'http://x').pathname
|
|
171
|
-
if (!pathname.startsWith(MOBILE_API_METHOD_PREFIX)) {
|
|
172
|
-
writeJson(res, 404, { ok: false, error: { code: 'not-found', message: 'unknown mobile api path' } })
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length)
|
|
176
|
-
const local = method === MOBILE_PREFERENCES_METHOD
|
|
177
|
-
|| method === MOBILE_PENDING_METHOD
|
|
178
|
-
|| method === MOBILE_RESPOND_METHOD
|
|
179
|
-
|| method === MOBILE_COMMAND_METHOD
|
|
180
|
-
if (!MOBILE_ALLOWLIST.has(method) && !local) {
|
|
181
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } })
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
let envelope: unknown
|
|
185
|
-
try {
|
|
186
|
-
envelope = await readBoundedJson(req, 64 * 1024)
|
|
187
|
-
} catch {
|
|
188
|
-
writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'invalid json body' } })
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
const parsed = envelope as { rpcId?: unknown; payload?: unknown }
|
|
192
|
-
const rpcId = typeof parsed?.rpcId === 'string' ? parsed.rpcId : ''
|
|
193
|
-
if (rpcId === '') {
|
|
194
|
-
writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'missing rpcId' } })
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
if (local) {
|
|
198
|
-
if (method === MOBILE_PREFERENCES_METHOD) {
|
|
199
|
-
writeJson(res, 200, {
|
|
200
|
-
type: 'server-response',
|
|
201
|
-
rpcId,
|
|
202
|
-
result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
|
|
203
|
-
})
|
|
204
|
-
} else if (method === MOBILE_PENDING_METHOD) {
|
|
205
|
-
const payload = parsed.payload as any
|
|
206
|
-
writeJson(res, 200, {
|
|
207
|
-
type: 'server-response',
|
|
208
|
-
rpcId,
|
|
209
|
-
result: { ok: true, value: deps.pendingTracker.pending(payload?.sessionId) },
|
|
210
|
-
})
|
|
211
|
-
} else if (method === MOBILE_RESPOND_METHOD) {
|
|
212
|
-
const payload = parsed.payload as any
|
|
213
|
-
try {
|
|
214
|
-
const receipt = await apiProxy.respond({
|
|
215
|
-
type: 'client-response',
|
|
216
|
-
rpcId: RpcId(payload.rpcId),
|
|
217
|
-
result: { ok: true, value: payload.response },
|
|
218
|
-
})
|
|
219
|
-
writeJson(res, 200, {
|
|
220
|
-
type: 'server-response',
|
|
221
|
-
rpcId,
|
|
222
|
-
result: { ok: true, value: receipt },
|
|
223
|
-
})
|
|
224
|
-
} catch (error) {
|
|
225
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
226
|
-
writeJson(res, 200, {
|
|
227
|
-
type: 'server-response',
|
|
228
|
-
rpcId,
|
|
229
|
-
result: { ok: false, error: { code: 'internal', message } },
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
} else if (method === MOBILE_COMMAND_METHOD) {
|
|
233
|
-
const payload = parsed.payload as { sessionId?: string; line?: string } | undefined
|
|
234
|
-
const sessionId = payload?.sessionId
|
|
235
|
-
const line = payload?.line
|
|
236
|
-
if (typeof sessionId !== 'string' || typeof line !== 'string') {
|
|
237
|
-
writeJson(res, 200, {
|
|
238
|
-
type: 'server-response',
|
|
239
|
-
rpcId,
|
|
240
|
-
result: { ok: false, error: { code: 'bad-request', message: 'missing sessionId or line' } },
|
|
241
|
-
})
|
|
242
|
-
} else if (deps.commandDispatcher === undefined) {
|
|
243
|
-
writeJson(res, 200, {
|
|
244
|
-
type: 'server-response',
|
|
245
|
-
rpcId,
|
|
246
|
-
result: { ok: false, error: { code: 'unavailable', message: 'command dispatcher is not available' } },
|
|
247
|
-
})
|
|
248
|
-
} else {
|
|
249
|
-
try {
|
|
250
|
-
const abort = new AbortController()
|
|
251
|
-
res.on('close', () => { if (!res.writableEnded) abort.abort() })
|
|
252
|
-
const outcome = await deps.commandDispatcher.execute(sessionId, line, abort.signal)
|
|
253
|
-
if ('error' in outcome) {
|
|
254
|
-
writeJson(res, 200, {
|
|
255
|
-
type: 'server-response',
|
|
256
|
-
rpcId,
|
|
257
|
-
result: { ok: false, error: { code: outcome.error, message: outcome.error } },
|
|
258
|
-
})
|
|
259
|
-
} else {
|
|
260
|
-
writeJson(res, 200, {
|
|
261
|
-
type: 'server-response',
|
|
262
|
-
rpcId,
|
|
263
|
-
result: { ok: true, value: { matched: true, result: outcome.result } },
|
|
264
|
-
})
|
|
265
|
-
}
|
|
266
|
-
} catch (error) {
|
|
267
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
268
|
-
writeJson(res, 200, {
|
|
269
|
-
type: 'server-response',
|
|
270
|
-
rpcId,
|
|
271
|
-
result: { ok: false, error: { code: 'internal', message } },
|
|
272
|
-
})
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
// Cancel the host-side work when the phone goes away mid-call (the
|
|
280
|
-
// response stream closing before we answer means nobody is listening).
|
|
281
|
-
const abort = new AbortController()
|
|
282
|
-
res.on('close', () => { if (!res.writableEnded) abort.abort() })
|
|
283
|
-
const response = await dispatch(apiProxy, method, parsed?.payload, rpcId, abort.signal)
|
|
284
|
-
writeJson(res, 200, response)
|
|
285
|
-
} catch (error) {
|
|
286
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
287
|
-
writeJson(res, 200, {
|
|
288
|
-
type: 'server-response',
|
|
289
|
-
rpcId,
|
|
290
|
-
result: { ok: false, error: { code: 'internal', message } },
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** Bridge the host mux stream over SSE: one `data:` frame per mux frame. */
|
|
296
|
-
const handleEvents = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
|
|
297
|
-
if (req.method !== 'GET') {
|
|
298
|
-
res.writeHead(405)
|
|
299
|
-
res.end()
|
|
300
|
-
return
|
|
301
|
-
}
|
|
302
|
-
if (!gateOk(req)) {
|
|
303
|
-
writeJson(res, 403, {
|
|
304
|
-
ok: false,
|
|
305
|
-
error: { code: 'unpaired', message: 'mobile session is not paired' },
|
|
306
|
-
})
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
res.writeHead(200, {
|
|
310
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
311
|
-
'cache-control': 'no-cache',
|
|
312
|
-
connection: 'keep-alive',
|
|
313
|
-
})
|
|
314
|
-
const controller = new AbortController()
|
|
315
|
-
let closed = false
|
|
316
|
-
const heartbeat = setInterval(() => {
|
|
317
|
-
if (closed) return
|
|
318
|
-
// An open SSE stream proves the phone is still live even while the agent
|
|
319
|
-
// idles (no RPC traffic), so refresh presence alongside the transport
|
|
320
|
-
// keepalive — otherwise an idle phone drifts to "disconnected".
|
|
321
|
-
touchDeviceFor(req)
|
|
322
|
-
try {
|
|
323
|
-
res.write(': ping\n\n')
|
|
324
|
-
} catch {
|
|
325
|
-
// The write failed; the close handler tears the subscription down.
|
|
326
|
-
}
|
|
327
|
-
}, eventsHeartbeatMs)
|
|
328
|
-
const onClose = (): void => {
|
|
329
|
-
if (closed) return
|
|
330
|
-
closed = true
|
|
331
|
-
controller.abort()
|
|
332
|
-
clearInterval(heartbeat)
|
|
333
|
-
}
|
|
334
|
-
res.on('close', onClose)
|
|
335
|
-
req.on('close', onClose)
|
|
336
|
-
try {
|
|
337
|
-
const frames = apiProxy.events.mux({ rpcId: RpcId(`mobile-mux-${Date.now().toString(36)}`), payload: {} }, controller.signal)
|
|
338
|
-
for await (const frame of frames) {
|
|
339
|
-
if (closed) break
|
|
340
|
-
deps.pendingTracker.onFrame(frame as any)
|
|
341
|
-
res.write(`data: ${JSON.stringify(frame)}\n\n`)
|
|
342
|
-
}
|
|
343
|
-
} catch {
|
|
344
|
-
// The stream ended or errored; the EventSource reconnects.
|
|
345
|
-
} finally {
|
|
346
|
-
controller.abort()
|
|
347
|
-
clearInterval(heartbeat)
|
|
348
|
-
}
|
|
349
|
-
if (!closed) res.end()
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return [
|
|
353
|
-
{ kind: 'prefix', path: MOBILE_API_PREFIX, handler: handleMethod },
|
|
354
|
-
{ kind: 'exact', path: MOBILE_API_PATHS.events, handler: handleEvents },
|
|
355
|
-
]
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** Dispatch one allowlisted method through the host ApiProxy. */
|
|
359
|
-
async function dispatch(apiProxy: ApiProxy, method: string, payload: unknown, rpcId: string, signal?: AbortSignal): Promise<unknown> {
|
|
360
|
-
const request: RpcRequest<unknown> = { rpcId: RpcId(rpcId), payload }
|
|
361
|
-
if (method === 'session.list') {
|
|
362
|
-
const full = await apiProxy.sessions.list(request as never)
|
|
363
|
-
// The error path must carry the same 'server-response' envelope the
|
|
364
|
-
// success path builds, or the phone's callUnary throws a transport error
|
|
365
|
-
// and masks the real business error.
|
|
366
|
-
if (!full.result.ok) return { type: 'server-response' as const, rpcId, result: full.result }
|
|
367
|
-
const items = full.result.value.items as Array<{ updatedAt: number; sessionId: string }>
|
|
368
|
-
const cursor = (payload as { cursor?: string } | undefined)?.cursor
|
|
369
|
-
// Every call pages (the first call with no cursor IS the first page):
|
|
370
|
-
// the phone must never transfer the whole session list at once.
|
|
371
|
-
// One stable page over (updatedAt desc, sessionId asc); pages never skip
|
|
372
|
-
// or repeat a row while the list changes between calls.
|
|
373
|
-
items.sort((a, b) => b.updatedAt - a.updatedAt
|
|
374
|
-
|| (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0))
|
|
375
|
-
const position = cursor === undefined ? undefined : parseSessionListCursor(cursor)
|
|
376
|
-
const from = position === undefined ? 0 : items.findIndex(row => afterCursor(row, position))
|
|
377
|
-
const start = from < 0 ? items.length : from
|
|
378
|
-
const page = items.slice(start, start + SESSION_PAGE_SIZE)
|
|
379
|
-
const last = page[page.length - 1]
|
|
380
|
-
const nextCursor = last !== undefined && start + page.length < items.length
|
|
381
|
-
? sessionListCursor(last.updatedAt, last.sessionId)
|
|
382
|
-
: undefined
|
|
383
|
-
return {
|
|
384
|
-
type: 'server-response',
|
|
385
|
-
rpcId,
|
|
386
|
-
result: {
|
|
387
|
-
ok: true,
|
|
388
|
-
value: {
|
|
389
|
-
items: page,
|
|
390
|
-
hasMore: nextCursor !== undefined,
|
|
391
|
-
...(nextCursor !== undefined ? { nextCursor } : {}),
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// The ApiProxy unary methods resolve to the internal response shape
|
|
397
|
-
// ({ rpcId, result }) without the transport envelope the phone's callUnary
|
|
398
|
-
// requires — wrap every pass-through in the same 'server-response'
|
|
399
|
-
// envelope session.list builds above.
|
|
400
|
-
const wrap = (response: { rpcId: string; result: unknown }): unknown => ({
|
|
401
|
-
type: 'server-response' as const,
|
|
402
|
-
rpcId,
|
|
403
|
-
result: response.result,
|
|
404
|
-
})
|
|
405
|
-
if (method === 'workspace.list') return wrap(await apiProxy.workspace.list(request as never))
|
|
406
|
-
if (method === 'workspace.create') return wrap(await apiProxy.workspace.create(request as never))
|
|
407
|
-
if (method === 'host.listDirectory') return wrap(await apiProxy.host.listDirectory(request as never, signal ?? new AbortController().signal))
|
|
408
|
-
if (method === 'agentPreset.list') return wrap(await apiProxy.agentPresets.list(request as never))
|
|
409
|
-
if (method === 'session.create') return wrap(await apiProxy.sessions.create(request as never))
|
|
410
|
-
if (method === 'session.history') return wrap(await apiProxy.sessions.history(request as never))
|
|
411
|
-
if (method === 'session.search') return wrap(await apiProxy.sessions.search(request as never, signal ?? new AbortController().signal))
|
|
412
|
-
if (method === 'session.prompt') return wrap(await apiProxy.sessions.prompt(request as never))
|
|
413
|
-
if (method === 'session.models') return wrap(await apiProxy.sessions.models(request as never))
|
|
414
|
-
if (method === 'session.selectModel') return wrap(await apiProxy.sessions.selectModel(request as never))
|
|
415
|
-
if (method === 'session.rename') return wrap(await apiProxy.sessions.rename(request as never))
|
|
416
|
-
if (method === 'session.cancel') return wrap(await apiProxy.sessions.cancel(request as never))
|
|
417
|
-
throw new Error(`unhandled allowlisted method ${method}`)
|
|
418
|
-
}
|