@linxin666/dsh-remote-web-ui 0.2.9 → 0.3.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 (83) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +41 -8
  3. package/README.zh.md +7 -2
  4. package/lib/client.js +65 -34
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +645 -61
  7. package/lib/mobile.js +874 -64
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/index.d.ts.map +1 -1
  10. package/lib/types/client/index.js +35 -5
  11. package/lib/types/client/remote-channel.d.ts +4 -5
  12. package/lib/types/client/remote-channel.d.ts.map +1 -1
  13. package/lib/types/client/remote-channel.js +13 -28
  14. package/lib/types/http.d.ts +37 -8
  15. package/lib/types/http.d.ts.map +1 -1
  16. package/lib/types/http.js +76 -11
  17. package/lib/types/index.d.ts.map +1 -1
  18. package/lib/types/index.js +25 -1
  19. package/lib/types/mobile/api.d.ts +57 -0
  20. package/lib/types/mobile/api.d.ts.map +1 -1
  21. package/lib/types/mobile/api.js +29 -0
  22. package/lib/types/mobile/markdown.d.ts +7 -1
  23. package/lib/types/mobile/markdown.d.ts.map +1 -1
  24. package/lib/types/mobile/markdown.js +16 -7
  25. package/lib/types/mobile/markdown.test.js +8 -0
  26. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  27. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  28. package/lib/types/mobile/mobile-styles.js +325 -6
  29. package/lib/types/mobile/views/ChatView.d.ts +10 -0
  30. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  31. package/lib/types/mobile/views/ChatView.js +205 -8
  32. package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
  33. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
  34. package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
  35. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  36. package/lib/types/mobile/views/WorkspaceView.js +46 -5
  37. package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
  38. package/lib/types/mobile-api.d.ts +9 -3
  39. package/lib/types/mobile-api.d.ts.map +1 -1
  40. package/lib/types/mobile-api.js +61 -16
  41. package/lib/types/mobile-pending.d.ts +38 -0
  42. package/lib/types/mobile-pending.d.ts.map +1 -0
  43. package/lib/types/mobile-pending.js +59 -0
  44. package/lib/types/paired-model-catalog.d.ts +17 -0
  45. package/lib/types/paired-model-catalog.d.ts.map +1 -0
  46. package/lib/types/paired-model-catalog.js +252 -0
  47. package/lib/types/pairing.d.ts +13 -4
  48. package/lib/types/pairing.d.ts.map +1 -1
  49. package/lib/types/pairing.js +13 -5
  50. package/lib/types/remote-api.d.ts.map +1 -1
  51. package/lib/types/remote-channel-boot.d.ts +26 -0
  52. package/lib/types/remote-channel-boot.d.ts.map +1 -0
  53. package/lib/types/remote-channel-boot.js +128 -0
  54. package/lib/types/remote-channel-rules.d.ts +40 -0
  55. package/lib/types/remote-channel-rules.d.ts.map +1 -0
  56. package/lib/types/remote-channel-rules.js +32 -0
  57. package/lib/types/routes.d.ts.map +1 -1
  58. package/lib/types/routes.js +12 -23
  59. package/lib/types/uuid-polyfill.d.ts +10 -0
  60. package/lib/types/uuid-polyfill.d.ts.map +1 -0
  61. package/lib/types/uuid-polyfill.js +27 -0
  62. package/package.json +1 -1
  63. package/src/client/index.ts +41 -5
  64. package/src/client/remote-channel.ts +18 -28
  65. package/src/http.ts +87 -12
  66. package/src/index.ts +27 -2
  67. package/src/mobile/api.ts +91 -0
  68. package/src/mobile/markdown.test.ts +9 -0
  69. package/src/mobile/markdown.ts +17 -7
  70. package/src/mobile/mobile-styles.ts +326 -6
  71. package/src/mobile/views/ChatView.stream.test.tsx +231 -0
  72. package/src/mobile/views/ChatView.tsx +328 -10
  73. package/src/mobile/views/WorkspaceView.test.tsx +163 -2
  74. package/src/mobile/views/WorkspaceView.tsx +152 -26
  75. package/src/mobile-api.ts +59 -17
  76. package/src/mobile-pending.ts +88 -0
  77. package/src/paired-model-catalog.ts +224 -0
  78. package/src/pairing.ts +13 -5
  79. package/src/remote-api.ts +0 -1
  80. package/src/remote-channel-boot.ts +131 -0
  81. package/src/remote-channel-rules.ts +62 -0
  82. package/src/routes.ts +13 -24
  83. package/src/uuid-polyfill.ts +28 -0
@@ -2,14 +2,18 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  // @vitest-environment jsdom
3
3
  /** Mobile workspace landing: roster rendering and QR deep-link selection. */
4
4
  import { afterEach, describe, expect, it, vi } from 'vitest';
5
- import { cleanup, render, screen, waitFor } from '@testing-library/react';
5
+ import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
6
6
  import { mobileWorkspaceTarget } from "./App.js";
7
7
  import { WorkspaceView } from "./WorkspaceView.js";
8
8
  vi.mock('../api.ts', () => ({
9
9
  listWorkspaces: vi.fn(),
10
+ listDirectory: vi.fn(),
11
+ createWorkspace: vi.fn(),
10
12
  }));
11
- import { listWorkspaces } from "../api.js";
13
+ import { listWorkspaces, listDirectory, createWorkspace } from "../api.js";
12
14
  const listWorkspacesMock = vi.mocked(listWorkspaces);
15
+ const listDirectoryMock = vi.mocked(listDirectory);
16
+ const createWorkspaceMock = vi.mocked(createWorkspace);
13
17
  const workspaces = [
14
18
  {
15
19
  workspaceId: 'ws-1',
@@ -54,3 +58,126 @@ describe('mobile workspace deep link', () => {
54
58
  expect(onPick).not.toHaveBeenCalled();
55
59
  });
56
60
  });
61
+ describe('mobile workspace creation', () => {
62
+ it('shows new workspace button and navigates to directory browser', async () => {
63
+ listWorkspacesMock.mockResolvedValue(workspaces);
64
+ listDirectoryMock.mockResolvedValue({
65
+ path: '/home/user',
66
+ home: '/home/user',
67
+ crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }, { name: 'user', path: '/home/user', hidden: false }],
68
+ entries: [
69
+ { name: 'projects', path: '/home/user/projects', hidden: false },
70
+ { name: '.config', path: '/home/user/.config', hidden: true }
71
+ ],
72
+ truncated: false
73
+ });
74
+ const onPick = vi.fn();
75
+ render(_jsx(WorkspaceView, { onPick: onPick }));
76
+ // Wait for workspaces to load
77
+ const createBtn = await screen.findByText('+ 新建工作区');
78
+ // Click create button
79
+ fireEvent.click(createBtn);
80
+ // Verify directory browser renders
81
+ expect(await screen.findByText('选择目录')).toBeTruthy();
82
+ // Verify crumbs
83
+ expect(await screen.findByText('user')).toBeTruthy();
84
+ // Verify entries
85
+ expect(await screen.findByText('📁 projects')).toBeTruthy();
86
+ expect(await screen.findByText('📁 .config')).toBeTruthy();
87
+ });
88
+ it('navigates into directory and allows breadcrumb navigation', async () => {
89
+ listWorkspacesMock.mockResolvedValue(workspaces);
90
+ // Initial dir
91
+ listDirectoryMock.mockResolvedValueOnce({
92
+ path: '/home/user',
93
+ home: '/home/user',
94
+ crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }, { name: 'user', path: '/home/user', hidden: false }],
95
+ entries: [{ name: 'projects', path: '/home/user/projects', hidden: false }],
96
+ truncated: false
97
+ });
98
+ const onPick = vi.fn();
99
+ render(_jsx(WorkspaceView, { onPick: onPick }));
100
+ // Open dir browser
101
+ fireEvent.click(await screen.findByText('+ 新建工作区'));
102
+ // Wait for first dir listing
103
+ const projBtn = await screen.findByText('📁 projects');
104
+ // Prepare next listing
105
+ listDirectoryMock.mockResolvedValueOnce({
106
+ path: '/home/user/projects',
107
+ home: '/home/user',
108
+ crumbs: [
109
+ { name: '', path: '/', hidden: false },
110
+ { name: 'home', path: '/home', hidden: false },
111
+ { name: 'user', path: '/home/user', hidden: false },
112
+ { name: 'projects', path: '/home/user/projects', hidden: false }
113
+ ],
114
+ entries: [{ name: 'foo', path: '/home/user/projects/foo', hidden: false }],
115
+ truncated: false
116
+ });
117
+ // Click into folder
118
+ fireEvent.click(projBtn);
119
+ // Verify new contents load
120
+ expect(await screen.findByText('📁 foo')).toBeTruthy();
121
+ // Check breadcrumb
122
+ const homeCrumb = screen.getByText('home');
123
+ expect(homeCrumb).toBeTruthy();
124
+ // Click breadcrumb
125
+ listDirectoryMock.mockResolvedValueOnce({
126
+ path: '/home',
127
+ home: '/home/user',
128
+ crumbs: [{ name: '', path: '/', hidden: false }, { name: 'home', path: '/home', hidden: false }],
129
+ entries: [{ name: 'user', path: '/home/user', hidden: false }],
130
+ truncated: false
131
+ });
132
+ fireEvent.click(homeCrumb);
133
+ expect(await screen.findByText('📁 user')).toBeTruthy();
134
+ });
135
+ it('creates workspace successfully', async () => {
136
+ listWorkspacesMock.mockResolvedValue(workspaces);
137
+ listDirectoryMock.mockResolvedValue({
138
+ path: '/home/user/projects/foo',
139
+ home: '/home/user',
140
+ crumbs: [{ name: '', path: '/', hidden: false }, { name: 'foo', path: '/home/user/projects/foo', hidden: false }],
141
+ entries: [],
142
+ truncated: false
143
+ });
144
+ const newWorkspace = { ...workspaces[0], workspaceId: 'ws-new', title: 'foo' };
145
+ createWorkspaceMock.mockResolvedValue({
146
+ workspace: newWorkspace,
147
+ created: true
148
+ });
149
+ const onPick = vi.fn();
150
+ render(_jsx(WorkspaceView, { onPick: onPick }));
151
+ fireEvent.click(await screen.findByText('+ 新建工作区'));
152
+ const selectBtn = await screen.findByText('选择此目录');
153
+ fireEvent.click(selectBtn);
154
+ expect(await screen.findByText('创建中…')).toBeTruthy();
155
+ await waitFor(() => {
156
+ expect(createWorkspaceMock).toHaveBeenCalledWith('/home/user/projects/foo');
157
+ expect(onPick).toHaveBeenCalledWith(newWorkspace);
158
+ });
159
+ });
160
+ it('handles errors from directory listing and workspace creation', async () => {
161
+ listWorkspacesMock.mockResolvedValue(workspaces);
162
+ listDirectoryMock.mockRejectedValue(new Error('Permission denied'));
163
+ const onPick = vi.fn();
164
+ render(_jsx(WorkspaceView, { onPick: onPick }));
165
+ fireEvent.click(await screen.findByText('+ 新建工作区'));
166
+ // Verify error from listing
167
+ expect(await screen.findByText('Permission denied')).toBeTruthy();
168
+ // Retry with success
169
+ listDirectoryMock.mockResolvedValue({
170
+ path: '/home',
171
+ home: '/home/user',
172
+ crumbs: [],
173
+ entries: [],
174
+ truncated: false
175
+ });
176
+ fireEvent.click(screen.getByText('重试'));
177
+ const selectBtn = await screen.findByText('选择此目录');
178
+ // Fail creation
179
+ createWorkspaceMock.mockRejectedValue(new Error('Already exists'));
180
+ fireEvent.click(selectBtn);
181
+ expect(await screen.findByText('Already exists')).toBeTruthy();
182
+ });
183
+ });
@@ -9,9 +9,12 @@
9
9
  * Security model:
10
10
  * - Every request must carry a live paired-device cookie (the same gate
11
11
  * semantic as the LAN fence), enforced before any host call.
12
- * - Only an explicit allowlist of methods is proxied; privileged domains
13
- * (settings, credentials, host actions, goals, subagents, …) are never
14
- * reachable from the phone.
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.
15
18
  * - `session.list` is paged here (the host API returns everything; this
16
19
  * layer slices stable pages) so the phone never transfers the whole list.
17
20
  * - The live mux stream is bridged over Server-Sent Events on the same
@@ -20,6 +23,7 @@
20
23
  */
21
24
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
22
25
  import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
26
+ import type { PendingTracker } from './mobile-pending.ts';
23
27
  import type { PairingService } from './pairing.ts';
24
28
  /** Route-family dependencies. */
25
29
  export interface MobileApiDeps {
@@ -27,6 +31,8 @@ export interface MobileApiDeps {
27
31
  service: PairingService;
28
32
  /** The host ApiProxy service (injected by the plugin). */
29
33
  apiProxy: ApiProxy;
34
+ /** The pending tracker. */
35
+ pendingTracker: PendingTracker;
30
36
  /** The resolved mobile composer preference (live per request). */
31
37
  mobileEnterToSend: () => boolean;
32
38
  /** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
@@ -1 +1 @@
1
- {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAyDlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA4InE"}
1
+ {"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA6DlD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,2BAA2B;IAC3B,cAAc,EAAE,cAAc,CAAA;IAC9B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA+KnE"}
@@ -9,9 +9,12 @@
9
9
  * Security model:
10
10
  * - Every request must carry a live paired-device cookie (the same gate
11
11
  * semantic as the LAN fence), enforced before any host call.
12
- * - Only an explicit allowlist of methods is proxied; privileged domains
13
- * (settings, credentials, host actions, goals, subagents, …) are never
14
- * reachable from the phone.
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.
15
18
  * - `session.list` is paged here (the host API returns everything; this
16
19
  * layer slices stable pages) so the phone never transfers the whole list.
17
20
  * - The live mux stream is bridged over Server-Sent Events on the same
@@ -30,6 +33,8 @@ import { readCookie } from "./gate.js";
30
33
  * device at a time.
31
34
  */
32
35
  const MOBILE_ALLOWLIST = new Set([
36
+ 'host.listDirectory',
37
+ 'workspace.create',
33
38
  'workspace.list',
34
39
  'agentPreset.list',
35
40
  'session.create',
@@ -47,6 +52,8 @@ const MOBILE_ALLOWLIST = new Set([
47
52
  * settings-domain write).
48
53
  */
49
54
  const MOBILE_PREFERENCES_METHOD = 'mobile.preferences';
55
+ const MOBILE_PENDING_METHOD = 'mobile.pending';
56
+ const MOBILE_RESPOND_METHOD = 'mobile.respond';
50
57
  /** One session.list page (thin phones load incrementally). */
51
58
  const SESSION_PAGE_SIZE = 20;
52
59
  /** SSE keep-alive ping cadence for the live mux stream (single connection). */
@@ -120,14 +127,16 @@ export function makeMobileApiRoutes(deps) {
120
127
  return;
121
128
  }
122
129
  const method = pathname.slice(MOBILE_API_METHOD_PREFIX.length);
123
- const local = method === MOBILE_PREFERENCES_METHOD;
130
+ const local = method === MOBILE_PREFERENCES_METHOD
131
+ || method === MOBILE_PENDING_METHOD
132
+ || method === MOBILE_RESPOND_METHOD;
124
133
  if (!MOBILE_ALLOWLIST.has(method) && !local) {
125
134
  writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: `method ${method} is not exposed to the mobile surface` } });
126
135
  return;
127
136
  }
128
137
  let envelope;
129
138
  try {
130
- envelope = await readJsonBody(req);
139
+ envelope = await readBoundedJson(req, 64 * 1024);
131
140
  }
132
141
  catch {
133
142
  writeJson(res, 400, { ok: false, error: { code: 'bad-request', message: 'invalid json body' } });
@@ -140,11 +149,44 @@ export function makeMobileApiRoutes(deps) {
140
149
  return;
141
150
  }
142
151
  if (local) {
143
- writeJson(res, 200, {
144
- type: 'server-response',
145
- rpcId,
146
- result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
147
- });
152
+ if (method === MOBILE_PREFERENCES_METHOD) {
153
+ writeJson(res, 200, {
154
+ type: 'server-response',
155
+ rpcId,
156
+ result: { ok: true, value: { mobileEnterToSend: mobileEnterToSend() } },
157
+ });
158
+ }
159
+ else if (method === MOBILE_PENDING_METHOD) {
160
+ const payload = parsed.payload;
161
+ writeJson(res, 200, {
162
+ type: 'server-response',
163
+ rpcId,
164
+ result: { ok: true, value: deps.pendingTracker.pending(payload?.sessionId) },
165
+ });
166
+ }
167
+ else if (method === MOBILE_RESPOND_METHOD) {
168
+ const payload = parsed.payload;
169
+ try {
170
+ const receipt = await apiProxy.respond({
171
+ type: 'client-response',
172
+ rpcId: RpcId(payload.rpcId),
173
+ result: { ok: true, value: payload.response },
174
+ });
175
+ writeJson(res, 200, {
176
+ type: 'server-response',
177
+ rpcId,
178
+ result: { ok: true, value: receipt },
179
+ });
180
+ }
181
+ catch (error) {
182
+ const message = error instanceof Error ? error.message : String(error);
183
+ writeJson(res, 200, {
184
+ type: 'server-response',
185
+ rpcId,
186
+ result: { ok: false, error: { code: 'internal', message } },
187
+ });
188
+ }
189
+ }
148
190
  return;
149
191
  }
150
192
  try {
@@ -173,8 +215,10 @@ export function makeMobileApiRoutes(deps) {
173
215
  return;
174
216
  }
175
217
  if (!gateOk(req)) {
176
- res.writeHead(403);
177
- res.end('forbidden');
218
+ writeJson(res, 403, {
219
+ ok: false,
220
+ error: { code: 'unpaired', message: 'mobile session is not paired' },
221
+ });
178
222
  return;
179
223
  }
180
224
  res.writeHead(200, {
@@ -212,6 +256,7 @@ export function makeMobileApiRoutes(deps) {
212
256
  for await (const frame of frames) {
213
257
  if (closed)
214
258
  break;
259
+ deps.pendingTracker.onFrame(frame);
215
260
  res.write(`data: ${JSON.stringify(frame)}\n\n`);
216
261
  }
217
262
  }
@@ -230,10 +275,6 @@ export function makeMobileApiRoutes(deps) {
230
275
  { kind: 'exact', path: MOBILE_API_PATHS.events, handler: handleEvents },
231
276
  ];
232
277
  }
233
- /** Read a request body as JSON (bounded). */
234
- async function readJsonBody(req) {
235
- return readBoundedJson(req, 64 * 1024);
236
- }
237
278
  /** Dispatch one allowlisted method through the host ApiProxy. */
238
279
  async function dispatch(apiProxy, method, payload, rpcId, signal) {
239
280
  const request = { rpcId: RpcId(rpcId), payload };
@@ -284,6 +325,10 @@ async function dispatch(apiProxy, method, payload, rpcId, signal) {
284
325
  });
285
326
  if (method === 'workspace.list')
286
327
  return wrap(await apiProxy.workspace.list(request));
328
+ if (method === 'workspace.create')
329
+ return wrap(await apiProxy.workspace.create(request));
330
+ if (method === 'host.listDirectory')
331
+ return wrap(await apiProxy.host.listDirectory(request, signal ?? new AbortController().signal));
287
332
  if (method === 'agentPreset.list')
288
333
  return wrap(await apiProxy.agentPresets.list(request));
289
334
  if (method === 'session.create')
@@ -0,0 +1,38 @@
1
+ import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events';
2
+ import type { RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api/rpc';
3
+ export interface PendingApproval {
4
+ rpcId: string;
5
+ approvalId: string;
6
+ toolName: string;
7
+ callId?: string;
8
+ reason?: string;
9
+ }
10
+ export interface PendingQuestion {
11
+ rpcId: string;
12
+ questions: Array<{
13
+ id: string;
14
+ question: string;
15
+ detail?: string;
16
+ header?: string;
17
+ options?: Array<{
18
+ label: string;
19
+ description?: string;
20
+ }>;
21
+ multiSelect?: boolean;
22
+ }>;
23
+ }
24
+ export interface PendingState {
25
+ approvals: PendingApproval[];
26
+ questions: PendingQuestion[];
27
+ }
28
+ export declare class PendingTracker {
29
+ private readonly sessions;
30
+ /** Process one mux frame and update the pending state. */
31
+ onFrame(frame: RpcRequest<MuxFrame>): void;
32
+ private _getOrInit;
33
+ /** Query pending items for a session. */
34
+ pending(sessionId: string): PendingState;
35
+ /** Clear all pending state for a session. */
36
+ clear(sessionId: string): void;
37
+ }
38
+ //# sourceMappingURL=mobile-pending.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-pending.d.ts","sourceRoot":"","sources":["../../src/mobile-pending.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2CAA2C,CAAA;AACzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wCAAwC,CAAA;AAExE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,KAAK,CAAC;QACf,EAAE,EAAE,MAAM,CAAA;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACxD,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,CAAC,CAAA;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,eAAe,EAAE,CAAA;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAA;CAC7B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+F;IAExH,0DAA0D;IAC1D,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI;IA8B1C,OAAO,CAAC,UAAU;IASlB,yCAAyC;IACzC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY;IAWxC,6CAA6C;IAC7C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAG/B"}
@@ -0,0 +1,59 @@
1
+ export class PendingTracker {
2
+ sessions = new Map();
3
+ /** Process one mux frame and update the pending state. */
4
+ onFrame(frame) {
5
+ const payload = frame.payload;
6
+ if (payload.type === 'approval/requested') {
7
+ const state = this._getOrInit(payload.sessionId);
8
+ state.approvals.set(payload.approvalId, {
9
+ rpcId: frame.rpcId,
10
+ approvalId: payload.approvalId,
11
+ toolName: payload.toolName,
12
+ callId: payload.callId,
13
+ reason: payload.reason,
14
+ });
15
+ }
16
+ else if (payload.type === 'approval/resolved') {
17
+ const state = this.sessions.get(payload.sessionId);
18
+ if (state) {
19
+ state.approvals.delete(payload.approvalId);
20
+ }
21
+ }
22
+ else if (payload.type === 'question/requested') {
23
+ const state = this._getOrInit(payload.sessionId);
24
+ state.questions.push({
25
+ rpcId: frame.rpcId,
26
+ questions: payload.questions,
27
+ });
28
+ }
29
+ else if (payload.type === 'question/resolved') {
30
+ const state = this.sessions.get(payload.sessionId);
31
+ if (state) {
32
+ state.questions = state.questions.filter(q => q.rpcId !== payload.questionRpcId);
33
+ }
34
+ }
35
+ }
36
+ _getOrInit(sessionId) {
37
+ let state = this.sessions.get(sessionId);
38
+ if (!state) {
39
+ state = { approvals: new Map(), questions: [] };
40
+ this.sessions.set(sessionId, state);
41
+ }
42
+ return state;
43
+ }
44
+ /** Query pending items for a session. */
45
+ pending(sessionId) {
46
+ const state = this.sessions.get(sessionId);
47
+ if (!state) {
48
+ return { approvals: [], questions: [] };
49
+ }
50
+ return {
51
+ approvals: Array.from(state.approvals.values()),
52
+ questions: state.questions,
53
+ };
54
+ }
55
+ /** Clear all pending state for a session. */
56
+ clear(sessionId) {
57
+ this.sessions.delete(sessionId);
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy/api';
2
+ import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
3
+ import type { PairingService } from './pairing.ts';
4
+ export declare const PAIRED_MODEL_CATALOG_PATHS: {
5
+ readonly catalog: "/api/pair/model-catalog";
6
+ readonly discover: "/api/pair/model-catalog/discover";
7
+ readonly upsert: "/api/pair/model-catalog/upsert";
8
+ };
9
+ export interface PairedModelCatalogDeps {
10
+ service: PairingService;
11
+ apiProxy: ApiProxy;
12
+ /** The LAN IP literals the host fence accepts, mirroring the /api/pair fence. */
13
+ lanAddresses: readonly string[];
14
+ }
15
+ /** Build the narrow paired-device model catalog routes. */
16
+ export declare function makePairedModelCatalogRoutes(deps: PairedModelCatalogDeps): WebRoute[];
17
+ //# sourceMappingURL=paired-model-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paired-model-catalog.d.ts","sourceRoot":"","sources":["../../src/paired-model-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAiE,MAAM,oCAAoC,CAAA;AACjI,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAE/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA6BlD,eAAO,MAAM,0BAA0B;;;;CAI7B,CAAA;AAEV,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,QAAQ,CAAA;IAClB,iFAAiF;IACjF,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAsGD,2DAA2D;AAC3D,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,sBAAsB,GAAG,QAAQ,EAAE,CA2ErF"}