@olares/lares-mobile 0.15.7 → 0.15.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/src/chat/voice.js CHANGED
@@ -1,21 +1,57 @@
1
- import { TranscriptQueue } from "@olares/lares-core/voice/client";
2
1
  import {
3
2
  formatElapsed,
4
3
  isRecordingTooShort,
5
4
  pickRecordingMime,
6
- RECORDING_AUDIO,
7
5
  } from "@olares/lares-core/voice/recorder";
8
6
 
9
7
  export { formatElapsed };
10
8
 
11
- export function createVoiceCapture({ transcribe, getDraft, setDraft, onPhase }) {
12
- const queue = new TranscriptQueue();
9
+ /** Phone mic. Bare booleans are exact and iOS rejects the headset profile. */
10
+ const DEVICE_AUDIO = {
11
+ channelCount: { ideal: 1 },
12
+ echoCancellation: { ideal: true },
13
+ noiseSuppression: { ideal: true },
14
+ autoGainControl: { ideal: true },
15
+ };
16
+
17
+ const CHUNK_MS = 250;
18
+
19
+ function micFailure(err) {
20
+ const name = err?.name || "";
21
+ if (name === "NotAllowedError" || name === "PermissionDeniedError") return "voice_permission_denied";
22
+ if (name === "SecurityError") return "voice_unsupported";
23
+ return "voice_failed";
24
+ }
25
+
26
+ async function openDeviceMic(mediaDevices) {
27
+ try {
28
+ return await mediaDevices.getUserMedia({ audio: DEVICE_AUDIO });
29
+ } catch (err) {
30
+ if (micFailure(err) !== "voice_failed") throw err;
31
+ return mediaDevices.getUserMedia({ audio: true });
32
+ }
33
+ }
34
+
35
+ export function createVoiceCapture({
36
+ transcribe,
37
+ submit,
38
+ onPhase,
39
+ onTrace,
40
+ now = Date.now,
41
+ media,
42
+ }) {
43
+ const mediaDevices = media?.mediaDevices ?? globalThis.navigator?.mediaDevices;
44
+ const Recorder = media?.MediaRecorder ?? globalThis.MediaRecorder;
13
45
  let recorder = null;
14
46
  let stream = null;
15
47
  let chunks = [];
16
48
  let started = 0;
17
49
  let timer = 0;
50
+ let errorTimer = 0;
51
+ let finishTimer = 0;
18
52
  let cancel = false;
53
+ let released = false;
54
+ let finished = false;
19
55
  let dead = false;
20
56
  let abort = null;
21
57
  let phase = "idle";
@@ -25,6 +61,14 @@ export function createVoiceCapture({ transcribe, getDraft, setDraft, onPhase })
25
61
  onPhase?.(next, extra);
26
62
  };
27
63
 
64
+ const trace = (event, fields) => {
65
+ try {
66
+ onTrace?.(event, fields);
67
+ } catch {
68
+ /* a trace must not change the take */
69
+ }
70
+ };
71
+
28
72
  const stopTimer = () => {
29
73
  if (timer) {
30
74
  clearInterval(timer);
@@ -40,96 +84,182 @@ export function createVoiceCapture({ transcribe, getDraft, setDraft, onPhase })
40
84
 
41
85
  const fail = (code) => {
42
86
  if (dead) return;
87
+ if (errorTimer) clearTimeout(errorTimer);
43
88
  emit("error", { error: code, elapsed: 0 });
44
- setTimeout(() => {
89
+ errorTimer = setTimeout(() => {
90
+ errorTimer = 0;
45
91
  if (!dead && phase === "error") emit("idle", { error: "", elapsed: 0 });
46
92
  }, 3200);
47
93
  };
48
94
 
49
95
  const finish = async () => {
96
+ if (finished || dead) return;
97
+ finished = true;
50
98
  stopTimer();
51
- const took = Date.now() - started;
52
- const type = recorder?.mimeType || "audio/webm";
99
+ const took = now() - started;
100
+ const type = recorder?.mimeType || chunks[0]?.type || "audio/webm";
53
101
  const blobChunks = chunks;
54
102
  chunks = [];
55
103
  release();
56
- if (dead) return;
57
104
  if (cancel) {
58
- emit("idle", { elapsed: 0 });
105
+ emit("idle", { error: "", elapsed: 0 });
59
106
  return;
60
107
  }
61
108
  const blob = new Blob(blobChunks, { type });
109
+ trace("blob", { bytes: blob.size, type, ms: took, chunks: blobChunks.length });
62
110
  if (isRecordingTooShort(took, blob.size)) {
111
+ trace("too-short", { bytes: blob.size, ms: took });
63
112
  fail("voice_too_short");
64
113
  return;
65
114
  }
66
- emit("transcribing", { elapsed: took });
115
+ emit("transcribing", { elapsed: took, error: "" });
67
116
  abort = new AbortController();
68
117
  try {
69
- const text = await transcribe(blob, abort.signal);
70
- if (dead) return;
71
- emit("idle", { elapsed: 0 });
72
- queue.apply(text, true, getDraft(), setDraft);
118
+ const text = String(await transcribe(blob, abort.signal) ?? "").trim();
119
+ if (dead || cancel) return;
120
+ if (!text) {
121
+ trace("no-input", {});
122
+ fail("voice_no_input");
123
+ return;
124
+ }
125
+ trace("transcript", { chars: text.length, head: text.slice(0, 40) });
126
+ emit("idle", { error: "", elapsed: 0 });
127
+ submit?.(text);
73
128
  } catch (err) {
74
- if (dead || abort.signal.aborted) return;
75
- fail(err instanceof Error ? err.message : "voice_failed");
129
+ if (dead || abort.signal.aborted || cancel) return;
130
+ const code = err instanceof Error ? err.message : "voice_failed";
131
+ trace("transcribe-error", { code });
132
+ fail(code);
76
133
  } finally {
77
134
  abort = null;
78
135
  }
79
136
  };
80
137
 
138
+ const scheduleFinish = () => {
139
+ if (finishTimer) clearTimeout(finishTimer);
140
+ finishTimer = setTimeout(() => {
141
+ finishTimer = 0;
142
+ void finish();
143
+ }, 30);
144
+ };
145
+
146
+ const endRecorder = () => {
147
+ if (!recorder || recorder.state === "inactive") {
148
+ scheduleFinish();
149
+ return;
150
+ }
151
+ try {
152
+ recorder.requestData?.();
153
+ } catch {
154
+ /* the final dataavailable still arrives from stop() */
155
+ }
156
+ try {
157
+ recorder.stop();
158
+ } catch {
159
+ scheduleFinish();
160
+ }
161
+ };
162
+
81
163
  return {
82
164
  get phase() {
83
165
  return phase;
84
166
  },
85
167
  async start() {
86
- if (dead || phase === "recording" || phase === "transcribing") return;
87
- if (typeof MediaRecorder === "undefined" || !navigator.mediaDevices?.getUserMedia) {
168
+ if (dead || phase === "preparing" || phase === "recording" || phase === "transcribing") return;
169
+ if (typeof Recorder === "undefined" || !mediaDevices?.getUserMedia) {
88
170
  fail("voice_unsupported");
89
171
  return;
90
172
  }
91
173
  cancel = false;
174
+ released = false;
175
+ finished = false;
176
+ trace("open", {});
177
+ emit("preparing", { elapsed: 0, error: "" });
92
178
  let next;
93
179
  try {
94
- next = await navigator.mediaDevices.getUserMedia({ audio: RECORDING_AUDIO });
95
- } catch {
96
- fail("voice_permission_denied");
180
+ next = await openDeviceMic(mediaDevices);
181
+ } catch (err) {
182
+ const code = micFailure(err);
183
+ trace("mic-fail", { code, name: err?.name || "" });
184
+ if (!dead) fail(code);
97
185
  return;
98
186
  }
99
187
  if (dead || cancel) {
100
188
  next.getTracks().forEach((track) => track.stop());
189
+ if (!dead && phase === "preparing") emit("idle", { error: "", elapsed: 0 });
101
190
  return;
102
191
  }
103
192
  stream = next;
104
193
  chunks = [];
105
- const mime = pickRecordingMime();
106
- recorder = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
194
+ const mime = pickRecordingMime(Recorder);
195
+ try {
196
+ recorder = new Recorder(stream, mime ? { mimeType: mime } : undefined);
197
+ } catch {
198
+ release();
199
+ fail("voice_unsupported");
200
+ return;
201
+ }
107
202
  recorder.ondataavailable = (event) => {
108
203
  if (event.data?.size) chunks.push(event.data);
204
+ if (recorder?.state === "inactive") scheduleFinish();
109
205
  };
110
- recorder.onstop = () => {
111
- void finish();
112
- };
113
- started = Date.now();
206
+ recorder.onstop = () => scheduleFinish();
207
+ started = now();
208
+ try {
209
+ recorder.start(CHUNK_MS);
210
+ } catch {
211
+ trace("record-fail", { mime });
212
+ release();
213
+ fail("voice_unsupported");
214
+ return;
215
+ }
216
+ trace("recording", { mime: recorder.mimeType || mime || "" });
217
+ if (released || cancel) {
218
+ endRecorder();
219
+ return;
220
+ }
114
221
  emit("recording", { elapsed: 0, error: "" });
115
- recorder.start();
116
- timer = setInterval(() => emit("recording", { elapsed: Date.now() - started }), 200);
222
+ timer = setInterval(() => emit("recording", { elapsed: now() - started, error: "" }), 200);
117
223
  },
224
+ /**
225
+ * Finger-up finishes the take. The mic opens asynchronously, so the finger
226
+ * often lifts while still preparing — that is not a cancel.
227
+ */
118
228
  stop(aborting = false) {
119
- cancel = Boolean(aborting);
120
- if (recorder && recorder.state !== "inactive") recorder.stop();
121
- else {
122
- stopTimer();
123
- release();
124
- emit("idle", { elapsed: 0 });
229
+ if (phase === "transcribing") {
230
+ if (aborting) {
231
+ cancel = true;
232
+ abort?.abort();
233
+ }
234
+ return;
235
+ }
236
+ if (phase !== "preparing" && phase !== "recording") {
237
+ trace("stop-ignored", { aborting: Boolean(aborting), phase });
238
+ return;
239
+ }
240
+ trace("stop", {
241
+ aborting: Boolean(aborting),
242
+ phase,
243
+ recorder: recorder?.state || "none",
244
+ });
245
+ if (aborting) {
246
+ cancel = true;
247
+ released = false;
248
+ if (recorder && recorder.state !== "inactive") recorder.stop();
249
+ return;
250
+ }
251
+ released = true;
252
+ stopTimer();
253
+ if (recorder && recorder.state !== "inactive") {
254
+ emit("transcribing", { elapsed: now() - started, error: "" });
255
+ endRecorder();
125
256
  }
126
- },
127
- toggle() {
128
- if (phase === "recording") this.stop(false);
129
- else if (phase === "idle" || phase === "error") void this.start();
130
257
  },
131
258
  dispose() {
132
259
  dead = true;
260
+ cancel = true;
261
+ if (errorTimer) clearTimeout(errorTimer);
262
+ if (finishTimer) clearTimeout(finishTimer);
133
263
  stopTimer();
134
264
  abort?.abort();
135
265
  if (recorder && recorder.state !== "inactive") recorder.stop();
@@ -2,9 +2,16 @@
2
2
  <section class="desktop-preview" :aria-label="t('preview')">
3
3
  <header class="desktop-preview__bar">
4
4
  <span class="desktop-preview__path" :title="path">{{ path }}</span>
5
+ <a
6
+ v-if="filesHref"
7
+ class="desktop-preview__action"
8
+ :href="filesHref"
9
+ target="_blank"
10
+ rel="noopener noreferrer"
11
+ >{{ t("openInFiles") }}</a>
5
12
  <a
6
13
  v-if="downloadHref"
7
- class="desktop-preview__download"
14
+ class="desktop-preview__action"
8
15
  :href="downloadHref"
9
16
  download
10
17
  >{{ t("download") }}</a>
@@ -40,6 +47,7 @@ export default {
40
47
  error: { type: String, default: "" },
41
48
  mediaSrc: { type: String, default: "" },
42
49
  downloadHref: { type: String, default: "" },
50
+ filesHref: { type: String, default: "" },
43
51
  hrefFor: { type: Function, default: null },
44
52
  t: { type: Function, required: true },
45
53
  },
@@ -53,6 +61,6 @@ export default {
53
61
  .desktop-preview { position:absolute; z-index:8; display:flex; min-width:0; min-height:0; flex-direction:column; inset:0; padding:12px 20px 20px; background:var(--q-background-1); }
54
62
  .desktop-preview__bar { display:flex; min-width:0; flex:none; align-items:center; gap:12px; padding-bottom:10px; }
55
63
  .desktop-preview__path { min-width:0; flex:1; overflow:hidden; padding:0 4px; color:var(--q-ink-3); font-size:12px; line-height:18px; text-overflow:ellipsis; white-space:nowrap; }
56
- .desktop-preview__download { flex:none; color:var(--q-blue-default); font-size:12px; line-height:18px; text-decoration:none; }
64
+ .desktop-preview__action { flex:none; color:var(--q-blue-default); font-size:12px; line-height:18px; text-decoration:none; }
57
65
  .desktop-preview__content { display:flex; min-height:0; flex:1; flex-direction:column; overflow:hidden; border:1px solid var(--q-separator); border-radius:14px; background:var(--q-background-2); }
58
66
  </style>
@@ -44,6 +44,7 @@
44
44
  ref="transcript"
45
45
  :items="state.viewItems"
46
46
  :running="state.running"
47
+ :running-since="state.runningSince"
47
48
  :loading="state.historyLoading || state.starting"
48
49
  :previews="state.previews"
49
50
  :media-url="mediaUrl"
@@ -95,6 +96,7 @@
95
96
  :error="state.preview.error"
96
97
  :media-src="state.previewMediaSrc"
97
98
  :download-href="state.previewDownloadHref"
99
+ :files-href="state.previewFilesHref"
98
100
  :href-for="previewHref"
99
101
  :t="t"
100
102
  @retry="$emit('open-file', state.preview.path)"
package/src/host.js CHANGED
@@ -2,12 +2,18 @@ import { hostTarget, hostUrl, MODELS_PATH, probeHost } from "@olares/lares-core/
2
2
  import { callRpc, consumeMux, ensureSession, loadTranscript, MUX_PATH, sendPrompt } from "@olares/lares-core/larepass/chat";
3
3
  import { commandsUrl, normalizeCommands } from "@olares/lares-core/larepass/commands";
4
4
  import { normalizeReferenceCandidates, referencesUrl } from "@olares/lares-core/larepass/references";
5
- import { muxWsUrl } from "@olares/lares-core/larepass/mux";
5
+ import { muxWsUrl, remoteStreamOpen, workspaceBaseline } from "@olares/lares-core/larepass/mux";
6
6
  import { RESPOND_PATH } from "@olares/lares-core/larepass/rpc";
7
7
  import { createHostSettings } from "@olares/lares-core/larepass/settings";
8
- import { downloadFileUrl, previewMetaUrl, rawFileUrl } from "@olares/lares-core/files/preview-workspace";
8
+ import {
9
+ downloadFileUrl,
10
+ isPreviewPayload,
11
+ previewMetaUrl,
12
+ rawFileUrl,
13
+ } from "@olares/lares-core/files/preview-workspace";
9
14
  import { FILES_UPLOAD_PATH, uploadFile } from "@olares/lares-core/files/upload-client";
10
15
  import { API as VOICE_API, postTranscribe } from "@olares/lares-core/voice/client";
16
+ import { postReport, REPORT_PATH } from "@olares/lares-core/log/report";
11
17
 
12
18
  function pageOrigin() {
13
19
  try {
@@ -35,6 +41,72 @@ function socketProtocols(ports) {
35
41
  return value ? String(value) : undefined;
36
42
  }
37
43
 
44
+ function listWorkspaces(ports) {
45
+ const Socket = globalThis.WebSocket;
46
+ if (typeof Socket !== "function") {
47
+ return Promise.resolve({
48
+ ok: false,
49
+ status: "error",
50
+ error: { code: "no-websocket", message: "WebSocket is not available" },
51
+ });
52
+ }
53
+ const streamId = typeof globalThis.crypto?.randomUUID === "function"
54
+ ? globalThis.crypto.randomUUID()
55
+ : `ws-${Date.now()}`;
56
+ return openMuxSocket(hostUrl({
57
+ baseUrl: hostTarget(ports).baseUrl,
58
+ proxyPrefix: hostTarget(ports).proxyPrefix,
59
+ path: MUX_PATH,
60
+ }), undefined, socketProtocols(ports)).then((opened) => {
61
+ if (!opened.ok || !opened.body) {
62
+ return {
63
+ ok: false,
64
+ status: opened.status || "error",
65
+ error: opened.error || { code: "workspace", message: "workspace follow failed" },
66
+ };
67
+ }
68
+ const socket = opened.body;
69
+ return new Promise((resolve) => {
70
+ let settled = false;
71
+ const finish = (result) => {
72
+ if (settled) return;
73
+ settled = true;
74
+ clearTimeout(timer);
75
+ try {
76
+ socket.send(JSON.stringify({ type: "cancel", streamId }));
77
+ } catch {
78
+ // already closing
79
+ }
80
+ try {
81
+ socket.close();
82
+ } catch {
83
+ // already closing
84
+ }
85
+ resolve(result);
86
+ };
87
+ const timer = setTimeout(() => {
88
+ finish({
89
+ ok: false,
90
+ status: "error",
91
+ error: { code: "workspace", message: "workspace follow timed out" },
92
+ });
93
+ }, 8000);
94
+ socket.addEventListener("message", (event) => {
95
+ let message;
96
+ try {
97
+ message = JSON.parse(String(event.data ?? ""));
98
+ } catch {
99
+ return;
100
+ }
101
+ const baseline = workspaceBaseline(message);
102
+ if (!baseline) return;
103
+ finish({ ok: true, status: "ok", value: baseline });
104
+ });
105
+ socket.send(JSON.stringify(remoteStreamOpen(streamId, "workspace/follow")));
106
+ });
107
+ });
108
+ }
109
+
38
110
  function openMuxSocket(httpUrl, signal, protocols) {
39
111
  const Socket = globalThis.WebSocket;
40
112
  if (typeof Socket !== "function") {
@@ -82,19 +154,24 @@ function openMuxSocket(httpUrl, signal, protocols) {
82
154
  });
83
155
  }
84
156
 
157
+ /** Audio and file bodies go out as bytes; only plain objects are JSON. */
158
+ function isBinaryBody(body) {
159
+ return (typeof Blob !== "undefined" && body instanceof Blob)
160
+ || body instanceof ArrayBuffer
161
+ || ArrayBuffer.isView(body)
162
+ || (typeof FormData !== "undefined" && body instanceof FormData);
163
+ }
164
+
85
165
  export async function defaultRequest(url, init = {}) {
166
+ const raw = init.body === undefined || typeof init.body === "string" || isBinaryBody(init.body);
86
167
  const res = await fetch(url, {
87
168
  method: init.method ?? "GET",
88
- body: init.body === undefined
89
- ? undefined
90
- : typeof init.body === "string"
91
- ? init.body
92
- : JSON.stringify(init.body),
169
+ body: raw ? init.body : JSON.stringify(init.body),
93
170
  credentials: "include",
94
171
  redirect: "manual",
95
172
  signal: init.signal,
96
173
  headers: {
97
- ...(init.body === undefined ? {} : { "content-type": "application/json" }),
174
+ ...(init.body === undefined || isBinaryBody(init.body) ? {} : { "content-type": "application/json" }),
98
175
  ...init.headers,
99
176
  },
100
177
  });
@@ -118,14 +195,20 @@ export function createHostClient(ports = {}) {
118
195
  const { baseUrl, proxyPrefix } = hostTarget(ports);
119
196
  const send = ports.request ?? defaultRequest;
120
197
  const urlFor = (path) => hostUrl({ baseUrl, proxyPrefix, path });
121
- const request = (path, init) => send(urlFor(path), init);
122
- const rpc = (method, payload) => callRpc(request, method, payload);
198
+ const direct = (url, init) => {
199
+ if (ports.missing) {
200
+ return Promise.resolve({ ok: false, status: 0, body: { error: { code: "missing" } } });
201
+ }
202
+ return send(url, init);
203
+ };
204
+ const request = (path, init) => direct(urlFor(path), init);
205
+ const rpc = (method, payload) => (
206
+ method === "workspace.list" ? listWorkspaces(ports) : callRpc(request, method, payload)
207
+ );
123
208
  return {
124
209
  urlFor,
125
210
  probe() {
126
- if (!proxyPrefix && !baseUrl) {
127
- return Promise.resolve({ status: "missing" });
128
- }
211
+ if (ports.missing) return Promise.resolve({ status: "missing" });
129
212
  return probeHost((path) => request(path), MODELS_PATH);
130
213
  },
131
214
  rpc,
@@ -168,32 +251,59 @@ export function createHostClient(ports = {}) {
168
251
  const code = body?.error?.code || "file_preview_failed";
169
252
  throw new Error(code);
170
253
  }
254
+ if (!isPreviewPayload(body)) throw new Error("file_preview_failed");
171
255
  return body;
172
256
  },
173
- mediaUrl(sessionId, path, modifiedAt) {
174
- return urlFor(rawFileUrl(sessionId, path, modifiedAt));
257
+ mediaUrl(sessionId, path, modifiedAt, size) {
258
+ return urlFor(rawFileUrl(sessionId, path, modifiedAt, size));
175
259
  },
176
260
  downloadUrl(sessionId, path) {
177
261
  return urlFor(downloadFileUrl(sessionId, path));
178
262
  },
179
- // Both carry a binary body, so neither can go through `request` as an RPC —
180
- // but both must still ride the host's auth. `ports.request` is passed
181
- // through rather than called here so a host that supplies none keeps the
182
- // cookie `fetch` these two used before.
263
+ // Binary bodies stay off the JSON path. `direct` still uses the host's
264
+ // auth port, or cookie fetch when none was given, and does not call out
265
+ // when LarePass has no entrance.
183
266
  upload(sessionId, file, options) {
184
267
  return uploadFile(file, sessionId, {
185
268
  ...options,
186
269
  url: urlFor(FILES_UPLOAD_PATH),
187
- request: ports.request,
270
+ request: direct,
188
271
  });
189
272
  },
190
273
  transcribe(blob, language, signal) {
191
274
  return postTranscribe(blob, language, signal, {
192
275
  baseUrl: urlFor(VOICE_API),
193
- request: ports.request,
276
+ request: direct,
277
+ });
278
+ },
279
+ reportLog(scope, event, fields) {
280
+ return postReport({ scope, event, ...(fields ?? {}) }, {
281
+ request,
282
+ url: urlFor(REPORT_PATH),
194
283
  });
195
284
  },
285
+ async speak(text) {
286
+ const res = await request(`${VOICE_API}/speak`, { method: "POST", body: { text } });
287
+ const body = res?.body;
288
+ if (res?.ok && body && typeof body.audio === "string") {
289
+ return {
290
+ bluetooth: body.bluetooth === "played" ? "played" : "skipped",
291
+ audio: body.audio,
292
+ };
293
+ }
294
+ const error = body && typeof body === "object" ? body.error : null;
295
+ const code = typeof error?.code === "string" && error.code ? error.code : "voice_speak_failed";
296
+ const message = typeof error?.message === "string" && error.message ? error.message : code;
297
+ throw Object.assign(new Error(message), { code });
298
+ },
299
+ async interruptSpeech() {
300
+ const res = await request(`${VOICE_API}/playback/stop`, { method: "POST" });
301
+ return Boolean(res?.ok);
302
+ },
196
303
  async openMux(signal) {
304
+ if (ports.missing) {
305
+ return { ok: false, status: "missing", error: { code: "missing", message: "no entrance" } };
306
+ }
197
307
  try {
198
308
  return await openMuxSocket(urlFor(MUX_PATH), signal, socketProtocols(ports));
199
309
  } catch (err) {
package/src/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export type HostPorts = {
30
30
  * carry `X-Authorization`. Pass a function to have it read per connect.
31
31
  */
32
32
  socketProtocol?: string | string[] | (() => string | string[] | undefined);
33
+ /** LarePass sets this when myApps has no Lares app. The entrance page leaves it unset. */
34
+ missing?: boolean;
33
35
  };
34
36
 
35
37
  export const PC_TEST_PROXY: string;
@@ -50,6 +52,7 @@ export function laresPortsFromAccount(input?: {
50
52
  baseUrl?: string;
51
53
  env?: HostEnv;
52
54
  proxyPrefix?: string;
55
+ missing?: boolean;
53
56
  };
54
57
 
55
58
  export function defaultRequest(url: string, init?: HostRequestInit): Promise<HostResponse>;
@@ -72,6 +75,7 @@ export const LaresApp: DefineComponent<{
72
75
  env?: HostEnv;
73
76
  request?: (url: string, init?: HostRequestInit) => Promise<HostResponse>;
74
77
  socketProtocol?: HostPorts["socketProtocol"];
78
+ missing?: boolean;
75
79
  }>;
76
80
 
77
81
  export const LaresAgentSettings: DefineComponent<{
@@ -81,4 +85,5 @@ export const LaresAgentSettings: DefineComponent<{
81
85
  proxyPrefix?: string;
82
86
  env?: HostEnv;
83
87
  request?: (url: string, init?: HostRequestInit) => Promise<HostResponse>;
88
+ missing?: boolean;
84
89
  }>;