@linxin666/dsh-remote-web-ui 0.4.4 → 0.4.6
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 +22 -7
- package/README.zh.md +21 -6
- package/lib/client.js +75 -138
- package/lib/client.js.map +1 -1
- package/lib/index.js +162 -22
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +2 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/locales.d.ts +0 -4
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +5 -9
- package/lib/types/client/mobile-adapt.d.ts +2 -2
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +51 -131
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
- package/lib/types/client/plugin-card-seat.js +2 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +27 -1
- package/lib/types/detached-work.d.ts +45 -0
- package/lib/types/detached-work.d.ts.map +1 -0
- package/lib/types/detached-work.js +55 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +31 -10
- package/lib/types/lan-bind.d.ts +21 -4
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +103 -17
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +4 -1
- package/package.json +19 -19
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/RemoteSettingsCard.tsx +2 -0
- package/src/client/index.ts +3 -3
- package/src/client/locales.ts +5 -9
- package/src/client/mobile-adapt.ts +53 -136
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/detached-work.ts +58 -0
- package/src/index.ts +31 -10
- package/src/lan-bind.ts +111 -17
- package/src/remote-channel-boot.ts +4 -1
package/lib/types/lan-bind.js
CHANGED
|
@@ -49,16 +49,96 @@ function readPatchContent(file) {
|
|
|
49
49
|
return '';
|
|
50
50
|
return readFileSync(file, 'utf8');
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Split a block-sequence region into its top-level items. Only a column-0 `-`
|
|
54
|
+
* starts an item: that is the shape every writer of this file produces - the
|
|
55
|
+
* managed block itself and the YAML document API's block-style re-emission.
|
|
56
|
+
* @param region - text between two line boundaries.
|
|
57
|
+
* @returns the items in source order; text outside them is not returned.
|
|
58
|
+
*/
|
|
59
|
+
function blockItems(region) {
|
|
60
|
+
const items = [];
|
|
61
|
+
let start = -1;
|
|
62
|
+
let offset = 0;
|
|
63
|
+
for (const line of region.split('\n')) {
|
|
64
|
+
if (line.startsWith('- ') || line === '-') {
|
|
65
|
+
if (start !== -1)
|
|
66
|
+
items.push({ start, end: offset, text: region.slice(start, offset) });
|
|
67
|
+
start = offset;
|
|
68
|
+
}
|
|
69
|
+
offset += line.length + 1;
|
|
70
|
+
}
|
|
71
|
+
if (start !== -1)
|
|
72
|
+
items.push({ start, end: region.length, text: region.slice(start) });
|
|
73
|
+
return items;
|
|
55
74
|
}
|
|
56
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Whether one top-level item is the webserver row this plugin's block owns.
|
|
77
|
+
* The row id is the whole signal: the markers declare everything inside them
|
|
78
|
+
* managed, our block always writes `- id: webserver`, and a same-id row later
|
|
79
|
+
* in the patch replaces this one wholesale anyway - so an id-webserver row
|
|
80
|
+
* found inside our markers is this plugin's row (a hand-truncated block may
|
|
81
|
+
* have lost its `name` line).
|
|
82
|
+
* @param item - one top-level item's text.
|
|
83
|
+
* @returns true when the item is the managed webserver row.
|
|
84
|
+
*/
|
|
85
|
+
function isManagedWebserverItem(item) {
|
|
86
|
+
return /^- id:\s*['"]?webserver['"]?[ \t]*(?:\r?\n|$)/.test(item);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The text between a block's BEGIN and END markers: from the character after
|
|
90
|
+
* the BEGIN line's terminator to the start of the END marker's line (or to
|
|
91
|
+
* the end of the content without an END marker).
|
|
92
|
+
* @param content - the patch file text.
|
|
93
|
+
* @param begin - index of the BEGIN marker.
|
|
94
|
+
* @param end - index of the END marker, or -1 when the block is unterminated.
|
|
95
|
+
* @returns the region's text.
|
|
96
|
+
*/
|
|
97
|
+
function managedRegion(content, begin, end) {
|
|
98
|
+
const newlineAfterBegin = content.indexOf('\n', begin);
|
|
99
|
+
const innerStart = newlineAfterBegin === -1 ? content.length : newlineAfterBegin + 1;
|
|
100
|
+
const endLineStart = end === -1 ? content.length : content.lastIndexOf('\n', end - 1) + 1;
|
|
101
|
+
return content.slice(innerStart, Math.max(innerStart, endLineStart));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove the managed block (its markers and the webserver row this plugin
|
|
105
|
+
* wrote) from patch content, keeping every other row.
|
|
106
|
+
*
|
|
107
|
+
* The markers travel with the file through writers that append rows through
|
|
108
|
+
* the YAML document API: the appended rows land at the end of the sequence,
|
|
109
|
+
* which is BEFORE a trailing END comment, so the region between the markers
|
|
110
|
+
* can hold rows this plugin never wrote. Deleting the whole region silently
|
|
111
|
+
* removed the user's model, plugin and remote-control rows on the next rewrite
|
|
112
|
+
* - the remote-control row carries the LAN toggle itself, so the feature reset
|
|
113
|
+
* to `undefined` and the phone was locked out again after a restart (DSH
|
|
114
|
+
* Desktop report, 2026-09-30). Only the managed item is removed here; every
|
|
115
|
+
* foreign row is kept in place and the fresh block is appended after them (the
|
|
116
|
+
* same-id row last wins).
|
|
117
|
+
* @param content - the patch file text.
|
|
118
|
+
* @returns the text with the markers and the managed row removed.
|
|
119
|
+
*/
|
|
57
120
|
export function stripManagedBlock(content) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
121
|
+
let text = content;
|
|
122
|
+
for (;;) {
|
|
123
|
+
const begin = text.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
124
|
+
if (begin === -1)
|
|
125
|
+
return text;
|
|
126
|
+
const end = text.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
127
|
+
const blockStart = text.lastIndexOf('\n', begin - 1) + 1;
|
|
128
|
+
const newlineAfterEnd = end === -1 ? -1 : text.indexOf('\n', end);
|
|
129
|
+
const resumeAt = end === -1 ? text.length : (newlineAfterEnd === -1 ? text.length : newlineAfterEnd + 1);
|
|
130
|
+
const region = managedRegion(text, begin, end);
|
|
131
|
+
let kept = '';
|
|
132
|
+
let cursor = 0;
|
|
133
|
+
for (const item of blockItems(region)) {
|
|
134
|
+
if (!isManagedWebserverItem(item.text))
|
|
135
|
+
continue;
|
|
136
|
+
kept += region.slice(cursor, item.start);
|
|
137
|
+
cursor = item.end;
|
|
138
|
+
}
|
|
139
|
+
kept += region.slice(cursor);
|
|
140
|
+
text = text.slice(0, blockStart) + kept + text.slice(resumeAt);
|
|
141
|
+
}
|
|
62
142
|
}
|
|
63
143
|
/**
|
|
64
144
|
* Render the managed block for one bind state. The values are static: the
|
|
@@ -149,9 +229,16 @@ export function managedBindOf(content) {
|
|
|
149
229
|
if (begin === -1)
|
|
150
230
|
return undefined;
|
|
151
231
|
const end = content.indexOf(LAN_BIND_BLOCK_END, begin);
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
232
|
+
const region = managedRegion(content, begin, end);
|
|
233
|
+
// Read the bind out of the managed row itself. A region a YAML writer
|
|
234
|
+
// displaced holds foreign rows too, and their `host`/`port` keys are not
|
|
235
|
+
// this block's bind - reading them made the boot re-assert
|
|
236
|
+
// believe the block was already applied. A region without a recognizable
|
|
237
|
+
// managed row (hand-edited literals) keeps the historical whole-region read.
|
|
238
|
+
const items = blockItems(region).filter(item => isManagedWebserverItem(item.text));
|
|
239
|
+
const scope = items.length > 0 ? items.map(item => item.text).join('') : region;
|
|
240
|
+
const hostMatch = /host:\s*'([^']+)'/.exec(scope);
|
|
241
|
+
const portMatch = /port:\s*(\d+)/.exec(scope);
|
|
155
242
|
return {
|
|
156
243
|
host: hostMatch?.[1] ?? '',
|
|
157
244
|
port: portMatch !== null ? Number(portMatch[1]) : undefined,
|
|
@@ -169,9 +256,10 @@ export function lanBindState(profile, home = dshHome()) {
|
|
|
169
256
|
* patch file is preserved; the block is rewritten atomically (unique temp
|
|
170
257
|
* file + rename, so concurrent writers can never rename a half-written
|
|
171
258
|
* file) with the original file's permissions preserved. A hand-truncated
|
|
172
|
-
* unterminated block (BEGIN marker without END
|
|
173
|
-
*
|
|
174
|
-
* never stack a second webserver row onto the
|
|
259
|
+
* unterminated block (BEGIN marker without END) is consumed by
|
|
260
|
+
* stripManagedBlock as well - markers and managed row removed, foreign rows
|
|
261
|
+
* kept - so the rewrite can never stack a second webserver row onto the
|
|
262
|
+
* orphan.
|
|
175
263
|
*
|
|
176
264
|
* The written file is always exactly one valid YAML document: a base the
|
|
177
265
|
* plugin manager left in flow style is re-emitted as a block sequence before
|
|
@@ -179,9 +267,7 @@ export function lanBindState(profile, home = dshHome()) {
|
|
|
179
267
|
*/
|
|
180
268
|
export function writeLanBind(host, port, profile, home = dshHome()) {
|
|
181
269
|
const file = profilePatchFile(profile, home);
|
|
182
|
-
const
|
|
183
|
-
const orphanBegin = stripped.indexOf(LAN_BIND_BLOCK_BEGIN);
|
|
184
|
-
const rawBase = (orphanBegin === -1 ? stripped : stripped.slice(0, orphanBegin)).trimEnd();
|
|
270
|
+
const rawBase = stripManagedBlock(readPatchContent(file)).trimEnd();
|
|
185
271
|
const base = rawBase.replace(/\[\s*\]\s*$/, '').trimEnd();
|
|
186
272
|
const block = managedBlock(host, port);
|
|
187
273
|
const content = mergePatchContent(base, block, file);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAkKrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"remote-channel-boot.d.ts","sourceRoot":"","sources":["../../src/remote-channel-boot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAoD,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAErH;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,GAAE,kBAAyC,GAAG,MAAM,CAkKrG;AAED,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,2BAA2B,CAAA;AAQzD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,SAAwB,EAAE,MAAM,SAAwB,GAAG,MAAM,CAmB9G;AAED,0EAA0E;AAC1E,eAAO,MAAM,0BAA0B,EAAE,MAAuC,CAAA"}
|
|
@@ -200,7 +200,7 @@ export function buildRemoteChannelBootScript(rules = REMOTE_CHANNEL_RULES) {
|
|
|
200
200
|
}
|
|
201
201
|
/** Boot-watchdog latch: one self-reload per session while the boot is broken. */
|
|
202
202
|
export const BOOT_WATCHDOG_KEY = 'dsh-remote-boot-reload';
|
|
203
|
-
/**
|
|
203
|
+
/** Post-load wait for the app's first conversation surface before the reload. */
|
|
204
204
|
const BOOT_WATCHDOG_WAIT_MS = 15_000;
|
|
205
205
|
/** Watchdog poll cadence. */
|
|
206
206
|
const BOOT_WATCHDOG_POLL_MS = 1_000;
|
|
@@ -221,6 +221,9 @@ export function buildBootWatchdogScript(waitMs = BOOT_WATCHDOG_WAIT_MS, pollMs =
|
|
|
221
221
|
';function wBoot(){try{return !!(w.document&&w.document.querySelector&&(w.document.querySelector("[data-conversation-scroll]")||w.document.querySelector("[data-slot=\\"conversation\\"]")))}catch(e){return false}}' +
|
|
222
222
|
'function wTick(n){try{' +
|
|
223
223
|
'if(wBoot()){try{w.sessionStorage.removeItem(' + key + ')}catch(e){}return}' +
|
|
224
|
+
// Parse-time installation must not spend the recovery budget downloading
|
|
225
|
+
// the initial scripts/styles over a slow remote link (issue #1845).
|
|
226
|
+
'if(!w.document||w.document.readyState!=="complete"){w.setTimeout(function(){wTick(0)},' + pollMs + ');return}' +
|
|
224
227
|
'if(n<' + waitMs + '){w.setTimeout(function(){wTick(n+' + pollMs + ')},' + pollMs + ');return}' +
|
|
225
228
|
'var done=false;try{done=w.sessionStorage.getItem(' + key + ')==="1"}catch(e){}' +
|
|
226
229
|
'if(done)return;' +
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
3
|
"description": "Scan-to-pair remote access for the dsh web GUI that shares one official interface: a QR beside the settings button pairs phones and PCs into the same Web GUI (a portrait-touch adaptation layer for phones, full desktop on PCs) through one-time tokens and revocable device sessions, with a LAN bind toggle, optional Cloudflare tunnel, and one-click family self-update",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.6",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"dsh": {
|
|
40
40
|
"engines": {
|
|
41
|
-
"dsh": ">=0.2.0-rc.
|
|
41
|
+
"dsh": ">=0.2.0-rc.2"
|
|
42
42
|
},
|
|
43
43
|
"bundle": {
|
|
44
44
|
"patch": "./cordis.patch.yml"
|
|
@@ -61,28 +61,28 @@
|
|
|
61
61
|
"zod": "^4.4.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@deepseek-ai/dsh": ">=0.2.0-rc.
|
|
64
|
+
"@deepseek-ai/dsh": ">=0.2.0-rc.2",
|
|
65
65
|
"react": "^18.2.0",
|
|
66
66
|
"react-dom": "^18.2.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
70
|
-
"@deepseek-ai/dsh-agent": "^0.2.0-rc.
|
|
71
|
-
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.
|
|
72
|
-
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.
|
|
73
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.
|
|
74
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.
|
|
76
|
-
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.
|
|
80
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.
|
|
81
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.
|
|
82
|
-
"@deepseek-ai/dsh-commands": "^0.2.0-rc.
|
|
83
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.
|
|
84
|
-
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.2.0-rc.
|
|
70
|
+
"@deepseek-ai/dsh-agent": "^0.2.0-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-api-remotes": "^0.2.0-rc.2",
|
|
72
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.2.0-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.0-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.2",
|
|
76
|
+
"@deepseek-ai/dsh-client-store": "^0.2.0-rc.2",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.2.0-rc.2",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.2",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.2",
|
|
82
|
+
"@deepseek-ai/dsh-commands": "^0.2.0-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
86
86
|
"@testing-library/dom": "^10.4.1",
|
|
87
87
|
"@testing-library/react": "^16.3.2",
|
|
88
88
|
"@types/node": "^22.20.0",
|
|
@@ -41,6 +41,13 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
41
41
|
descriptionKey: TKey
|
|
42
42
|
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
43
43
|
descriptionNode?: ReactNode
|
|
44
|
+
/**
|
|
45
|
+
* Optional leading mark identifying the plugin. A card without one keeps the
|
|
46
|
+
* plain official header shape, so the chrome stays a readable sibling of the
|
|
47
|
+
* built-in PluginCard wherever a family plugin lands in the official bundle
|
|
48
|
+
* seat.
|
|
49
|
+
*/
|
|
50
|
+
icon?: ReactNode
|
|
44
51
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
45
52
|
state: CardShell
|
|
46
53
|
/** Write every staged edit. */
|
|
@@ -91,6 +98,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
91
98
|
const header = alwaysOpen === true
|
|
92
99
|
? (
|
|
93
100
|
<div className={css.headerStatic}>
|
|
101
|
+
{props.icon !== undefined ? <span className={css.mark} aria-hidden="true">{props.icon}</span> : null}
|
|
94
102
|
<span className={css.headText}>
|
|
95
103
|
<span className={css.name} title={title}>{title}</span>
|
|
96
104
|
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
@@ -106,6 +114,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
106
114
|
aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
|
|
107
115
|
onClick={() => { setOpen(!open) }}
|
|
108
116
|
>
|
|
117
|
+
{props.icon !== undefined ? <span className={css.mark} aria-hidden="true">{props.icon}</span> : null}
|
|
109
118
|
<span className={css.headText}>
|
|
110
119
|
<span className={css.name} title={title}>{title}</span>
|
|
111
120
|
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
@@ -9,6 +9,7 @@ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-cli
|
|
|
9
9
|
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
10
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
11
11
|
import { PluginSettingsCard, ValueField, BooleanField } from './PluginSettingsCard.tsx'
|
|
12
|
+
import { PhoneIcon } from './PhoneIcon.tsx'
|
|
12
13
|
import { CardForm, booleanField, numberField, secretField, textField, type CardActions, type CardShell, type FieldState as CardFieldState } from './settings-form.ts'
|
|
13
14
|
import { LanBindStatusError, readLanBindStatus, shouldStopLanBindPoll, type LanBindFrame } from './pair-api.ts'
|
|
14
15
|
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
@@ -173,6 +174,7 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
|
|
|
173
174
|
t={t}
|
|
174
175
|
titleKey="settings.title"
|
|
175
176
|
descriptionKey="settings.description"
|
|
177
|
+
icon={<PhoneIcon size={18} />}
|
|
176
178
|
defaultOpen={false}
|
|
177
179
|
state={state}
|
|
178
180
|
onSave={props.save}
|
package/src/client/index.ts
CHANGED
|
@@ -242,9 +242,9 @@ export function apply(ctx: ClientContext): void {
|
|
|
242
242
|
|
|
243
243
|
const t = ctx.locale.bind(NS)
|
|
244
244
|
// Hand the `remote` translate seat to the module-scope adaptation layer:
|
|
245
|
-
// the whale
|
|
246
|
-
//
|
|
247
|
-
//
|
|
245
|
+
// the whale label was rendered with the English fallback before any
|
|
246
|
+
// dictionary existed; the wiring plus the layer's own sync tick re-renders
|
|
247
|
+
// it in the active locale.
|
|
248
248
|
if (adapt !== undefined) adapt.translate = t
|
|
249
249
|
// The settings form this card edits. The family group's binder resolves a
|
|
250
250
|
// family namespace to the profile entry that owns it and binds that entry's
|
package/src/client/locales.ts
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
export const zh = {
|
|
5
5
|
'entry.label': '远程访问',
|
|
6
6
|
'mobile.whale.open': '打开侧边栏',
|
|
7
|
-
'mobile.composer.pickModel': '选择模型',
|
|
8
|
-
'mobile.composer.pickEffort': '选择推理等级',
|
|
9
7
|
'title': '远程访问',
|
|
10
8
|
'subtitle': '通过手机或另一台电脑配对,远程使用同一份 Web 界面(官方界面 + 移动端适配)',
|
|
11
9
|
'card.title': '设备配对',
|
|
@@ -15,7 +13,7 @@ export const zh = {
|
|
|
15
13
|
'status.disconnected': '已配对设备离线',
|
|
16
14
|
'status.stopped': '已停止远程访问',
|
|
17
15
|
'status.lanRequired': '此功能需要局域网绑定或公网地址才能使用',
|
|
18
|
-
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 →
|
|
16
|
+
'status.lanRequiredHint': '当前服务仅绑定在 127.0.0.1 且未配置公网地址,手机无法访问。请在本插件的设置卡片中打开“局域网访问”(装在全家桶里时在「设置 → Web 插件 → 远程访问」,单独安装时在设置 → 插件 → 本插件行),或用 dsh web --host 0.0.0.0 重新启动,或填写内网穿透的公网地址。',
|
|
19
17
|
'status.loopbackRequired': '配对面板仅限本机使用',
|
|
20
18
|
'status.loopbackRequiredHint': '请通过 http://127.0.0.1 打开此页面后重试;手机请使用配对链接访问。',
|
|
21
19
|
'status.unreachable': '无法连接配对服务',
|
|
@@ -72,8 +70,8 @@ export const zh = {
|
|
|
72
70
|
'relay.failed': '固定域名同步失败:{error}。正在自动重试,期间二维码可能使用临时地址。',
|
|
73
71
|
'close.label': '关闭远程访问面板',
|
|
74
72
|
// 插件设置卡片(家族 list 席位,或官方 plugins.bundle.config 席位)。
|
|
75
|
-
'settings.title': '
|
|
76
|
-
'settings.description': '
|
|
73
|
+
'settings.title': '远程访问',
|
|
74
|
+
'settings.description': '手机配对、公网隧道与设备限额。',
|
|
77
75
|
'settings.enabled': '启用远程访问',
|
|
78
76
|
'settings.enabledHint': '关闭后移除侧边栏入口并停用配对路由与局域网栅栏。',
|
|
79
77
|
'settings.tokenTtlMs': '配对令牌有效期(毫秒)',
|
|
@@ -135,8 +133,6 @@ export type SettingsCardKey = RemoteKey
|
|
|
135
133
|
export const en = {
|
|
136
134
|
'entry.label': 'Remote access',
|
|
137
135
|
'mobile.whale.open': 'Open sidebar',
|
|
138
|
-
'mobile.composer.pickModel': 'Pick model',
|
|
139
|
-
'mobile.composer.pickEffort': 'Pick reasoning effort',
|
|
140
136
|
'title': 'Remote access',
|
|
141
137
|
'subtitle': 'Pair a phone or another computer to share the same Web GUI (official UI + mobile adaptation)',
|
|
142
138
|
'card.title': 'Pair a device',
|
|
@@ -203,8 +199,8 @@ export const en = {
|
|
|
203
199
|
'relay.failed': 'Stable-hostname sync failed: {error}. Retrying automatically; the QR may use the ephemeral address meanwhile.',
|
|
204
200
|
'close.label': 'Close remote access panel',
|
|
205
201
|
// Plugin settings card (the family list seat, or the official bundle-config seat).
|
|
206
|
-
'settings.title': 'Remote access
|
|
207
|
-
'settings.description': '
|
|
202
|
+
'settings.title': 'Remote access',
|
|
203
|
+
'settings.description': 'Phone pairing, public tunnel and device limits.',
|
|
208
204
|
'settings.enabled': 'Enable remote access',
|
|
209
205
|
'settings.enabledHint': 'When off, the sidebar entry is removed and pairing routes plus the LAN fence stop.',
|
|
210
206
|
'settings.tokenTtlMs': 'Pairing token lifetime (ms)',
|
|
@@ -32,8 +32,8 @@ export interface RemoteAdaptGlobal {
|
|
|
32
32
|
closeDetails: (() => void) | null
|
|
33
33
|
/**
|
|
34
34
|
* Wired by the plugin apply once the `remote` locale namespace is bound:
|
|
35
|
-
* the translate seat the injected
|
|
36
|
-
*
|
|
35
|
+
* the translate seat the injected surface (the whale) reads its label
|
|
36
|
+
* from. Null until then; label reads fall back to English
|
|
37
37
|
* (the SDK's universal fallback) and the 600ms sync tick re-renders the
|
|
38
38
|
* labels once the seat is wired.
|
|
39
39
|
*/
|
|
@@ -67,12 +67,6 @@ const STYLE_SELECTOR = `style[data-plugin-css="${ADAPT_CSS_ID}"]`
|
|
|
67
67
|
|
|
68
68
|
/** Whale button id. */
|
|
69
69
|
const WHALE_ID = 'dshRemoteWhale'
|
|
70
|
-
/** Compact picker: synthesized model button id. */
|
|
71
|
-
const MODEL_BTN_ID = 'dshRemoteModelPick'
|
|
72
|
-
/** Compact picker: synthesized effort button id. */
|
|
73
|
-
const EFFORT_BTN_ID = 'dshRemoteEffortPick'
|
|
74
|
-
/** Body class while the compact picker buttons are wired. */
|
|
75
|
-
const COMPACT_CLASS = 'dsh-remote-compact-picker'
|
|
76
70
|
/** Body class while the header actions are seated in the tabs row. */
|
|
77
71
|
const HEADER_SEATED_CLASS = 'dsh-remote-header-seated'
|
|
78
72
|
/**
|
|
@@ -81,15 +75,6 @@ const HEADER_SEATED_CLASS = 'dsh-remote-header-seated'
|
|
|
81
75
|
* reserving the space with padding.
|
|
82
76
|
*/
|
|
83
77
|
const HEADER_RESERVE_VAR = '--dsh-remote-header-actions-reserve'
|
|
84
|
-
/** The two composer picker entries the compact buttons drill into. */
|
|
85
|
-
type PickerKind = 'model' | 'effort'
|
|
86
|
-
/** Locale-dependent fast path for the official picker cells (zh/en). */
|
|
87
|
-
const PICKER_CELL_PATTERN: Record<PickerKind, RegExp> = {
|
|
88
|
-
model: /模型|Model/, // i18n-allow: matches official picker cell text, not plugin copy
|
|
89
|
-
effort: /推理等级|Reasoning|Effort/i, // i18n-allow: matches official picker cell text, not plugin copy
|
|
90
|
-
}
|
|
91
|
-
/** Position of each drill cell among the sheet's chevron cells. */
|
|
92
|
-
const DRILL_INDEX: Record<PickerKind, number> = { model: 0, effort: 1 }
|
|
93
78
|
/**
|
|
94
79
|
* The official application frame. The layout column classes are the anchor:
|
|
95
80
|
* `_frame` is shared by unrelated official components (the chat turn rail,
|
|
@@ -188,14 +173,41 @@ const ADAPT_CSS: readonly string[] = [
|
|
|
188
173
|
'[class$="_headerUtilities"]{display:none}',
|
|
189
174
|
// v52 composer: the two lines (permission / model) stay stacked with zero
|
|
190
175
|
// row gap; the command, context-meter and send/stop buttons float at the
|
|
191
|
-
// left/right edges vertically centered over both lines.
|
|
176
|
+
// left/right edges vertically centered over both lines. "Both lines" is what
|
|
177
|
+
// makes _row the containing block for those three floats: _row is the one box
|
|
178
|
+
// that wraps the lines, so nothing between a float and _row may claim it.
|
|
192
179
|
'[class$="_composerSeat"] [class$="_card"]{margin-bottom:1px}',
|
|
193
180
|
'[class$="_composerSeat"] [class$="_row"]{flex-wrap:wrap;row-gap:0;padding:2px 8px 1px;position:relative}',
|
|
194
|
-
|
|
181
|
+
// The attach button is the only file-picker entry on the portrait composer,
|
|
182
|
+
// and taking it out of flow costs it the paint order it had inline: a
|
|
183
|
+
// positioned sibling painted after it (the trailing line, see below) covers
|
|
184
|
+
// it and eats the taps on the covered half. Its own z-index makes the hit
|
|
185
|
+
// layer a property of the button instead of a side effect of a sibling
|
|
186
|
+
// (#1829). It clears the composer's own stacking ladder and stays far below
|
|
187
|
+
// the bottom-sheet menus (2147482000), which own the overlay above it.
|
|
188
|
+
'[class$="_composerSeat"] [class$="_add"]{position:absolute;left:8px;top:50%;transform:translateY(-50%);z-index:10}',
|
|
195
189
|
'[class$="_composerSeat"] [class$="_modes"]{min-width:0;padding-left:38px}',
|
|
196
190
|
// Model line left-aligned with the permission line (same command-button
|
|
197
191
|
// clearance), rows stay tightly stacked.
|
|
198
|
-
|
|
192
|
+
// The trailing line is forced onto its own row at flex-basis:100% and carries
|
|
193
|
+
// 116px of side padding (38 left for the command button, 78 right for the
|
|
194
|
+
// send button). Under the default content-box that padding is ADDED to the
|
|
195
|
+
// 100% basis, so the line's own box overflowed the card by exactly 116px and
|
|
196
|
+
// dragged the absolutely-positioned send button out with it: on a 393px phone
|
|
197
|
+
// the card ended at x=367 while the button sat at x=433..467, past the
|
|
198
|
+
// viewport's right edge, which is the unreachable button the reporter
|
|
199
|
+
// measured (#1818). border-box contains the padding inside the basis, so the
|
|
200
|
+
// line stays within the card and the button lands back inside it.
|
|
201
|
+
//
|
|
202
|
+
// The line stays UNPOSITIONED on purpose. position:relative arrived here with
|
|
203
|
+
// the compact picker and outlived it, but this box is the LAST line only: as
|
|
204
|
+
// the nearest positioned ancestor it took the containing block from _row, so
|
|
205
|
+
// the send button and the context meter centered inside the trailing line
|
|
206
|
+
// instead of over both lines - the send button rode the input's bottom edge,
|
|
207
|
+
// out of line with the attach button - and, painted after the out-of-flow
|
|
208
|
+
// attach button, the line covered that button's lower half and swallowed the
|
|
209
|
+
// taps there (#1829).
|
|
210
|
+
'[class$="_composerSeat"] [class$="_trailing"]{box-sizing:border-box;flex-basis:100%;min-height:32px;justify-content:flex-start;padding-left:38px;padding-right:78px}',
|
|
199
211
|
'[class$="_composerSeat"] [class$="_trailing"] *{font-size:12px}',
|
|
200
212
|
// v54: smaller permission/model buttons (font + height). v79: the
|
|
201
213
|
// permission trigger collapses to its shield icon on phones — the label
|
|
@@ -218,26 +230,12 @@ const ADAPT_CSS: readonly string[] = [
|
|
|
218
230
|
'[class$="_composerSeat"]{transform:none !important}',
|
|
219
231
|
'[class$="_composerSeat"] [class$="_menu"]{position:fixed !important;left:8px !important;right:8px !important;top:auto !important;bottom:calc(8px + env(safe-area-inset-bottom)) !important;width:auto !important;max-width:none !important;max-height:70dvh !important;overflow-y:auto !important;z-index:2147482000}',
|
|
220
232
|
'[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]{height:44px;min-height:44px;font-size:13px}',
|
|
221
|
-
// v79
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// degrades back to the usable text trigger instead of no picker.
|
|
228
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"]){display:none}`,
|
|
229
|
-
// The icon buttons sit inline in the tools row (parallel to the
|
|
230
|
-
// permission trigger), so the trailing line collapses to zero and the
|
|
231
|
-
// context ring + send re-anchor to the row itself. The ring shifts a
|
|
232
|
-
// few px right: at its desktop offset its hit box kisses the effort
|
|
233
|
-
// button.
|
|
234
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"]{flex-basis:auto;position:static;min-height:0;padding:0;width:0}`,
|
|
235
|
-
`body.${COMPACT_CLASS} [class$="_composerSeat"] [class$="_trailing"] > [class$="_root"]:has([class$="_track"]){right:44px}`,
|
|
236
|
-
`#${MODEL_BTN_ID},#${EFFORT_BTN_ID}{width:26px;height:32px;min-width:26px;padding:0;border-radius:9px;background:var(--dsw-alias-bg-module-platform);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:none;margin-left:4px}`,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
`#${MODEL_BTN_ID} svg,#${EFFORT_BTN_ID} svg{width:16px;height:16px;display:block}`,
|
|
240
|
-
`#${MODEL_BTN_ID}:active,#${EFFORT_BTN_ID}:active{opacity:.7}`,
|
|
233
|
+
// v79's compact picker (two synthesized model/effort icon buttons in the
|
|
234
|
+
// tools row, the trailing text trigger hidden behind a body class, and the
|
|
235
|
+
// trailing line collapsed to zero width) was removed at the user's request.
|
|
236
|
+
// The portrait composer keeps the official text trigger with the trailing
|
|
237
|
+
// line rules above; the bottom-sheet rules immediately above still apply to
|
|
238
|
+
// the official menu.
|
|
241
239
|
// Bottom stats line (N rounds / M steps) two sizes below the tabs; v55
|
|
242
240
|
// wraps freely but clamps at two lines (the official rule is nowrap +
|
|
243
241
|
// single-line ellipsis); v60 drops the official 32px right padding.
|
|
@@ -311,20 +309,23 @@ const ADAPT_CSS: readonly string[] = [
|
|
|
311
309
|
// v68: settings modal on mobile — the official panel is a fixed 800px
|
|
312
310
|
// two-column layout (nav + content); switch to a column layout: the
|
|
313
311
|
// section nav becomes a horizontal scrollable row on top.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
312
|
+
// Scoped to the portrait body class like the suppressions above: these
|
|
313
|
+
// are the only rules in this array that reshape an official two-column
|
|
314
|
+
// surface, and the overlay portal is shared with every other dialog.
|
|
315
|
+
// Unscoped, the tag — which lives in <head> for as long as the layer
|
|
316
|
+
// is active and is restored by the sync tick after a revert —
|
|
317
|
+
// collapsed the DESKTOP settings panel into a stacked column (measured
|
|
318
|
+
// on a real 1440px host: panel flex-direction row -> column, nav 800px
|
|
319
|
+
// tall -> 236px, content 612px -> 800px wide and pushed below the nav).
|
|
320
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"]{flex-direction:column;max-height:calc(100dvh - 32px)}`,
|
|
321
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_nav"]{flex-direction:row;gap:4px;width:100%;padding:12px 12px 0;overflow-x:auto;overflow-y:hidden}`,
|
|
322
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navTitle"]{display:none}`,
|
|
323
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navList"]{flex-direction:row;gap:4px}`,
|
|
324
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navCell"]{height:34px;padding:0 12px;gap:6px;flex:none;border-radius:10px}`,
|
|
325
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_navLabel"]{font-size:13px}`,
|
|
326
|
+
`body.${ACTIVE_CLASS} [class$="_overlay"] [class$="_panel"] [class$="_content"]{flex:1;min-height:0}`,
|
|
321
327
|
]
|
|
322
328
|
|
|
323
|
-
/** Cube glyph for the compact model button (a plain box outline). */
|
|
324
|
-
const CUBE_ICON = '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>'
|
|
325
|
-
/** Level glyph for the compact effort button (three rising bars). */
|
|
326
|
-
const LEVELS_ICON = '<path d="M4 6h16"/><path d="M7 12h10"/><path d="M10 18h4"/>'
|
|
327
|
-
|
|
328
329
|
/** The DeepSeek fish glyph (the official brand mark path). */
|
|
329
330
|
const FISH_PATH = 'M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018Z'
|
|
330
331
|
|
|
@@ -435,7 +436,6 @@ export function startMobileAdapt(): void {
|
|
|
435
436
|
active = false
|
|
436
437
|
unseatHeaderActions()
|
|
437
438
|
restoreRowDrag()
|
|
438
|
-
removeCompactPicker()
|
|
439
439
|
document.body.classList.remove(ACTIVE_CLASS)
|
|
440
440
|
document.body.classList.remove(RAIL_HIDDEN_CLASS)
|
|
441
441
|
const tag = document.querySelector(`style[data-plugin-css="${ADAPT_CSS_ID}"]`)
|
|
@@ -505,88 +505,6 @@ export function startMobileAdapt(): void {
|
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
/**
|
|
509
|
-
* Compact picker (v79): a phone row cannot fit the desktop text triggers,
|
|
510
|
-
* so the model/effort entries become two icon buttons in the trailing
|
|
511
|
-
* row. Both forward to the official picker trigger (its menu renders as
|
|
512
|
-
* the bottom sheet) and then drill straight into the asked cell — model
|
|
513
|
-
* list or effort list — so one tap lands on the list, matching the
|
|
514
|
-
* cube-model / brain-effort mapping. The official context ring next to
|
|
515
|
-
* the send button keeps its own semantics untouched.
|
|
516
|
-
*/
|
|
517
|
-
function removeCompactPicker(): void {
|
|
518
|
-
document.body.classList.remove(COMPACT_CLASS)
|
|
519
|
-
document.getElementById(MODEL_BTN_ID)?.remove()
|
|
520
|
-
document.getElementById(EFFORT_BTN_ID)?.remove()
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function drillIntoPicker(kind: PickerKind): void {
|
|
524
|
-
const trigger = document.querySelector('[class$="_composerSeat"] [class$="_trailing"] [class$="_trigger"]:has([class$="_triggerEffort"])') as HTMLElement | null
|
|
525
|
-
if (trigger === null) return
|
|
526
|
-
trigger.click()
|
|
527
|
-
// The sheet mount takes a beat (observed ~0.2-0.6s on a cold phone
|
|
528
|
-
// mirror); poll until the asked cell exists instead of a fixed delay.
|
|
529
|
-
let tries = 0
|
|
530
|
-
const tapCell = (): void => {
|
|
531
|
-
tries += 1
|
|
532
|
-
const cells = Array.from(document.querySelectorAll('[class$="_composerSeat"] [class$="_menu"] [class$="_cell"]'))
|
|
533
|
-
// The official cell copy is localized (zh/en/ru), so the label match is
|
|
534
|
-
// only the fast path; the sheet's drill cells (label + value + chevron)
|
|
535
|
-
// are the structural anchor: model first, effort second, on every locale.
|
|
536
|
-
const byLabel = cells.find((c) => PICKER_CELL_PATTERN[kind].test(c.textContent ?? ''))
|
|
537
|
-
const drillable = cells.filter((c) => c.querySelector('[class*="_cellChevron"], [class*="_chevron"]'))
|
|
538
|
-
const cell = byLabel ?? drillable[DRILL_INDEX[kind]]
|
|
539
|
-
if (cell !== undefined) {
|
|
540
|
-
;(cell as HTMLElement).click()
|
|
541
|
-
return
|
|
542
|
-
}
|
|
543
|
-
if (tries < 8) window.setTimeout(tapCell, 150)
|
|
544
|
-
}
|
|
545
|
-
window.setTimeout(tapCell, 150)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function makeCompactButton(id: string, title: string, icon: string, kind: PickerKind): HTMLButtonElement {
|
|
549
|
-
const btn = document.createElement('button')
|
|
550
|
-
btn.id = id
|
|
551
|
-
btn.type = 'button'
|
|
552
|
-
btn.dataset.dshPlugin = 'remote-web-ui'
|
|
553
|
-
btn.title = title
|
|
554
|
-
btn.setAttribute('aria-label', title)
|
|
555
|
-
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icon}</svg>`
|
|
556
|
-
btn.addEventListener('click', () => { drillIntoPicker(kind) })
|
|
557
|
-
return btn
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function syncCompactPicker(): void {
|
|
561
|
-
if (!active) return
|
|
562
|
-
const tools = nodeOf('[class$="_composerSeat"] [class$="_tools"]')
|
|
563
|
-
const trigger = tools?.parentElement?.querySelector('[class$="_triggerEffort"]')?.parentElement
|
|
564
|
-
if (tools === null || trigger === null) {
|
|
565
|
-
removeCompactPicker()
|
|
566
|
-
return
|
|
567
|
-
}
|
|
568
|
-
if (document.getElementById(MODEL_BTN_ID) === null) {
|
|
569
|
-
// The cell pattern matches the OFFICIAL picker cell text (the official
|
|
570
|
-
// client's own zh/en copy), not plugin-owned text.
|
|
571
|
-
tools.appendChild(makeCompactButton(MODEL_BTN_ID, surfaceText('mobile.composer.pickModel', 'Pick model'), CUBE_ICON, 'model'))
|
|
572
|
-
}
|
|
573
|
-
if (document.getElementById(EFFORT_BTN_ID) === null) {
|
|
574
|
-
tools.appendChild(makeCompactButton(EFFORT_BTN_ID, surfaceText('mobile.composer.pickEffort', 'Pick reasoning effort'), LEVELS_ICON, 'effort'))
|
|
575
|
-
}
|
|
576
|
-
// Titles re-read every tick so a locale switch (or the translate seat
|
|
577
|
-
// arriving after this layer installed) is picked up without a reload.
|
|
578
|
-
for (const [id, key, fallback] of [
|
|
579
|
-
[MODEL_BTN_ID, 'mobile.composer.pickModel', 'Pick model'],
|
|
580
|
-
[EFFORT_BTN_ID, 'mobile.composer.pickEffort', 'Pick reasoning effort'],
|
|
581
|
-
] as const) {
|
|
582
|
-
const btn = document.getElementById(id)
|
|
583
|
-
if (btn === null) continue
|
|
584
|
-
const label = surfaceText(key, fallback)
|
|
585
|
-
btn.title = label
|
|
586
|
-
btn.setAttribute('aria-label', label)
|
|
587
|
-
}
|
|
588
|
-
document.body.classList.add(COMPACT_CLASS)
|
|
589
|
-
}
|
|
590
508
|
/**
|
|
591
509
|
* The official sidebar toggle in the logo row. The row's own `_toggle` class
|
|
592
510
|
* is the precise anchor; the row's last button is the fallback for cohorts
|
|
@@ -911,7 +829,6 @@ export function startMobileAdapt(): void {
|
|
|
911
829
|
disableRowDrag()
|
|
912
830
|
seatHeaderActions()
|
|
913
831
|
alignActionsText()
|
|
914
|
-
syncCompactPicker()
|
|
915
832
|
}
|
|
916
833
|
|
|
917
834
|
// v67: on mobile the header actions (agent-preset mode label + background
|