@linxin666/dsh-remote-web-ui 0.3.5 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +77 -468
- package/README.zh.md +83 -113
- package/lib/client.js +955 -203
- package/lib/client.js.map +1 -1
- package/lib/index.js +1136 -1466
- package/lib/types/client/FooterRemoteEntry.d.ts +6 -7
- package/lib/types/client/FooterRemoteEntry.d.ts.map +1 -1
- package/lib/types/client/FooterRemoteEntry.js +1 -1
- package/lib/types/client/RemoteEntry.d.ts +7 -4
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +7 -10
- package/lib/types/client/RemotePanel.d.ts +2 -2
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +2 -2
- package/lib/types/client/RemoteSettingsCard.d.ts +9 -5
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +56 -3
- package/lib/types/client/deep-link.d.ts +10 -13
- package/lib/types/client/deep-link.d.ts.map +1 -1
- package/lib/types/client/deep-link.js +14 -74
- package/lib/types/client/index.d.ts +2 -6
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +73 -39
- package/lib/types/client/locales.d.ts +26 -12
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +36 -22
- package/lib/types/client/mobile-adapt.d.ts +40 -0
- package/lib/types/client/mobile-adapt.d.ts.map +1 -0
- package/lib/types/client/mobile-adapt.js +967 -0
- package/lib/types/client/pair-api.d.ts +24 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +9 -14
- package/lib/types/client/remote-channel.d.ts +3 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +35 -3
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +60 -53
- package/lib/types/firewall.d.ts +75 -0
- package/lib/types/firewall.d.ts.map +1 -0
- package/lib/types/firewall.js +184 -0
- package/lib/types/gate.d.ts +11 -5
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +11 -5
- package/lib/types/index.d.ts +39 -10
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +206 -58
- package/lib/types/inner-auth.d.ts +32 -0
- package/lib/types/inner-auth.d.ts.map +1 -0
- package/lib/types/inner-auth.js +72 -0
- package/lib/types/lan-bind-plan.d.ts +55 -0
- package/lib/types/lan-bind-plan.d.ts.map +1 -0
- package/lib/types/lan-bind-plan.js +60 -0
- package/lib/types/lan-bind.d.ts +69 -0
- package/lib/types/lan-bind.d.ts.map +1 -0
- package/lib/types/lan-bind.js +128 -0
- package/lib/types/loopback-proxy.d.ts +9 -2
- package/lib/types/loopback-proxy.d.ts.map +1 -1
- package/lib/types/loopback-proxy.js +24 -2
- package/lib/types/pairing.d.ts +15 -3
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +20 -6
- package/lib/types/remote-api.d.ts +30 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +55 -42
- package/lib/types/remote-channel-boot.d.ts +21 -2
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +65 -3
- package/lib/types/remote-channel-rules.d.ts +8 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +9 -2
- package/lib/types/remote-methods.d.ts +45 -11
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +56 -27
- package/lib/types/remote-presence-pet.d.ts +51 -0
- package/lib/types/remote-presence-pet.d.ts.map +1 -0
- package/lib/types/remote-presence-pet.js +101 -0
- package/lib/types/routes.d.ts +76 -4
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +336 -19
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -1
- package/package.json +20 -18
- package/src/client/FooterRemoteEntry.tsx +9 -10
- package/src/client/RemoteEntry.tsx +13 -14
- package/src/client/RemotePanel.tsx +5 -16
- package/src/client/RemoteSettingsCard.tsx +72 -13
- package/src/client/deep-link.ts +14 -75
- package/src/client/index.ts +77 -39
- package/src/client/locales.ts +36 -22
- package/src/client/mobile-adapt.ts +913 -0
- package/src/client/pair-api.ts +26 -15
- package/src/client/remote-channel.ts +31 -3
- package/src/client/settings-form.ts +78 -92
- package/src/dsh-home.ts +1 -1
- package/src/firewall.ts +230 -0
- package/src/gate.ts +11 -5
- package/src/index.ts +235 -65
- package/src/inner-auth.ts +85 -0
- package/src/lan-bind-plan.ts +81 -0
- package/src/lan-bind.ts +145 -0
- package/src/loopback-proxy.ts +34 -0
- package/src/pairing.persist.test.ts +1 -1
- package/src/pairing.ts +20 -6
- package/src/remote-api.ts +67 -45
- package/src/remote-channel-boot.ts +70 -3
- package/src/remote-channel-rules.ts +17 -2
- package/src/remote-methods.ts +58 -27
- package/src/remote-presence-pet.ts +129 -0
- package/src/routes.ts +358 -19
- package/src/tunnel.ts +7 -1
- package/assets/apple-touch-icon.png +0 -0
- package/assets/mobile-icon-192.png +0 -0
- package/assets/mobile-icon-512.png +0 -0
- package/assets/mobile-service-worker.js +0 -63
- package/lib/mobile.js +0 -17621
- package/lib/mobile.js.map +0 -1
- package/lib/types/mobile/PairRequiredView.d.ts +0 -7
- package/lib/types/mobile/PairRequiredView.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.js +0 -28
- package/lib/types/mobile/PairRequiredView.test.d.ts +0 -2
- package/lib/types/mobile/PairRequiredView.test.d.ts.map +0 -1
- package/lib/types/mobile/PairRequiredView.test.js +0 -24
- package/lib/types/mobile/api.d.ts +0 -145
- package/lib/types/mobile/api.d.ts.map +0 -1
- package/lib/types/mobile/api.js +0 -103
- package/lib/types/mobile/index.d.ts +0 -10
- package/lib/types/mobile/index.d.ts.map +0 -1
- package/lib/types/mobile/index.js +0 -38
- package/lib/types/mobile/markdown.d.ts +0 -34
- package/lib/types/mobile/markdown.d.ts.map +0 -1
- package/lib/types/mobile/markdown.js +0 -285
- package/lib/types/mobile/markdown.test.d.ts +0 -2
- package/lib/types/mobile/markdown.test.d.ts.map +0 -1
- package/lib/types/mobile/markdown.test.js +0 -79
- package/lib/types/mobile/messages.d.ts +0 -141
- package/lib/types/mobile/messages.d.ts.map +0 -1
- package/lib/types/mobile/messages.js +0 -553
- package/lib/types/mobile/messages.test.d.ts +0 -2
- package/lib/types/mobile/messages.test.d.ts.map +0 -1
- package/lib/types/mobile/messages.test.js +0 -356
- package/lib/types/mobile/mobile-styles.d.ts +0 -6
- package/lib/types/mobile/mobile-styles.d.ts.map +0 -1
- package/lib/types/mobile/mobile-styles.js +0 -1438
- package/lib/types/mobile/mobile-theme.d.ts +0 -22
- package/lib/types/mobile/mobile-theme.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.js +0 -70
- package/lib/types/mobile/mobile-theme.test.d.ts +0 -2
- package/lib/types/mobile/mobile-theme.test.d.ts.map +0 -1
- package/lib/types/mobile/mobile-theme.test.js +0 -71
- package/lib/types/mobile/mux.d.ts +0 -128
- package/lib/types/mobile/mux.d.ts.map +0 -1
- package/lib/types/mobile/mux.js +0 -305
- package/lib/types/mobile/mux.test.d.ts +0 -2
- package/lib/types/mobile/mux.test.d.ts.map +0 -1
- package/lib/types/mobile/mux.test.js +0 -267
- package/lib/types/mobile/pairing.d.ts +0 -31
- package/lib/types/mobile/pairing.d.ts.map +0 -1
- package/lib/types/mobile/pairing.js +0 -62
- package/lib/types/mobile/rpc.d.ts +0 -38
- package/lib/types/mobile/rpc.d.ts.map +0 -1
- package/lib/types/mobile/rpc.js +0 -83
- package/lib/types/mobile/theme-toggle.d.ts +0 -12
- package/lib/types/mobile/theme-toggle.d.ts.map +0 -1
- package/lib/types/mobile/theme-toggle.js +0 -26
- package/lib/types/mobile/views/App.d.ts +0 -57
- package/lib/types/mobile/views/App.d.ts.map +0 -1
- package/lib/types/mobile/views/App.js +0 -150
- package/lib/types/mobile/views/App.test.d.ts +0 -2
- package/lib/types/mobile/views/App.test.d.ts.map +0 -1
- package/lib/types/mobile/views/App.test.js +0 -52
- package/lib/types/mobile/views/ChatView.d.ts +0 -56
- package/lib/types/mobile/views/ChatView.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.js +0 -773
- package/lib/types/mobile/views/ChatView.stream.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.stream.test.js +0 -231
- package/lib/types/mobile/views/ChatView.test.d.ts +0 -2
- package/lib/types/mobile/views/ChatView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/ChatView.test.js +0 -718
- package/lib/types/mobile/views/SessionListView.d.ts +0 -27
- package/lib/types/mobile/views/SessionListView.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.js +0 -139
- package/lib/types/mobile/views/SessionListView.test.d.ts +0 -2
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/SessionListView.test.js +0 -167
- package/lib/types/mobile/views/WorkspaceView.d.ts +0 -21
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.js +0 -93
- package/lib/types/mobile/views/WorkspaceView.test.d.ts +0 -2
- package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +0 -1
- package/lib/types/mobile/views/WorkspaceView.test.js +0 -183
- package/lib/types/mobile-api.d.ts +0 -65
- package/lib/types/mobile-api.d.ts.map +0 -1
- package/lib/types/mobile-api.js +0 -404
- package/lib/types/mobile-pending.d.ts +0 -38
- package/lib/types/mobile-pending.d.ts.map +0 -1
- package/lib/types/mobile-pending.js +0 -59
- package/lib/types/mobile-pwa.d.ts +0 -6
- package/lib/types/mobile-pwa.d.ts.map +0 -1
- package/lib/types/mobile-pwa.js +0 -17
- package/lib/types/mobile-routes.d.ts +0 -13
- package/lib/types/mobile-routes.d.ts.map +0 -1
- package/lib/types/mobile-routes.js +0 -217
- package/lib/types/paired-model-catalog.d.ts +0 -17
- package/lib/types/paired-model-catalog.d.ts.map +0 -1
- package/lib/types/paired-model-catalog.js +0 -311
- package/src/mobile/PairRequiredView.test.tsx +0 -28
- package/src/mobile/PairRequiredView.tsx +0 -58
- package/src/mobile/api.ts +0 -229
- package/src/mobile/index.tsx +0 -41
- package/src/mobile/markdown.test.ts +0 -91
- package/src/mobile/markdown.ts +0 -291
- package/src/mobile/messages.test.ts +0 -402
- package/src/mobile/messages.ts +0 -677
- package/src/mobile/mobile-styles.ts +0 -1439
- package/src/mobile/mobile-theme.test.ts +0 -84
- package/src/mobile/mobile-theme.ts +0 -76
- package/src/mobile/mux.test.ts +0 -314
- package/src/mobile/mux.ts +0 -336
- package/src/mobile/pairing.ts +0 -77
- package/src/mobile/rpc.ts +0 -90
- package/src/mobile/theme-toggle.tsx +0 -47
- package/src/mobile/views/App.test.tsx +0 -62
- package/src/mobile/views/App.tsx +0 -213
- package/src/mobile/views/ChatView.stream.test.tsx +0 -263
- package/src/mobile/views/ChatView.test.tsx +0 -816
- package/src/mobile/views/ChatView.tsx +0 -1295
- package/src/mobile/views/SessionListView.test.tsx +0 -207
- package/src/mobile/views/SessionListView.tsx +0 -244
- package/src/mobile/views/WorkspaceView.test.tsx +0 -227
- package/src/mobile/views/WorkspaceView.tsx +0 -245
- package/src/mobile-api.ts +0 -418
- package/src/mobile-pending.ts +0 -88
- package/src/mobile-pwa.ts +0 -19
- package/src/mobile-routes.ts +0 -231
- package/src/paired-model-catalog.ts +0 -272
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { readCookie } from "./gate.js";
|
|
3
|
-
import { readBoundedJson, writeJson } from "./http.js";
|
|
4
|
-
import { isTrustedApiRequest, publicHostOf } from "./routes.js";
|
|
5
|
-
const MAX_BODY_BYTES = 16 * 1024;
|
|
6
|
-
const MAX_IDENTIFIER_LENGTH = 160;
|
|
7
|
-
const MAX_DISPLAY_NAME_LENGTH = 240;
|
|
8
|
-
const MAX_TOKEN_LIMIT = 10_000_000;
|
|
9
|
-
const REASONING_EFFORTS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
10
|
-
const stringField = (max) => z.string().trim().min(1).max(max).refine(value => !/[\r\n\0]/.test(value), 'must not contain a newline or NUL');
|
|
11
|
-
const providerSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH) }).strict();
|
|
12
|
-
const modelSchema = z.object({
|
|
13
|
-
id: stringField(MAX_IDENTIFIER_LENGTH),
|
|
14
|
-
name: stringField(MAX_DISPLAY_NAME_LENGTH).optional(),
|
|
15
|
-
contextWindow: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
|
|
16
|
-
maxTokens: z.number().int().safe().positive().max(MAX_TOKEN_LIMIT).optional(),
|
|
17
|
-
reasoningEfforts: z.array(z.enum(REASONING_EFFORTS)).min(1).refine(efforts => efforts.every((effort, index) => index === 0 || REASONING_EFFORTS.indexOf(efforts[index - 1]) < REASONING_EFFORTS.indexOf(effort)), 'reasoning efforts must be unique and in canonical order').optional(),
|
|
18
|
-
}).strict();
|
|
19
|
-
const upsertSchema = z.object({ provider: stringField(MAX_IDENTIFIER_LENGTH), model: modelSchema }).strict();
|
|
20
|
-
export const PAIRED_MODEL_CATALOG_PATHS = {
|
|
21
|
-
catalog: '/api/pair/model-catalog',
|
|
22
|
-
discover: '/api/pair/model-catalog/discover',
|
|
23
|
-
upsert: '/api/pair/model-catalog/upsert',
|
|
24
|
-
};
|
|
25
|
-
let rpcSequence = 0;
|
|
26
|
-
function request(payload) {
|
|
27
|
-
rpcSequence += 1;
|
|
28
|
-
return { rpcId: `paired-model-catalog-${Date.now().toString(36)}-${rpcSequence.toString(36)}`, payload };
|
|
29
|
-
}
|
|
30
|
-
function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); }
|
|
31
|
-
function valueOf(response) { return response.result.ok ? response.result.value : undefined; }
|
|
32
|
-
function errorCode(response) { return response.result.ok ? undefined : response.result.error.code; }
|
|
33
|
-
function reject(res, status, error) { writeJson(res, status, { error }); }
|
|
34
|
-
function publicProviderId(value) {
|
|
35
|
-
const normalized = value.replace(/[\r\n\0]/g, ' ').trim().slice(0, MAX_IDENTIFIER_LENGTH);
|
|
36
|
-
return normalized === '' ? 'unknown' : normalized;
|
|
37
|
-
}
|
|
38
|
-
function publicCatalog(value) {
|
|
39
|
-
return {
|
|
40
|
-
groups: value.groups.map(group => ({ id: group.id, name: group.name, models: group.models.map(model => ({
|
|
41
|
-
id: model.id, name: model.name,
|
|
42
|
-
...model.description === undefined ? {} : { description: model.description },
|
|
43
|
-
...model.reasoning === undefined ? {} : { reasoning: model.reasoning },
|
|
44
|
-
})) })),
|
|
45
|
-
failures: value.failures.map(failure => ({
|
|
46
|
-
id: failure.id,
|
|
47
|
-
name: failure.name,
|
|
48
|
-
message: `model catalog unavailable for provider ${publicProviderId(failure.id)}`,
|
|
49
|
-
})),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function publicCandidates(models) {
|
|
53
|
-
return models.map(model => ({ id: model.id,
|
|
54
|
-
...model.name === undefined ? {} : { name: model.name },
|
|
55
|
-
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
56
|
-
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
function fieldsOf(model, includeId) {
|
|
60
|
-
const fields = {
|
|
61
|
-
...includeId ? { id: model.id } : {},
|
|
62
|
-
...model.name === undefined ? {} : { name: model.name },
|
|
63
|
-
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
64
|
-
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
65
|
-
};
|
|
66
|
-
if (model.reasoningEfforts !== undefined)
|
|
67
|
-
fields.reasoningEfforts = Object.fromEntries(model.reasoningEfforts.map(effort => [effort, effort === 'off' ? null : effort]));
|
|
68
|
-
return fields;
|
|
69
|
-
}
|
|
70
|
-
function groupFor(catalog, provider) {
|
|
71
|
-
if (catalog.failures.some(failure => failure.id === provider))
|
|
72
|
-
return undefined;
|
|
73
|
-
return catalog.groups.find(group => group.id === provider);
|
|
74
|
-
}
|
|
75
|
-
async function readCatalog(apiProxy) {
|
|
76
|
-
try {
|
|
77
|
-
return valueOf(await apiProxy.llm.models(request({})));
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** Resolve one exact active llm-pi-ai provider address, never an input-supplied path. */
|
|
84
|
-
async function eligibleProvider(apiProxy, requested) {
|
|
85
|
-
let providersResponse;
|
|
86
|
-
let settingsResponse;
|
|
87
|
-
try {
|
|
88
|
-
providersResponse = await apiProxy.llm.providers(request({}));
|
|
89
|
-
settingsResponse = await apiProxy.settings.describe(request({}));
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return { status: 502, error: 'model catalog capability is unavailable' };
|
|
93
|
-
}
|
|
94
|
-
const providers = valueOf(providersResponse);
|
|
95
|
-
const settings = valueOf(settingsResponse);
|
|
96
|
-
if (providers === undefined || settings === undefined)
|
|
97
|
-
return { status: 502, error: 'model catalog capability is unavailable' };
|
|
98
|
-
const route = providers.providers.find(candidate => candidate.provider === requested);
|
|
99
|
-
if (route === undefined)
|
|
100
|
-
return { status: 404, error: `unknown provider ${requested}` };
|
|
101
|
-
if (!settings.writable || !route.active || route.settingsNs !== 'llm-pi-ai' || route.settingsPath.length !== 2
|
|
102
|
-
|| route.settingsPath[0] !== 'providers' || route.settingsPath[1] !== requested)
|
|
103
|
-
return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
|
|
104
|
-
const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
|
|
105
|
-
const root = namespace !== undefined && isRecord(namespace.value) ? namespace.value : undefined;
|
|
106
|
-
const providersValue = root !== undefined && isRecord(root.providers) ? root.providers : undefined;
|
|
107
|
-
const profile = providersValue !== undefined && isRecord(providersValue[requested]) ? providersValue[requested] : undefined;
|
|
108
|
-
if (namespace === undefined || profile === undefined)
|
|
109
|
-
return { status: 403, error: `provider ${requested} is not eligible for the paired model catalog` };
|
|
110
|
-
return { provider: { provider: requested, displayName: route.displayName, namespace, profile } };
|
|
111
|
-
}
|
|
112
|
-
function validIdentifier(value) {
|
|
113
|
-
const parsed = stringField(MAX_IDENTIFIER_LENGTH).safeParse(value);
|
|
114
|
-
return parsed.success && parsed.data === value;
|
|
115
|
-
}
|
|
116
|
-
function configuredModels(profile) {
|
|
117
|
-
if (!Object.hasOwn(profile, 'models'))
|
|
118
|
-
return { inherited: true };
|
|
119
|
-
if (!Array.isArray(profile.models))
|
|
120
|
-
return undefined;
|
|
121
|
-
if (profile.models.length === 0)
|
|
122
|
-
return { inherited: true };
|
|
123
|
-
const values = [];
|
|
124
|
-
for (const entry of profile.models) {
|
|
125
|
-
if (!isRecord(entry) || !validIdentifier(entry.id))
|
|
126
|
-
return undefined;
|
|
127
|
-
values.push({ ...entry });
|
|
128
|
-
}
|
|
129
|
-
return { inherited: false, values };
|
|
130
|
-
}
|
|
131
|
-
function configuredOverrides(profile) {
|
|
132
|
-
if (!Object.hasOwn(profile, 'modelOverrides'))
|
|
133
|
-
return { present: false, values: new Map() };
|
|
134
|
-
if (!isRecord(profile.modelOverrides))
|
|
135
|
-
return undefined;
|
|
136
|
-
const values = new Map();
|
|
137
|
-
for (const [id, value] of Object.entries(profile.modelOverrides)) {
|
|
138
|
-
if (!validIdentifier(id) || !isRecord(value))
|
|
139
|
-
return undefined;
|
|
140
|
-
values.set(id, { ...value });
|
|
141
|
-
}
|
|
142
|
-
return { present: true, values };
|
|
143
|
-
}
|
|
144
|
-
function updatedModels(existing, model) {
|
|
145
|
-
const next = existing.map(entry => ({ ...entry }));
|
|
146
|
-
const index = next.findIndex(entry => entry.id === model.id);
|
|
147
|
-
const patch = fieldsOf(model, true);
|
|
148
|
-
if (index >= 0)
|
|
149
|
-
next[index] = { ...next[index], ...patch };
|
|
150
|
-
else
|
|
151
|
-
next.push(patch);
|
|
152
|
-
return next;
|
|
153
|
-
}
|
|
154
|
-
function mutationPlan(profile, provider, model, catalog) {
|
|
155
|
-
const configured = configuredModels(profile);
|
|
156
|
-
const overrides = configuredOverrides(profile);
|
|
157
|
-
if (configured === undefined || overrides === undefined)
|
|
158
|
-
return undefined;
|
|
159
|
-
const modelsPath = ['providers', provider, 'models'];
|
|
160
|
-
const overridesPath = ['providers', provider, 'modelOverrides'];
|
|
161
|
-
if (!configured.inherited) {
|
|
162
|
-
if (overrides.values.size > 0)
|
|
163
|
-
return undefined;
|
|
164
|
-
const ops = [{ op: 'set', path: modelsPath, value: updatedModels(configured.values, model) }];
|
|
165
|
-
if (overrides.present)
|
|
166
|
-
ops.push({ op: 'unset', path: overridesPath });
|
|
167
|
-
return { ops };
|
|
168
|
-
}
|
|
169
|
-
const group = groupFor(catalog, provider);
|
|
170
|
-
if (group?.models.some(entry => entry.id === model.id) === true) {
|
|
171
|
-
const existing = overrides.values.get(model.id) ?? {};
|
|
172
|
-
return { ops: [{ op: 'set', path: [...overridesPath, model.id], value: { ...existing, ...fieldsOf(model, false) } }] };
|
|
173
|
-
}
|
|
174
|
-
if (group === undefined)
|
|
175
|
-
return undefined;
|
|
176
|
-
const materialized = [];
|
|
177
|
-
const materializedIds = new Set();
|
|
178
|
-
for (const installed of group.models) {
|
|
179
|
-
materialized.push({ ...overrides.values.get(installed.id), id: installed.id });
|
|
180
|
-
materializedIds.add(installed.id);
|
|
181
|
-
}
|
|
182
|
-
for (const [id, override] of overrides.values) {
|
|
183
|
-
if (!materializedIds.has(id))
|
|
184
|
-
materialized.push({ ...override, id });
|
|
185
|
-
}
|
|
186
|
-
const ops = [{ op: 'set', path: modelsPath, value: updatedModels(materialized, model) }];
|
|
187
|
-
if (overrides.present)
|
|
188
|
-
ops.push({ op: 'unset', path: overridesPath });
|
|
189
|
-
return { ops };
|
|
190
|
-
}
|
|
191
|
-
function paired(req, service) {
|
|
192
|
-
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
193
|
-
return deviceId !== undefined && service.touchDevice(deviceId);
|
|
194
|
-
}
|
|
195
|
-
async function bodyOf(req, schema) {
|
|
196
|
-
try {
|
|
197
|
-
return schema.parse(await readBoundedJson(req, MAX_BODY_BYTES));
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
return undefined;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/** Build the narrow paired-device model catalog routes. */
|
|
204
|
-
export function makePairedModelCatalogRoutes(deps) {
|
|
205
|
-
const { service, apiProxy, lanAddresses } = deps;
|
|
206
|
-
/** Same fence as the /api/pair family: loopback, the advertised LAN literals, or the configured public host. */
|
|
207
|
-
const fence = (req) => {
|
|
208
|
-
const publicHost = publicHostOf(service.publicBaseUrl);
|
|
209
|
-
return isTrustedApiRequest(req, publicHost === undefined ? lanAddresses : [...lanAddresses, publicHost]);
|
|
210
|
-
};
|
|
211
|
-
const gate = (req, res) => {
|
|
212
|
-
if (!fence(req)) {
|
|
213
|
-
req.resume();
|
|
214
|
-
reject(res, 403, 'paired model catalog is not reachable for this origin');
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
if (!paired(req, service)) {
|
|
218
|
-
req.resume();
|
|
219
|
-
reject(res, 403, 'paired model catalog requires a live paired device');
|
|
220
|
-
return false;
|
|
221
|
-
}
|
|
222
|
-
return true;
|
|
223
|
-
};
|
|
224
|
-
const catalog = async (req, res) => {
|
|
225
|
-
if (!gate(req, res))
|
|
226
|
-
return;
|
|
227
|
-
if (req.method !== 'GET')
|
|
228
|
-
return reject(res, 405, 'method not allowed');
|
|
229
|
-
let providersResponse;
|
|
230
|
-
let settingsResponse;
|
|
231
|
-
try {
|
|
232
|
-
providersResponse = await apiProxy.llm.providers(request({}));
|
|
233
|
-
settingsResponse = await apiProxy.settings.describe(request({}));
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return reject(res, 502, 'model catalog capability is unavailable');
|
|
237
|
-
}
|
|
238
|
-
const providers = valueOf(providersResponse);
|
|
239
|
-
const settings = valueOf(settingsResponse);
|
|
240
|
-
if (providers === undefined || settings === undefined)
|
|
241
|
-
return reject(res, 502, 'model catalog capability is unavailable');
|
|
242
|
-
const namespace = settings.namespaces.find(candidate => candidate.ns === 'llm-pi-ai');
|
|
243
|
-
const providerMap = namespace !== undefined && isRecord(namespace.value) && isRecord(namespace.value.providers) ? namespace.value.providers : undefined;
|
|
244
|
-
const eligible = settings.writable && providerMap !== undefined ? providers.providers.filter(candidate => candidate.active && candidate.settingsNs === 'llm-pi-ai'
|
|
245
|
-
&& candidate.settingsPath.length === 2 && candidate.settingsPath[0] === 'providers' && candidate.settingsPath[1] === candidate.provider && isRecord(providerMap[candidate.provider]))
|
|
246
|
-
.map(candidate => ({ provider: candidate.provider, displayName: candidate.displayName })) : [];
|
|
247
|
-
writeJson(res, 200, { capability: 'paired-model-catalog', providers: eligible });
|
|
248
|
-
};
|
|
249
|
-
const discover = async (req, res) => {
|
|
250
|
-
if (!gate(req, res))
|
|
251
|
-
return;
|
|
252
|
-
if (req.method !== 'POST')
|
|
253
|
-
return reject(res, 405, 'method not allowed');
|
|
254
|
-
const body = await bodyOf(req, providerSchema);
|
|
255
|
-
if (body === undefined)
|
|
256
|
-
return reject(res, 400, 'invalid model catalog request');
|
|
257
|
-
const eligible = await eligibleProvider(apiProxy, body.provider);
|
|
258
|
-
if (eligible.provider === undefined)
|
|
259
|
-
return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
|
|
260
|
-
try {
|
|
261
|
-
const result = valueOf(await apiProxy.llm.discoverModels(request({ settingsNs: 'llm-pi-ai', provider: eligible.provider.provider })));
|
|
262
|
-
if (result === undefined)
|
|
263
|
-
return reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
|
|
264
|
-
writeJson(res, 200, { models: publicCandidates(result.models) });
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
reject(res, 502, `model discovery failed for provider ${eligible.provider.provider}`);
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
const upsert = async (req, res) => {
|
|
271
|
-
if (!gate(req, res))
|
|
272
|
-
return;
|
|
273
|
-
if (req.method !== 'POST')
|
|
274
|
-
return reject(res, 405, 'method not allowed');
|
|
275
|
-
const body = await bodyOf(req, upsertSchema);
|
|
276
|
-
if (body === undefined)
|
|
277
|
-
return reject(res, 400, 'invalid model catalog request');
|
|
278
|
-
const eligible = await eligibleProvider(apiProxy, body.provider);
|
|
279
|
-
if (eligible.provider === undefined)
|
|
280
|
-
return reject(res, eligible.status ?? 502, eligible.error ?? 'model catalog capability is unavailable');
|
|
281
|
-
const before = await readCatalog(apiProxy);
|
|
282
|
-
if (before === undefined)
|
|
283
|
-
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
284
|
-
const plan = mutationPlan(eligible.provider.profile, body.provider, body.model, before);
|
|
285
|
-
if (plan === undefined)
|
|
286
|
-
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
287
|
-
let mutation;
|
|
288
|
-
try {
|
|
289
|
-
mutation = await apiProxy.settings.mutate(request({ ns: 'llm-pi-ai', expectedRevision: eligible.provider.namespace.revision, ops: plan.ops }));
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return reject(res, 502, `model update failed for provider ${body.provider}`);
|
|
293
|
-
}
|
|
294
|
-
const code = errorCode(mutation);
|
|
295
|
-
if (code === 'settings-conflict')
|
|
296
|
-
return reject(res, 409, `model update conflicted for provider ${body.provider}`);
|
|
297
|
-
if (code === 'settings-rejected')
|
|
298
|
-
return reject(res, 422, `model update was rejected for provider ${body.provider}`);
|
|
299
|
-
if (code !== undefined)
|
|
300
|
-
return reject(res, 502, `model update failed for provider ${body.provider}`);
|
|
301
|
-
const after = await readCatalog(apiProxy);
|
|
302
|
-
if (after === undefined)
|
|
303
|
-
return reject(res, 502, `model catalog is unavailable for provider ${body.provider}`);
|
|
304
|
-
writeJson(res, 200, publicCatalog(after));
|
|
305
|
-
};
|
|
306
|
-
return [
|
|
307
|
-
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.catalog, handler: catalog },
|
|
308
|
-
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.discover, handler: discover },
|
|
309
|
-
{ kind: 'exact', path: PAIRED_MODEL_CATALOG_PATHS.upsert, handler: upsert },
|
|
310
|
-
];
|
|
311
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
4
|
-
import { PairRequiredView } from './PairRequiredView.tsx'
|
|
5
|
-
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
cleanup()
|
|
8
|
-
vi.unstubAllGlobals()
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
describe('PairRequiredView', () => {
|
|
12
|
-
it('accepts a pasted link in the installed app context', async () => {
|
|
13
|
-
const fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }))
|
|
14
|
-
vi.stubGlobal('fetch', fetch)
|
|
15
|
-
const onPaired = vi.fn()
|
|
16
|
-
render(<PairRequiredView onPaired={onPaired} />)
|
|
17
|
-
|
|
18
|
-
fireEvent.change(screen.getByLabelText('配对链接'), { target: { value: 'https://phone.example/m/?pair=tok-1&workspace=ws-7' } })
|
|
19
|
-
fireEvent.click(screen.getByRole('button', { name: '配对' }))
|
|
20
|
-
|
|
21
|
-
await waitFor(() => expect(onPaired).toHaveBeenCalledWith('/m/?workspace=ws-7'))
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('shows an initial QR failure without starting the mobile data channel', () => {
|
|
25
|
-
render(<PairRequiredView initialError="配对链接已被使用。" onPaired={vi.fn()} />)
|
|
26
|
-
expect(screen.getByRole('alert').textContent).toContain('配对链接已被使用。')
|
|
27
|
-
})
|
|
28
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/** Device-pairing gate for installed mobile web-app contexts with isolated storage. */
|
|
2
|
-
import { type FormEvent, useState } from 'react'
|
|
3
|
-
import { acceptMobilePair, mobilePairPath, parseMobilePairInput } from './pairing.ts'
|
|
4
|
-
|
|
5
|
-
export interface PairRequiredViewProps {
|
|
6
|
-
initialError?: string
|
|
7
|
-
onPaired(path: string): void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Collect a fresh desktop-issued pairing link when this client has no paired cookie. */
|
|
11
|
-
export function PairRequiredView({ initialError, onPaired }: PairRequiredViewProps) {
|
|
12
|
-
const [value, setValue] = useState('')
|
|
13
|
-
const [error, setError] = useState<string | undefined>(initialError)
|
|
14
|
-
const [submitting, setSubmitting] = useState(false)
|
|
15
|
-
|
|
16
|
-
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
|
|
17
|
-
event.preventDefault()
|
|
18
|
-
const input = parseMobilePairInput(value)
|
|
19
|
-
if (input === undefined) {
|
|
20
|
-
setError('请输入有效的配对链接。')
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
setSubmitting(true)
|
|
25
|
-
setError(undefined)
|
|
26
|
-
const result = await acceptMobilePair(input.token)
|
|
27
|
-
setSubmitting(false)
|
|
28
|
-
if (!result.ok) {
|
|
29
|
-
setError(result.message)
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
onPaired(mobilePairPath(input.workspaceId))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<main className="mobile mobile-pair" aria-labelledby="mobile-pair-title">
|
|
38
|
-
<form className="mobile-pairCard" onSubmit={(event) => { void submit(event) }}>
|
|
39
|
-
<h1 id="mobile-pair-title" className="mobile-title">设备配对</h1>
|
|
40
|
-
<p className="mobile-muted">粘贴桌面端复制的配对链接以连接此设备。</p>
|
|
41
|
-
<label className="mobile-pairLabel" htmlFor="mobile-pair-link">配对链接</label>
|
|
42
|
-
<input
|
|
43
|
-
id="mobile-pair-link"
|
|
44
|
-
className="mobile-pairInput"
|
|
45
|
-
value={value}
|
|
46
|
-
onChange={(event) => { setValue(event.target.value) }}
|
|
47
|
-
autoComplete="off"
|
|
48
|
-
autoCapitalize="off"
|
|
49
|
-
spellCheck={false}
|
|
50
|
-
/>
|
|
51
|
-
{error === undefined ? null : <p className="mobile-error" role="alert">{error}</p>}
|
|
52
|
-
<button className="mobile-new mobile-pairSubmit" type="submit" disabled={submitting}>
|
|
53
|
-
{submitting ? '正在配对' : '配对'}
|
|
54
|
-
</button>
|
|
55
|
-
</form>
|
|
56
|
-
</main>
|
|
57
|
-
)
|
|
58
|
-
}
|
package/src/mobile/api.ts
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile-surface business API: the handful of host RPC methods the
|
|
3
|
-
* simplified surface needs. Types come from the harness apiproxy contract
|
|
4
|
-
* (type-only imports; the wire schemas stay in the bundle only through the
|
|
5
|
-
* rpc/mux layers).
|
|
6
|
-
*/
|
|
7
|
-
|
|
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'
|
|
10
|
-
import type { SessionSummary, SessionModels, SessionProjectionsBlock } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
|
|
11
|
-
import { callUnary } from './rpc.ts'
|
|
12
|
-
|
|
13
|
-
/** One session.list page. */
|
|
14
|
-
export interface SessionPage {
|
|
15
|
-
items: SessionSummary[]
|
|
16
|
-
/** Continuation cursor; undefined once the tail is reached. */
|
|
17
|
-
nextCursor?: string
|
|
18
|
-
hasMore: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** The session.create result (the id is the commit the caller navigates to). */
|
|
22
|
-
export interface CreatedSession {
|
|
23
|
-
sessionId: string
|
|
24
|
-
/** The composition the new session runs (echoed so the caller can label it). */
|
|
25
|
-
agentPreset?: string
|
|
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
|
-
|
|
35
|
-
/** One history page (already bounded to whole messages by the host). */
|
|
36
|
-
export interface HistoryPage {
|
|
37
|
-
events: import('@deepseek-ai/dsh-host-apiproxy/api/sessions').HistoryEntry[]
|
|
38
|
-
hasMore: boolean
|
|
39
|
-
/**
|
|
40
|
-
* Projection baseline riding the tail page (permissions select etc.);
|
|
41
|
-
* absent when the deployment mounts no projection registry.
|
|
42
|
-
*/
|
|
43
|
-
projections?: SessionProjectionsBlock
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Read-only display preferences the plugin answers locally on `/m/api`. */
|
|
47
|
-
export interface MobilePreferences {
|
|
48
|
-
/** Plain Enter sends the drafted prompt (false: Enter inserts a newline). */
|
|
49
|
-
mobileEnterToSend: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The workspace roster (session ids come back per workspace). */
|
|
53
|
-
export async function listWorkspaces(): Promise<WorkspaceView[]> {
|
|
54
|
-
const { items } = await callUnary<{ items: WorkspaceView[] }>('workspace.list', {})
|
|
55
|
-
return items
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Read-only mobile display preferences (answered by the plugin, not the host proxy). */
|
|
59
|
-
export async function fetchMobilePreferences(): Promise<MobilePreferences> {
|
|
60
|
-
return await callUnary<MobilePreferences>('mobile.preferences', {})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** One session.list page; omit the cursor for the first page. */
|
|
64
|
-
export async function listSessions(cursor?: string): Promise<SessionPage> {
|
|
65
|
-
return await callUnary<SessionPage>('session.list', cursor === undefined ? {} : { cursor })
|
|
66
|
-
}
|
|
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
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Create a blank session (entity birth precedes the first message). Name a
|
|
75
|
-
* workspace to attach it there, or a cwd; omitting both uses the host cwd.
|
|
76
|
-
*/
|
|
77
|
-
export async function createSession(
|
|
78
|
-
options: { workspaceId?: string; cwd?: string; agentPreset?: string } = {},
|
|
79
|
-
): Promise<CreatedSession> {
|
|
80
|
-
return await callUnary<CreatedSession>('session.create', options)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** One history window; omit beforeSeq for the tail page, pass a signal to abort. */
|
|
84
|
-
export async function history(
|
|
85
|
-
sessionId: string,
|
|
86
|
-
beforeSeq?: number,
|
|
87
|
-
maxMessages = 30,
|
|
88
|
-
signal?: AbortSignal,
|
|
89
|
-
): Promise<HistoryPage> {
|
|
90
|
-
return await callUnary<HistoryPage>('session.history', {
|
|
91
|
-
sessionId,
|
|
92
|
-
maxMessages,
|
|
93
|
-
...(beforeSeq !== undefined ? { beforeSeq } : {}),
|
|
94
|
-
}, signal)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Send one text prompt (queued: the agent picks it up in order). */
|
|
98
|
-
export async function prompt(sessionId: string, text: string): Promise<void> {
|
|
99
|
-
await callUnary<{ accepted: true }>('session.prompt', {
|
|
100
|
-
sessionId,
|
|
101
|
-
mode: 'queue',
|
|
102
|
-
content: [{ type: 'text', text }],
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Send one slash command line (e.g. `/permission workspace-write`). */
|
|
107
|
-
export async function sendCommand(sessionId: string, line: string): Promise<unknown> {
|
|
108
|
-
return await callUnary<unknown>('mobile.command', {
|
|
109
|
-
sessionId,
|
|
110
|
-
line,
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Stop the session's active turn (the mobile stop button). Pending queued
|
|
116
|
-
* work is preserved and resumes in FIFO order once cancellation settles.
|
|
117
|
-
*/
|
|
118
|
-
export async function cancelSession(sessionId: string): Promise<{ accepted: true }> {
|
|
119
|
-
return await callUnary<{ accepted: true }>('session.cancel', { sessionId })
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Fresh advisory model directory for one session (current + groups + failures). */
|
|
123
|
-
export async function models(sessionId: string): Promise<SessionModels> {
|
|
124
|
-
return await callUnary<SessionModels>('session.models', { sessionId })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Select the complete model selection (provider/model/reasoning effort) for a session. */
|
|
128
|
-
export async function selectModel(
|
|
129
|
-
sessionId: string,
|
|
130
|
-
selection: { provider: string; model: string; reasoningEffort?: string },
|
|
131
|
-
): Promise<{ selected: { provider: string; model: string; reasoningEffort?: string } }> {
|
|
132
|
-
return await callUnary<{ selected: { provider: string; model: string; reasoningEffort?: string } }>('session.selectModel', {
|
|
133
|
-
sessionId,
|
|
134
|
-
provider: selection.provider,
|
|
135
|
-
model: selection.model,
|
|
136
|
-
...(selection.reasoningEffort !== undefined ? { reasoningEffort: selection.reasoningEffort } : {}),
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* ── pending approvals / questions (#1025) ───────────────────────────── */
|
|
141
|
-
|
|
142
|
-
/** One pending tool approval awaiting the user's decision. */
|
|
143
|
-
export interface PendingApproval {
|
|
144
|
-
approvalId: string
|
|
145
|
-
toolName: string
|
|
146
|
-
callId?: string
|
|
147
|
-
reason?: string
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** One pending question group awaiting the user's answer. */
|
|
151
|
-
export interface PendingQuestionItem {
|
|
152
|
-
id: string
|
|
153
|
-
question: string
|
|
154
|
-
detail?: string
|
|
155
|
-
header?: string
|
|
156
|
-
options?: Array<{ label: string; description?: string }>
|
|
157
|
-
multiSelect?: boolean
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** The pending state for one session. */
|
|
161
|
-
export interface PendingState {
|
|
162
|
-
approvals: PendingApproval[]
|
|
163
|
-
questions: PendingQuestionItem[]
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Fetch pending approvals and questions for one session (polling fallback data source). */
|
|
167
|
-
export async function fetchPending(sessionId: string): Promise<PendingState> {
|
|
168
|
-
return await callUnary<PendingState>('mobile.pending', { sessionId })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Submit an approval decision (allowed-once or rejected). */
|
|
172
|
-
export async function respondApproval(
|
|
173
|
-
sessionId: string,
|
|
174
|
-
approvalId: string,
|
|
175
|
-
outcome: 'allowed-once' | 'rejected',
|
|
176
|
-
): Promise<void> {
|
|
177
|
-
await callUnary<unknown>('mobile.respond', {
|
|
178
|
-
sessionId,
|
|
179
|
-
type: 'approval',
|
|
180
|
-
approvalId,
|
|
181
|
-
outcome,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Submit answers to a question group. */
|
|
186
|
-
export async function respondQuestion(
|
|
187
|
-
sessionId: string,
|
|
188
|
-
answers: Array<{ id: string; selected: string[]; custom?: string }>,
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
await callUnary<unknown>('mobile.respond', {
|
|
191
|
-
sessionId,
|
|
192
|
-
type: 'question',
|
|
193
|
-
answers,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* ── directory browsing / workspace creation (#977) ──────────────────── */
|
|
198
|
-
|
|
199
|
-
/** One entry in a directory listing. */
|
|
200
|
-
export interface DirectoryEntry {
|
|
201
|
-
name: string
|
|
202
|
-
path: string
|
|
203
|
-
hidden: boolean
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** The host directory listing result (one level, with ancestor breadcrumbs). */
|
|
207
|
-
export interface DirectoryListing {
|
|
208
|
-
path: string
|
|
209
|
-
home: string
|
|
210
|
-
crumbs: DirectoryEntry[]
|
|
211
|
-
entries: DirectoryEntry[]
|
|
212
|
-
truncated: boolean
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** The result of creating a workspace from an existing directory. */
|
|
216
|
-
export interface WorkspaceCreateResult {
|
|
217
|
-
workspace: WorkspaceView
|
|
218
|
-
created: boolean
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Browse one directory level on the host (defaults to home directory). */
|
|
222
|
-
export async function listDirectory(path?: string): Promise<DirectoryListing> {
|
|
223
|
-
return await callUnary<DirectoryListing>('host.listDirectory', path === undefined ? {} : { path })
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Create a workspace from an existing host directory (does not mkdir). */
|
|
227
|
-
export async function createWorkspace(path: string): Promise<WorkspaceCreateResult> {
|
|
228
|
-
return await callUnary<WorkspaceCreateResult>('workspace.create', { path })
|
|
229
|
-
}
|
package/src/mobile/index.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile surface entry: the standalone phone UI served at /m. Boots its own
|
|
3
|
-
* React tree (no main-UI module loader), talks to the host over the shared
|
|
4
|
-
* /api transport with the paired-device cookie, and renders a deliberately
|
|
5
|
-
* thin three-level surface:
|
|
6
|
-
* workspaces (landing, no new-session homepage) → sessions (paged) →
|
|
7
|
-
* chat (history paged on demand + live mux stream + prompt input).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createRoot } from 'react-dom/client'
|
|
11
|
-
import { App } from './views/App.tsx'
|
|
12
|
-
import { mobileCss } from './mobile-styles.ts'
|
|
13
|
-
import { initMobileTheme } from './mobile-theme.ts'
|
|
14
|
-
import { registerMobilePwa } from '../mobile-pwa.ts'
|
|
15
|
-
import { consumeMobilePairUrl } from './pairing.ts'
|
|
16
|
-
|
|
17
|
-
// Apply the persisted (or default light) theme before first paint, so the
|
|
18
|
-
// page never flashes the wrong palette.
|
|
19
|
-
initMobileTheme()
|
|
20
|
-
|
|
21
|
-
// Inject the standalone stylesheet (the page has no shell to load it for us).
|
|
22
|
-
const style = document.createElement('style')
|
|
23
|
-
style.dataset.plugin = 'remote-web-ui/mobile'
|
|
24
|
-
style.textContent = mobileCss
|
|
25
|
-
document.head.appendChild(style)
|
|
26
|
-
|
|
27
|
-
const root = document.getElementById('root')
|
|
28
|
-
if (root === null) throw new Error('mobile: #root missing')
|
|
29
|
-
void bootMobile(root)
|
|
30
|
-
|
|
31
|
-
async function bootMobile(mount: HTMLElement): Promise<void> {
|
|
32
|
-
const pair = await consumeMobilePairUrl(window.location.href)
|
|
33
|
-
if (pair.kind === 'accepted') {
|
|
34
|
-
window.location.replace(pair.path)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
37
|
-
if (pair.kind === 'failed') window.history.replaceState(null, '', pair.path)
|
|
38
|
-
|
|
39
|
-
void registerMobilePwa()
|
|
40
|
-
createRoot(mount).render(<App initialPairError={pair.kind === 'failed' ? pair.message : undefined} />)
|
|
41
|
-
}
|