@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.1
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 +15 -7
- package/README.zh.md +4 -4
- package/lib/client.js +235 -37
- package/lib/client.js.map +1 -1
- package/lib/index.js +26 -7
- package/lib/mobile.js +209 -39
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +22 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +142 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +11 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +2 -0
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +2 -0
- package/lib/types/mobile/api.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +38 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -2
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +1 -1
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +1 -1
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +219 -18
- package/src/client/RemoteEntry.tsx +10 -0
- package/src/client/RemoteSettingsCard.tsx +2 -2
- package/src/client/UpdatePanel.tsx +2 -0
- package/src/client/locales.ts +2 -0
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +2 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +38 -0
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/routes.ts +22 -2
- package/src/tunnel.ts +7 -2
- package/src/update.ts +2 -2
package/lib/types/routes.js
CHANGED
|
@@ -200,7 +200,16 @@ export function makeRoutes(deps) {
|
|
|
200
200
|
const ACCEPT_MAX_ATTEMPTS = 10;
|
|
201
201
|
const ACCEPT_WINDOW_MS = 30_000;
|
|
202
202
|
const rateLimitAccept = (req) => {
|
|
203
|
-
const
|
|
203
|
+
const socketIp = req.socket?.remoteAddress ?? 'unknown';
|
|
204
|
+
// Behind the auto-tunnel every internet client arrives from 127.0.0.1,
|
|
205
|
+
// so a single shared bucket would let one attacker keep the legitimate
|
|
206
|
+
// owner rate-limited. Partition the availability bucket by the first
|
|
207
|
+
// client-visible XFF hop (set by the tunnel edge): XFF is untrusted for
|
|
208
|
+
// authentication and only separates buckets, it never grants access.
|
|
209
|
+
const forwarded = typeof req.headers['x-forwarded-for'] === 'string'
|
|
210
|
+
? (req.headers['x-forwarded-for'].split(',')[0] ?? '').trim()
|
|
211
|
+
: undefined;
|
|
212
|
+
const ip = forwarded === undefined || forwarded === '' ? socketIp : socketIp + '|' + forwarded;
|
|
204
213
|
const nowMs = Date.now();
|
|
205
214
|
// The map lives as long as the plugin: prune expired windows once the
|
|
206
215
|
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
@@ -287,6 +296,9 @@ export function makeRoutes(deps) {
|
|
|
287
296
|
writeJson(res, result.code === 'used' ? 409 : 404, { ok: false, code: result.code });
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
299
|
+
// No Secure attribute: LAN pairing runs over plain HTTP (the cookie must
|
|
300
|
+
// work there), and the same cookie rides HTTPS on the tunnel. Lax keeps
|
|
301
|
+
// top-level navigations working while blocking cross-site subrequests.
|
|
290
302
|
res.writeHead(200, {
|
|
291
303
|
'content-type': 'application/json; charset=utf-8',
|
|
292
304
|
'set-cookie': [
|
|
@@ -337,7 +349,15 @@ export function makeRoutes(deps) {
|
|
|
337
349
|
return;
|
|
338
350
|
}
|
|
339
351
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
340
|
-
|
|
352
|
+
const paired = deviceId !== undefined && service.hasDevice(deviceId);
|
|
353
|
+
const snapshot = service.snapshot();
|
|
354
|
+
// Unpaired LAN/tunnel clients get only the pairing-relevant fields; the
|
|
355
|
+
// token expiry, device roster, and public tunnel URL are an oracle for
|
|
356
|
+
// targeting and timing and stay behind a live device cookie.
|
|
357
|
+
const visible = paired
|
|
358
|
+
? snapshot
|
|
359
|
+
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable, lanAddresses: snapshot.lanAddresses };
|
|
360
|
+
writeJson(res, 200, { ok: true, paired, ...visible });
|
|
341
361
|
};
|
|
342
362
|
const handleEvents = (req, res) => {
|
|
343
363
|
if (!requireMethod(req, res, 'GET'))
|
package/lib/types/tunnel.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,0EAA0E;AAC1E,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAA;AAEvE,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAA;IAClB,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC9D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,GAAG,OAAO,CAAA;IAC/D,IAAI,IAAI,OAAO,CAAA;CAChB;AAED,kFAAkF;AAClF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,YAAY,CAAA;IAC7C,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAA;KAAE,CAAA;CAC5F;AAkBD;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAC7D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqF;IAE3G,OAAO,CAAC,KAAK,CAAyB;IACtC,OAAO,CAAC,GAAG,CAAoB;IAC/B,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAI;IAGpB,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;IAChE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE;;OAEG;IACH,YAAY,OAAO,GAAE,oBAAyB,EAO7C;IAED,gCAAgC;IAChC,IAAI,IAAI,IAAI,UAAU,CAMrB;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAQ7B;IAED,uDAAuD;IACvD,IAAI,IAAI,IAAI,CAKX;IAED,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAEd;IAED,4EAA4E;IAC5E,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAGjD;IAED,uCAAuC;IACvC,OAAO,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI,CAGxD;IAED,OAAO,CAAC,OAAO;IAqCf,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,IAAI;IAqBZ,iFAAiF;IACjF,OAAO,CAAC,QAAQ;IAgBhB,OAAO,CAAC,QAAQ;CAWjB"}
|
package/lib/types/tunnel.js
CHANGED
|
@@ -46,6 +46,9 @@ export class TunnelManager {
|
|
|
46
46
|
urlTimer;
|
|
47
47
|
restartTimer;
|
|
48
48
|
attempts = 0;
|
|
49
|
+
// Generation counter: a stale ensureBinary resolution from an earlier
|
|
50
|
+
// start() must not spawn a second handle after a stop/start cycle.
|
|
51
|
+
generation = 0;
|
|
49
52
|
stopping = false;
|
|
50
53
|
urlListeners = new Set();
|
|
51
54
|
phaseListeners = new Set();
|
|
@@ -81,6 +84,7 @@ export class TunnelManager {
|
|
|
81
84
|
this.stopping = false;
|
|
82
85
|
this.targetUrl = targetUrl;
|
|
83
86
|
this.attempts = 0;
|
|
87
|
+
this.generation += 1;
|
|
84
88
|
this.attempt();
|
|
85
89
|
}
|
|
86
90
|
/** Stop the tunnel for good: no restarts, no state. */
|
|
@@ -107,12 +111,13 @@ export class TunnelManager {
|
|
|
107
111
|
attempt() {
|
|
108
112
|
if (this.stopping || this.targetUrl === undefined)
|
|
109
113
|
return;
|
|
114
|
+
const gen = this.generation;
|
|
110
115
|
this.setPhase('starting');
|
|
111
116
|
this.handle = undefined;
|
|
112
117
|
this.url = undefined;
|
|
113
118
|
this.error = undefined;
|
|
114
119
|
void this.ensureBinary().then(() => {
|
|
115
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
120
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
116
121
|
return;
|
|
117
122
|
const handle = this.factory(this.targetUrl);
|
|
118
123
|
this.handle = handle;
|
|
@@ -139,7 +144,7 @@ export class TunnelManager {
|
|
|
139
144
|
});
|
|
140
145
|
}).catch((value) => {
|
|
141
146
|
// Binary install failed (no network, no platform build): report it.
|
|
142
|
-
if (this.stopping || this.targetUrl === undefined)
|
|
147
|
+
if (this.stopping || this.targetUrl === undefined || gen !== this.generation)
|
|
143
148
|
return;
|
|
144
149
|
const message = value instanceof Error ? value.message : String(value);
|
|
145
150
|
this.fail(`could not obtain the cloudflared binary: ${message}`);
|
package/lib/types/update.d.ts
CHANGED
|
@@ -137,7 +137,7 @@ export declare function familyChildren(anchorManifest: Record<string, unknown>):
|
|
|
137
137
|
* @param timeoutMs - probe timeout.
|
|
138
138
|
* @returns the latest version string, or undefined on any failure.
|
|
139
139
|
*/
|
|
140
|
-
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string) => Promise<{
|
|
140
|
+
export declare function fetchLatestVersion(name: string, fetchImpl: (url: string, init?: RequestInit) => Promise<{
|
|
141
141
|
ok: boolean;
|
|
142
142
|
json(): Promise<unknown>;
|
|
143
143
|
}>, timeoutMs?: number): Promise<string | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|
package/lib/types/update.js
CHANGED
|
@@ -218,7 +218,7 @@ export async function fetchLatestVersion(name, fetchImpl, timeoutMs = 10_000) {
|
|
|
218
218
|
const controller = new AbortController();
|
|
219
219
|
const timer = setTimeout(() => { controller.abort(); }, timeoutMs);
|
|
220
220
|
try {
|
|
221
|
-
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest');
|
|
221
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal });
|
|
222
222
|
if (!response.ok)
|
|
223
223
|
return undefined;
|
|
224
224
|
const body = await response.json();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* ui-plugin-config PluginCard in a self-contained slice.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { useState, type ReactNode } from 'react'
|
|
11
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent, type ReactNode } from 'react'
|
|
12
12
|
import type { CardShell } from './settings-form.ts'
|
|
13
13
|
import css from './settings-card.module.css'
|
|
14
14
|
|
|
@@ -255,6 +255,210 @@ export function ValueField(props: FieldProps & {
|
|
|
255
255
|
)
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
+
const NON_SKIN_BODY_MARKERS = new Set(['dshSkinCenter', 'dshSidebarCollapsed'])
|
|
259
|
+
// 检测使用使用皮肤,用了皮肤退回原生select样式,防止样式冲突。默认外观下使用优化后的select样式。
|
|
260
|
+
function isSkinActive(): boolean {
|
|
261
|
+
const datasetList = Object.keys(document.body.dataset)
|
|
262
|
+
const isActive = datasetList.some(key => key.startsWith('dsh') && !NON_SKIN_BODY_MARKERS.has(key))
|
|
263
|
+
return isActive
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface SelectOption {
|
|
267
|
+
value: string
|
|
268
|
+
label: string
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const SELECT_CLOSE_MS = 100
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The shared dual-mode select control. While an appearance skin is active it
|
|
275
|
+
* renders the legacy native `<select>` untouched, so element-level skin
|
|
276
|
+
* selectors keep working; under the default appearance it renders a
|
|
277
|
+
* self-drawn `role="listbox"` popup whose open/close is transition-animated.
|
|
278
|
+
* Staged cards reach it through BooleanField/ChoiceField; immediate-apply
|
|
279
|
+
* editors (the side-card prefs) bind it directly through onEdit.
|
|
280
|
+
* 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
|
|
281
|
+
*/
|
|
282
|
+
export function SelectField(props: {
|
|
283
|
+
id: string
|
|
284
|
+
options: ReadonlyArray<SelectOption>
|
|
285
|
+
value: string
|
|
286
|
+
disabled: boolean
|
|
287
|
+
invalid: boolean
|
|
288
|
+
onEdit: (text: string) => void
|
|
289
|
+
}) {
|
|
290
|
+
const { id, options, value } = props
|
|
291
|
+
const [open, setOpen] = useState(false)
|
|
292
|
+
const [closing, setClosing] = useState(false)
|
|
293
|
+
const [phase, setPhase] = useState<'initial' | 'open'>('initial')
|
|
294
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
295
|
+
const closeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
296
|
+
const wrapRef = useRef<HTMLDivElement | null>(null)
|
|
297
|
+
const popupRef = useRef<HTMLDivElement | null>(null)
|
|
298
|
+
|
|
299
|
+
const currentIndex = () => {
|
|
300
|
+
const index = options.findIndex(option => option.value === value)
|
|
301
|
+
return index >= 0 ? index : 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const close = useCallback(() => {
|
|
305
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
306
|
+
setClosing(true)
|
|
307
|
+
closeTimer.current = setTimeout(() => {
|
|
308
|
+
setClosing(false)
|
|
309
|
+
setOpen(false)
|
|
310
|
+
}, SELECT_CLOSE_MS)
|
|
311
|
+
}, [])
|
|
312
|
+
|
|
313
|
+
const openPopup = () => {
|
|
314
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
315
|
+
setActiveIndex(currentIndex())
|
|
316
|
+
setPhase('initial')
|
|
317
|
+
setClosing(false)
|
|
318
|
+
setOpen(true)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const commit = (index: number) => {
|
|
322
|
+
const option = options[index]
|
|
323
|
+
if (option) props.onEdit(option.value)
|
|
324
|
+
close()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const onTriggerClick = () => {
|
|
328
|
+
if (props.disabled) return
|
|
329
|
+
if (open && !closing) close()
|
|
330
|
+
else openPopup()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
334
|
+
if (props.disabled) return
|
|
335
|
+
const count = options.length
|
|
336
|
+
switch (event.key) {
|
|
337
|
+
case 'ArrowDown':
|
|
338
|
+
case 'ArrowUp':
|
|
339
|
+
case 'Enter':
|
|
340
|
+
case ' ':
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
if (!open) {
|
|
343
|
+
openPopup()
|
|
344
|
+
} else if (!closing) {
|
|
345
|
+
if (event.key === 'ArrowDown') setActiveIndex(index => (index + 1) % count)
|
|
346
|
+
else if (event.key === 'ArrowUp') setActiveIndex(index => (index - 1 + count) % count)
|
|
347
|
+
else commit(activeIndex)
|
|
348
|
+
}
|
|
349
|
+
break
|
|
350
|
+
case 'Escape':
|
|
351
|
+
if (open) {
|
|
352
|
+
event.preventDefault()
|
|
353
|
+
event.stopPropagation()
|
|
354
|
+
close()
|
|
355
|
+
}
|
|
356
|
+
break
|
|
357
|
+
case 'Tab':
|
|
358
|
+
if (open) close()
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
useEffect(() => () => {
|
|
364
|
+
if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
|
|
365
|
+
}, [])
|
|
366
|
+
useLayoutEffect(() => {
|
|
367
|
+
if (open && !closing && phase === 'initial') {
|
|
368
|
+
void popupRef.current?.offsetHeight
|
|
369
|
+
setPhase('open')
|
|
370
|
+
}
|
|
371
|
+
}, [open, closing, phase])
|
|
372
|
+
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (!open) return
|
|
375
|
+
const onPointerDown = (event: PointerEvent) => {
|
|
376
|
+
const target = event.target
|
|
377
|
+
if (target instanceof Node && !wrapRef.current?.contains(target)) close()
|
|
378
|
+
}
|
|
379
|
+
document.addEventListener('pointerdown', onPointerDown)
|
|
380
|
+
return () => document.removeEventListener('pointerdown', onPointerDown)
|
|
381
|
+
}, [open, close])
|
|
382
|
+
|
|
383
|
+
useEffect(() => {
|
|
384
|
+
if (props.disabled && open) close()
|
|
385
|
+
}, [props.disabled, open, close])
|
|
386
|
+
|
|
387
|
+
// 使用了皮肤则退回原生select样式防止样式冲突。
|
|
388
|
+
if (isSkinActive()) {
|
|
389
|
+
return (
|
|
390
|
+
<select
|
|
391
|
+
id={id}
|
|
392
|
+
className={css.select}
|
|
393
|
+
value={value}
|
|
394
|
+
disabled={props.disabled}
|
|
395
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
396
|
+
>
|
|
397
|
+
{options.map(option => (
|
|
398
|
+
<option key={option.value} value={option.value}>{option.label}</option>
|
|
399
|
+
))}
|
|
400
|
+
</select>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const label = options.find(option => option.value === value)?.label ?? ''
|
|
405
|
+
const popupClass = closing
|
|
406
|
+
? `${css.selectPopup} ${css.selectPopupClose}`
|
|
407
|
+
: phase === 'open'
|
|
408
|
+
? `${css.selectPopup} ${css.selectPopupOpen}`
|
|
409
|
+
: css.selectPopup
|
|
410
|
+
return (
|
|
411
|
+
<div className={css.selectWrap} ref={wrapRef}>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
id={id}
|
|
415
|
+
className={`${css.select} ${css.selectButton}`}
|
|
416
|
+
disabled={props.disabled}
|
|
417
|
+
aria-haspopup="listbox"
|
|
418
|
+
aria-expanded={open}
|
|
419
|
+
aria-activedescendant={open ? `${id}-o${activeIndex}` : undefined}
|
|
420
|
+
aria-invalid={props.invalid || undefined}
|
|
421
|
+
onClick={onTriggerClick}
|
|
422
|
+
onKeyDown={onKeyDown}
|
|
423
|
+
>
|
|
424
|
+
<span className={css.selectLabel}>{label}</span>
|
|
425
|
+
<svg
|
|
426
|
+
width="14"
|
|
427
|
+
height="14"
|
|
428
|
+
viewBox="0 0 14 14"
|
|
429
|
+
fill="none"
|
|
430
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
431
|
+
className={open ? `${css.selectChevron} ${css.selectChevronOpen}` : css.selectChevron}
|
|
432
|
+
aria-hidden="true"
|
|
433
|
+
>
|
|
434
|
+
<path
|
|
435
|
+
d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
|
|
436
|
+
fill="currentColor"
|
|
437
|
+
/>
|
|
438
|
+
</svg>
|
|
439
|
+
</button>
|
|
440
|
+
{open
|
|
441
|
+
? (
|
|
442
|
+
<div className={popupClass} role="listbox" ref={popupRef}>
|
|
443
|
+
{options.map((option, index) => (
|
|
444
|
+
<div
|
|
445
|
+
key={option.value}
|
|
446
|
+
id={`${id}-o${index}`}
|
|
447
|
+
role="option"
|
|
448
|
+
aria-selected={option.value === value}
|
|
449
|
+
className={`${css.selectOption}${option.value === value ? ` ${css.selectOptionSelected}` : ''}${index === activeIndex && !closing ? ` ${css.selectOptionActive}` : ''}`}
|
|
450
|
+
onClick={() => { commit(index) }}
|
|
451
|
+
>
|
|
452
|
+
{option.label}
|
|
453
|
+
</div>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
)
|
|
457
|
+
: null}
|
|
458
|
+
</div>
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
|
|
258
462
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
259
463
|
export function BooleanField(props: FieldProps & {
|
|
260
464
|
/** Copy for the inherit option. */
|
|
@@ -284,17 +488,18 @@ export function BooleanField(props: FieldProps & {
|
|
|
284
488
|
)
|
|
285
489
|
: null}
|
|
286
490
|
</div>
|
|
287
|
-
<
|
|
491
|
+
<SelectField
|
|
288
492
|
id={props.id}
|
|
289
|
-
|
|
493
|
+
options={[
|
|
494
|
+
{ value: '', label: props.inheritLabel },
|
|
495
|
+
{ value: 'true', label: props.onLabel },
|
|
496
|
+
{ value: 'false', label: props.offLabel },
|
|
497
|
+
]}
|
|
290
498
|
value={props.text}
|
|
291
499
|
disabled={props.disabled}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<option value="true">{props.onLabel}</option>
|
|
296
|
-
<option value="false">{props.offLabel}</option>
|
|
297
|
-
</select>
|
|
500
|
+
invalid={props.invalid}
|
|
501
|
+
onEdit={props.onEdit}
|
|
502
|
+
/>
|
|
298
503
|
<p className={css.hint}>{props.hint}</p>
|
|
299
504
|
</div>
|
|
300
505
|
)
|
|
@@ -327,18 +532,14 @@ export function ChoiceField(props: FieldProps & {
|
|
|
327
532
|
)
|
|
328
533
|
: null}
|
|
329
534
|
</div>
|
|
330
|
-
<
|
|
535
|
+
<SelectField
|
|
331
536
|
id={props.id}
|
|
332
|
-
|
|
537
|
+
options={[{ value: '', label: props.inheritLabel }, ...props.choices]}
|
|
333
538
|
value={props.text}
|
|
334
539
|
disabled={props.disabled}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
{props.choices.map(choice => (
|
|
339
|
-
<option key={choice.value} value={choice.value}>{choice.label}</option>
|
|
340
|
-
))}
|
|
341
|
-
</select>
|
|
540
|
+
invalid={props.invalid}
|
|
541
|
+
onEdit={props.onEdit}
|
|
542
|
+
/>
|
|
342
543
|
<p className={props.invalid ? css.invalid : css.hint}>
|
|
343
544
|
{props.invalid ? props.invalidLabel : props.hint}
|
|
344
545
|
</p>
|
|
@@ -57,6 +57,10 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
57
57
|
useEffect(() => { stateRef.current = state }, [state])
|
|
58
58
|
const [copied, setCopied] = useState(false)
|
|
59
59
|
const eventSource = useRef<EventSource | undefined>(undefined)
|
|
60
|
+
// Generation counter for the open flow: closing (or re-opening) the panel
|
|
61
|
+
// bumps it, so an in-flight issue() that resolves after a close does not
|
|
62
|
+
// spawn a stray EventSource.
|
|
63
|
+
const openSeq = useRef(0)
|
|
60
64
|
|
|
61
65
|
// The current workspace (the recent-workspace projection the shell's New
|
|
62
66
|
// Session flow targets) — the deep-link target for the phone.
|
|
@@ -104,8 +108,13 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
104
108
|
}, [workspaceId])
|
|
105
109
|
|
|
106
110
|
const openPanel = useCallback(async (): Promise<void> => {
|
|
111
|
+
const seq = ++openSeq.current
|
|
107
112
|
setOpen(true)
|
|
108
113
|
const next = await mint()
|
|
114
|
+
// A close (or re-open) during the await invalidates this issue: skip the
|
|
115
|
+
// state write and the stream so a panel closed mid-mint neither leaks an
|
|
116
|
+
// EventSource nor resurrects a stale QR.
|
|
117
|
+
if (seq !== openSeq.current) return
|
|
109
118
|
setState(next)
|
|
110
119
|
// Live status: the desktop panel mirrors the pairing service state. The
|
|
111
120
|
// stream makes sense in the ready state and on the lan-required banner —
|
|
@@ -143,6 +152,7 @@ export function RemoteEntry({ wide, useWorkspaces, t }: RemoteEntryProps) {
|
|
|
143
152
|
}, [mint])
|
|
144
153
|
|
|
145
154
|
const closePanel = useCallback(() => {
|
|
155
|
+
openSeq.current += 1
|
|
146
156
|
closeEventSource()
|
|
147
157
|
setOpen(false)
|
|
148
158
|
}, [closeEventSource])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The remote-control settings card: pairing security and device limits.
|
|
3
|
-
* Registers into the `
|
|
4
|
-
*
|
|
3
|
+
* Registers into the `web-ui.plugin.item` child slot the Web UI plugin group
|
|
4
|
+
* renders, bound to the `remote-web-ui` settings namespace.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
@@ -63,6 +63,7 @@ export function UpdatePanel({ t, view, onClose, onRecheck }: UpdatePanelProps) {
|
|
|
63
63
|
<p className={css.updateStatus}>
|
|
64
64
|
{t("update.updating", { name: anchorName(status) ?? "", version: anchorLatest(status) ?? "" })}
|
|
65
65
|
</p>
|
|
66
|
+
<p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
|
|
66
67
|
<PackageList status={status} />
|
|
67
68
|
</div>
|
|
68
69
|
)}
|
|
@@ -136,6 +137,7 @@ function ResultBody({ t, status }: { t: TranslateNS<"remote">; status: UpdateSta
|
|
|
136
137
|
<p className={css.updateDetail}>
|
|
137
138
|
{anchor !== undefined ? t("update.foundDetail", { name: anchor, version: latest ?? "" }) : ""}
|
|
138
139
|
</p>
|
|
140
|
+
<p className={css.updateDetail}>{t("update.cooldownNotice")}</p>
|
|
139
141
|
</div>
|
|
140
142
|
)
|
|
141
143
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -82,6 +82,7 @@ export const zh = {
|
|
|
82
82
|
'update.found': '发现新版本',
|
|
83
83
|
'update.foundDetail': '{name} 有新版本 v{version},正在自动更新…',
|
|
84
84
|
'update.updating': '正在更新 {name} 至 v{version},可能需要 1-2 分钟…',
|
|
85
|
+
'update.cooldownNotice': '本次更新由你主动触发,将跳过 pnpm 的 24 小时发布冷却期(供应链保护)直接安装所选版本。',
|
|
85
86
|
'update.updatingTitle': '正在自动更新…',
|
|
86
87
|
'update.done': '更新完成',
|
|
87
88
|
'update.doneDetail': '所有组件已更新到最新版本。',
|
|
@@ -196,6 +197,7 @@ export const en = {
|
|
|
196
197
|
'update.found': 'A new version is available',
|
|
197
198
|
'update.foundDetail': '{name} has a newer release v{version}; updating automatically…',
|
|
198
199
|
'update.updating': 'Updating {name} to v{version}; this may take 1-2 minutes…',
|
|
200
|
+
'update.cooldownNotice': 'This user-initiated update bypasses pnpm\'s 24-hour release cooldown (supply-chain protection) and installs the selected version immediately.',
|
|
199
201
|
'update.updatingTitle': 'Updating automatically…',
|
|
200
202
|
'update.done': 'Update complete',
|
|
201
203
|
'update.doneDetail': 'All components are up to date.',
|
|
@@ -290,6 +290,92 @@
|
|
|
290
290
|
border-color: var(--dsw-alias-label-error);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
.selectWrap {
|
|
294
|
+
position: relative;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.selectButton {
|
|
298
|
+
appearance: none;
|
|
299
|
+
width: 100%;
|
|
300
|
+
text-align: left;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
align-items: center;
|
|
303
|
+
justify-content: space-between;
|
|
304
|
+
gap: 8px;
|
|
305
|
+
display: flex;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.selectLabel {
|
|
309
|
+
min-width: 0;
|
|
310
|
+
overflow: hidden;
|
|
311
|
+
text-overflow: ellipsis;
|
|
312
|
+
white-space: nowrap;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.selectChevron {
|
|
316
|
+
color: var(--dsw-alias-label-tertiary);
|
|
317
|
+
flex: none;
|
|
318
|
+
transition: transform 0.16s;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.selectChevronOpen {
|
|
322
|
+
transform: rotate(180deg);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.selectPopup {
|
|
326
|
+
position: absolute;
|
|
327
|
+
top: calc(100% + 4px);
|
|
328
|
+
left: 0;
|
|
329
|
+
right: 0;
|
|
330
|
+
z-index: 40;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
max-height: 240px;
|
|
333
|
+
overflow-y: auto;
|
|
334
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
335
|
+
border-radius: 8px;
|
|
336
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
337
|
+
box-shadow: 0 8px 24px var(--dsw-alias-bg-mask-2);
|
|
338
|
+
padding: 4px;
|
|
339
|
+
opacity: 0;
|
|
340
|
+
transform: translateY(-4px);
|
|
341
|
+
transition: opacity 0.1s ease, transform 0.1s ease;
|
|
342
|
+
display: flex;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.selectPopupOpen {
|
|
346
|
+
opacity: 1;
|
|
347
|
+
transform: none;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.selectPopupClose {
|
|
351
|
+
opacity: 0;
|
|
352
|
+
transform: translateY(-4px);
|
|
353
|
+
pointer-events: none;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.selectOption {
|
|
357
|
+
border-radius: 6px;
|
|
358
|
+
padding: 6px 10px;
|
|
359
|
+
font-size: 13px;
|
|
360
|
+
line-height: 1.5;
|
|
361
|
+
color: var(--dsw-alias-label-primary);
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
overflow: hidden;
|
|
365
|
+
text-overflow: ellipsis;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.selectOption:hover,
|
|
369
|
+
.selectOptionActive {
|
|
370
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.selectOptionSelected {
|
|
374
|
+
color: var(--dsw-alias-brand-primary);
|
|
375
|
+
font-weight: 500;
|
|
376
|
+
background: color-mix(in srgb, var(--dsw-alias-brand-primary-new-colorprimary-new-color) 10%, transparent);
|
|
377
|
+
}
|
|
378
|
+
|
|
293
379
|
.invalid {
|
|
294
380
|
color: var(--dsw-alias-label-error);
|
|
295
381
|
margin: 0;
|
|
@@ -310,7 +396,10 @@
|
|
|
310
396
|
.chevron,
|
|
311
397
|
.chevronOpen,
|
|
312
398
|
.discard,
|
|
313
|
-
.save
|
|
399
|
+
.save,
|
|
400
|
+
.selectChevron,
|
|
401
|
+
.selectChevronOpen,
|
|
402
|
+
.selectPopup {
|
|
314
403
|
transition: none;
|
|
315
404
|
}
|
|
316
405
|
}
|
package/src/gate.ts
CHANGED
|
@@ -89,6 +89,8 @@ export function makeGateListener(
|
|
|
89
89
|
if (!active) return false
|
|
90
90
|
const require = typeof requirePairingForLan === 'function' ? requirePairingForLan() : requirePairingForLan
|
|
91
91
|
if (!require) return next()
|
|
92
|
+
// The paired-device cookie is a full host-API credential: it passes this
|
|
93
|
+
// gate for the whole ApiProxy surface, not just the /m/api allowlist.
|
|
92
94
|
return isPairedDeviceRequest(service, request) ? next() : false
|
|
93
95
|
}
|
|
94
96
|
}
|
package/src/mobile/api.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { WorkspaceView } from '@deepseek-ai/dsh-host-apiproxy/api/workspace'
|
|
9
|
+
import type { AgentPresetEntry } from '@deepseek-ai/dsh-host-apiproxy/api/agent-presets'
|
|
9
10
|
import type { SessionSummary, SessionModels, SessionProjectionsBlock } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
|
|
10
11
|
import { callUnary } from './rpc.ts'
|
|
11
12
|
|
|
@@ -24,6 +25,13 @@ export interface CreatedSession {
|
|
|
24
25
|
agentPreset?: string
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** Agent presets available when composing a new session. */
|
|
29
|
+
export interface AgentPresetRoster {
|
|
30
|
+
presets: readonly AgentPresetEntry[]
|
|
31
|
+
authorable: boolean
|
|
32
|
+
hasDocument: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/** One history page (already bounded to whole messages by the host). */
|
|
28
36
|
export interface HistoryPage {
|
|
29
37
|
events: import('@deepseek-ai/dsh-host-apiproxy/api/sessions').HistoryEntry[]
|
|
@@ -57,12 +65,17 @@ export async function listSessions(cursor?: string): Promise<SessionPage> {
|
|
|
57
65
|
return await callUnary<SessionPage>('session.list', cursor === undefined ? {} : { cursor })
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
/** Read the available agent compositions for a new session. */
|
|
69
|
+
export async function listAgentPresets(): Promise<AgentPresetRoster> {
|
|
70
|
+
return await callUnary<AgentPresetRoster>('agentPreset.list', {})
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/**
|
|
61
74
|
* Create a blank session (entity birth precedes the first message). Name a
|
|
62
75
|
* workspace to attach it there, or a cwd; omitting both uses the host cwd.
|
|
63
76
|
*/
|
|
64
77
|
export async function createSession(
|
|
65
|
-
options: { workspaceId?: string; cwd?: string } = {},
|
|
78
|
+
options: { workspaceId?: string; cwd?: string; agentPreset?: string } = {},
|
|
66
79
|
): Promise<CreatedSession> {
|
|
67
80
|
return await callUnary<CreatedSession>('session.create', options)
|
|
68
81
|
}
|