@olares/lares-mobile 0.15.6 → 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/package.json +2 -2
- package/src/App.vue +255 -144
- package/src/chat/ChatBar.vue +134 -33
- package/src/chat/Composer.vue +245 -213
- package/src/chat/Deliverables.vue +83 -11
- package/src/chat/HistoryPanel.vue +40 -1
- package/src/chat/QuestionCard.vue +3 -3
- package/src/chat/Transcript.vue +162 -4
- package/src/chat/format.js +11 -0
- package/src/chat/playback.js +41 -0
- package/src/chat/voice.js +169 -39
- package/src/desktop/DesktopComposer.vue +74 -3
- package/src/desktop/DesktopPreview.vue +10 -2
- package/src/desktop/DesktopShell.vue +31 -3
- package/src/desktop/DirectoryBrowser.vue +94 -33
- package/src/desktop/ui/PcDialog.vue +23 -8
- package/src/host.js +131 -21
- package/src/index.d.ts +5 -0
- package/src/mobile/MobileNewSession.vue +6 -117
- package/src/mobile/MobileShell.vue +57 -69
- package/src/preview/Preview.vue +30 -16
- package/src/preview/PreviewBody.vue +4 -1
- package/src/settings/AgentSettings.vue +16 -8
- package/src/settings/Sheet.vue +4 -0
|
@@ -1,18 +1,50 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<LaresPcDialog
|
|
3
3
|
:open="state.open"
|
|
4
|
-
:title="t('desktop.
|
|
4
|
+
:title="t('desktop.selectWorkspaceDirectory')"
|
|
5
5
|
:close-label="t('desktop.close')"
|
|
6
6
|
:busy="state.creating || state.creatingFolder"
|
|
7
7
|
:width="620"
|
|
8
|
+
variant="directory"
|
|
8
9
|
@close="$emit('close')"
|
|
9
10
|
>
|
|
11
|
+
<template #header="{ titleId }">
|
|
12
|
+
<div class="directory-browser__header">
|
|
13
|
+
<h2 :id="titleId">{{ t("desktop.selectWorkspaceDirectory") }}</h2>
|
|
14
|
+
<div class="directory-browser__location">
|
|
15
|
+
<input
|
|
16
|
+
v-if="editingPath"
|
|
17
|
+
ref="pathInput"
|
|
18
|
+
v-model="pathDraft"
|
|
19
|
+
:aria-label="t('desktop.folderPath')"
|
|
20
|
+
@keydown.enter.prevent="submitPath"
|
|
21
|
+
@keydown.esc.prevent="editingPath = false"
|
|
22
|
+
@blur="editingPath = false"
|
|
23
|
+
/>
|
|
24
|
+
<nav v-else-if="state.listing" :aria-label="t('desktop.folderPath')">
|
|
25
|
+
<template v-for="(crumb, index) in homeCrumbs" :key="crumb.path">
|
|
26
|
+
<span v-if="index" aria-hidden="true">/</span>
|
|
27
|
+
<button type="button" @click="$emit('browse', crumb.path)">
|
|
28
|
+
{{ crumb.path === state.listing.home ? t("desktop.home") : crumb.name }}
|
|
29
|
+
</button>
|
|
30
|
+
</template>
|
|
31
|
+
</nav>
|
|
32
|
+
<button
|
|
33
|
+
v-if="!editingPath"
|
|
34
|
+
type="button"
|
|
35
|
+
class="directory-browser__edit"
|
|
36
|
+
:aria-label="t('desktop.folderPath')"
|
|
37
|
+
@click="beginPathEdit"
|
|
38
|
+
>
|
|
39
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
40
|
+
<path d="m4 16-.75 4.25L7.5 19.5 18.4 8.6a2.1 2.1 0 0 0-3-3Z" />
|
|
41
|
+
<path d="m13.8 7.2 3 3M3 21h18" />
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
10
47
|
<div class="directory-browser">
|
|
11
|
-
<nav v-if="state.listing" class="directory-browser__crumbs" :aria-label="t('desktop.folderPath')">
|
|
12
|
-
<button v-for="crumb in state.listing.crumbs" :key="crumb.path" type="button" @click="$emit('browse', crumb.path)">
|
|
13
|
-
{{ crumb.path === state.listing.home ? t("desktop.home") : crumb.name }}
|
|
14
|
-
</button>
|
|
15
|
-
</nav>
|
|
16
48
|
<div class="directory-browser__body">
|
|
17
49
|
<p v-if="state.loading" class="directory-browser__status">{{ t("desktop.loadingFolders") }}</p>
|
|
18
50
|
<p v-else-if="state.error" class="directory-browser__error">{{ state.error }}</p>
|
|
@@ -40,9 +72,7 @@
|
|
|
40
72
|
</div>
|
|
41
73
|
<template #footer>
|
|
42
74
|
<div class="directory-browser__footer">
|
|
43
|
-
<
|
|
44
|
-
<div class="directory-browser__footer-row">
|
|
45
|
-
<div class="directory-browser__footer-left">
|
|
75
|
+
<div class="directory-browser__footer-left">
|
|
46
76
|
<button
|
|
47
77
|
type="button"
|
|
48
78
|
class="directory-browser__new-folder"
|
|
@@ -60,8 +90,8 @@
|
|
|
60
90
|
/>
|
|
61
91
|
<span>{{ t("desktop.showHiddenFiles") }}</span>
|
|
62
92
|
</label>
|
|
63
|
-
|
|
64
|
-
|
|
93
|
+
</div>
|
|
94
|
+
<div class="directory-browser__footer-right">
|
|
65
95
|
<button type="button" class="directory-browser__cancel" :disabled="state.creating" @click="$emit('close')">
|
|
66
96
|
{{ t("desktop.cancel") }}
|
|
67
97
|
</button>
|
|
@@ -71,9 +101,8 @@
|
|
|
71
101
|
:disabled="state.loading || state.creating || !(selected || state.listing?.path)"
|
|
72
102
|
@click="$emit('choose', selected || state.listing.path)"
|
|
73
103
|
>
|
|
74
|
-
{{ state.creating ? t("desktop.addingWorkspace") : t("desktop.
|
|
104
|
+
{{ state.creating ? t("desktop.addingWorkspace") : t("desktop.open") }}
|
|
75
105
|
</button>
|
|
76
|
-
</div>
|
|
77
106
|
</div>
|
|
78
107
|
</div>
|
|
79
108
|
</template>
|
|
@@ -100,17 +129,25 @@ export default {
|
|
|
100
129
|
},
|
|
101
130
|
emits: ["close", "browse", "choose", "create-folder", "toggle-hidden"],
|
|
102
131
|
data() {
|
|
103
|
-
return { selected: "", newFolderOpen: false };
|
|
132
|
+
return { selected: "", newFolderOpen: false, editingPath: false, pathDraft: "" };
|
|
104
133
|
},
|
|
105
134
|
computed: {
|
|
106
135
|
visibleEntries() {
|
|
107
136
|
const entries = this.state.listing?.entries ?? [];
|
|
108
137
|
return this.state.showHidden ? entries : entries.filter((entry) => !entry.hidden);
|
|
109
138
|
},
|
|
139
|
+
homeCrumbs() {
|
|
140
|
+
const crumbs = this.state.listing?.crumbs ?? [];
|
|
141
|
+
const home = this.state.listing?.home;
|
|
142
|
+
const homeIndex = crumbs.findIndex((crumb) => crumb.path === home);
|
|
143
|
+
return homeIndex >= 0 ? crumbs.slice(homeIndex) : crumbs;
|
|
144
|
+
},
|
|
110
145
|
},
|
|
111
146
|
watch: {
|
|
112
147
|
"state.listing.path"() {
|
|
113
148
|
this.selected = this.state.selectPath || "";
|
|
149
|
+
this.pathDraft = this.state.listing?.path || "";
|
|
150
|
+
this.editingPath = false;
|
|
114
151
|
},
|
|
115
152
|
"state.selectPath"(path) {
|
|
116
153
|
if (path) this.selected = path;
|
|
@@ -129,38 +166,62 @@ export default {
|
|
|
129
166
|
onCreateFolder(name) {
|
|
130
167
|
this.$emit("create-folder", name);
|
|
131
168
|
},
|
|
169
|
+
beginPathEdit() {
|
|
170
|
+
this.pathDraft = this.state.listing?.path || "";
|
|
171
|
+
this.editingPath = true;
|
|
172
|
+
this.$nextTick(() => {
|
|
173
|
+
this.$refs.pathInput?.focus();
|
|
174
|
+
this.$refs.pathInput?.select();
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
submitPath() {
|
|
178
|
+
const path = this.pathDraft.trim();
|
|
179
|
+
if (!path) return;
|
|
180
|
+
this.editingPath = false;
|
|
181
|
+
this.$emit("browse", path);
|
|
182
|
+
},
|
|
132
183
|
},
|
|
133
184
|
};
|
|
134
185
|
</script>
|
|
135
186
|
|
|
136
187
|
<style scoped>
|
|
137
|
-
.directory-
|
|
188
|
+
.directory-browser__header { height:76px; }
|
|
189
|
+
.directory-browser__header h2 { height:42px; margin:0; padding:19px 22px 0; font-size:16px; font-weight:500; line-height:22px; }
|
|
190
|
+
.directory-browser__location { display:flex; height:34px; align-items:center; gap:8px; padding:0 20px 7px 22px; }
|
|
191
|
+
.directory-browser__location nav { display:flex; min-width:0; flex:1; align-items:center; gap:7px; overflow:hidden; color:var(--q-ink-2); }
|
|
192
|
+
.directory-browser__location nav button { min-width:0; overflow:hidden; border:0; padding:0; background:transparent; color:inherit; font:inherit; font-size:14px; text-overflow:ellipsis; white-space:nowrap; cursor:pointer; }
|
|
193
|
+
.directory-browser__location nav button:hover { color:var(--q-ink-1); }
|
|
194
|
+
.directory-browser__location input { box-sizing:border-box; min-width:0; height:27px; flex:1; border:1px solid var(--q-input-stroke); border-radius:5px; padding:0 8px; outline:none; background:var(--q-background-2); color:var(--q-ink-1); font:inherit; font-size:13px; }
|
|
195
|
+
.directory-browser__edit { display:grid; width:30px; height:30px; flex-shrink:0; place-items:center; border:0; border-radius:6px; background:transparent; color:var(--q-ink-2); cursor:pointer; }
|
|
196
|
+
.directory-browser__edit:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
|
|
197
|
+
.directory-browser__edit svg { width:17px; height:17px; fill:none; stroke:currentColor; stroke-width:1.6; stroke-linecap:round; stroke-linejoin:round; }
|
|
198
|
+
.directory-browser { display:flex; height:min(316px, calc(100vh - 186px)); flex-direction:column; }
|
|
138
199
|
.directory-browser svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
|
|
139
|
-
.directory-
|
|
140
|
-
.directory-
|
|
141
|
-
.directory-
|
|
142
|
-
.directory-
|
|
143
|
-
.directory-browser__row { display:flex; width:100%; align-items:center; gap:9px; border:0; border-radius:7px; padding:8px 10px; background:transparent; color:var(--q-ink-1); text-align:left; }
|
|
144
|
-
.directory-browser__row:hover,
|
|
145
|
-
.directory-browser__row[data-selected="true"] { background:var(--q-background-hover); }
|
|
200
|
+
.directory-browser__body { flex:1; min-height:0; overflow:auto; padding:13px 20px; }
|
|
201
|
+
.directory-browser__row { display:flex; width:100%; height:28px; align-items:center; gap:7px; border:0; border-radius:6px; padding:0 6px; background:transparent; color:var(--q-ink-1); font-size:14px; text-align:left; }
|
|
202
|
+
.directory-browser__row:hover { background:var(--q-background-hover); }
|
|
203
|
+
.directory-browser__row[data-selected="true"] { background:var(--q-background-selected); }
|
|
146
204
|
.directory-browser__row > span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
147
205
|
.directory-browser__open { display:grid; width:26px; height:26px; place-items:center; border:0; background:transparent; color:var(--q-ink-3); }
|
|
148
206
|
.directory-browser__status,
|
|
149
|
-
.directory-browser__error { margin:20px; color:var(--q-ink-3); font-size:
|
|
207
|
+
.directory-browser__error { margin:20px; color:var(--q-ink-3); font-size:14px; text-align:center; }
|
|
150
208
|
.directory-browser__error { color:var(--q-orange-default); }
|
|
151
|
-
.directory-browser__footer { display:flex; width:100%;
|
|
152
|
-
.directory-browser__path { min-width:0; margin:0; overflow:hidden; color:var(--q-ink-3); font-size:13px; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
|
|
153
|
-
.directory-browser__footer-row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
209
|
+
.directory-browser__footer { display:flex; width:100%; align-items:center; justify-content:space-between; gap:12px; }
|
|
154
210
|
.directory-browser__footer-left { display:flex; min-width:0; flex:1; align-items:center; gap:12px; }
|
|
155
211
|
.directory-browser__footer-right { display:flex; flex-shrink:0; align-items:center; gap:8px; }
|
|
156
|
-
.directory-browser__new-folder { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--q-
|
|
157
|
-
|
|
158
|
-
.directory-
|
|
159
|
-
.directory-browser__hidden
|
|
212
|
+
.directory-browser__new-folder { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--q-btn-stroke); border-radius:999px; padding:0 14px; height:34px; background:transparent; color:var(--q-ink-1); font:inherit; font-size:14px; white-space:nowrap; }
|
|
213
|
+
/* The footer is a dialog slot, outside `.directory-browser`, so it misses the shared icon rule. */
|
|
214
|
+
.directory-browser__new-folder svg { width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; stroke-linejoin:round; }
|
|
215
|
+
.directory-browser__hidden { display:inline-flex; align-items:center; color:var(--q-ink-2); font-size:13px; white-space:nowrap; cursor:pointer; }
|
|
216
|
+
.directory-browser__hidden input { position:absolute; width:1px; height:1px; overflow:hidden; opacity:0; pointer-events:none; }
|
|
217
|
+
.directory-browser__hidden:has(input:checked) { color:var(--q-ink-1); }
|
|
160
218
|
.directory-browser__cancel,
|
|
161
|
-
.directory-browser__choose { min-width:
|
|
162
|
-
.directory-browser__cancel { border:1px solid var(--q-
|
|
163
|
-
.directory-
|
|
219
|
+
.directory-browser__choose { min-width:67px; height:34px; border-radius:999px; padding:0 14px; font-size:14px; }
|
|
220
|
+
.directory-browser__cancel { border:1px solid var(--q-btn-stroke); background:transparent; color:var(--q-ink-1); }
|
|
221
|
+
.directory-browser__new-folder:hover:not(:disabled),
|
|
222
|
+
.directory-browser__cancel:hover:not(:disabled) { background:var(--q-btn-bg-hover); border-color:var(--q-btn-stroke-hover); }
|
|
223
|
+
/* The confirm button is the inverse surface: white on dark, dark on light. */
|
|
224
|
+
.directory-browser__choose { border:0; background:var(--q-background-9); color:var(--q-background-1); }
|
|
164
225
|
.directory-browser button,
|
|
165
226
|
.directory-browser__cancel,
|
|
166
227
|
.directory-browser__choose { cursor:pointer; font:inherit; }
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Teleport to="body">
|
|
3
3
|
<Transition name="lares-dialog">
|
|
4
|
-
<div
|
|
4
|
+
<div
|
|
5
|
+
v-if="open"
|
|
6
|
+
class="lares-pc-dialog"
|
|
7
|
+
role="dialog"
|
|
8
|
+
aria-modal="true"
|
|
9
|
+
:aria-labelledby="titleId"
|
|
10
|
+
:data-variant="variant || undefined"
|
|
11
|
+
>
|
|
5
12
|
<button class="lares-pc-dialog__backdrop" type="button" :aria-label="closeLabel" @click="close" />
|
|
6
13
|
<section class="lares-pc-dialog__surface" :style="{ width: surfaceWidth }">
|
|
7
14
|
<header class="lares-pc-dialog__header">
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
<slot name="header" :title-id="titleId">
|
|
16
|
+
<div class="lares-pc-dialog__title">
|
|
17
|
+
<h2 :id="titleId">{{ title }}</h2>
|
|
18
|
+
<button type="button" class="lares-pc-dialog__close" :aria-label="closeLabel" @click="close">
|
|
19
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m7 7 10 10M17 7 7 17" /></svg>
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<p v-if="description">{{ description }}</p>
|
|
23
|
+
</slot>
|
|
15
24
|
</header>
|
|
16
25
|
<div class="lares-pc-dialog__body">
|
|
17
26
|
<slot />
|
|
@@ -37,6 +46,7 @@ export default {
|
|
|
37
46
|
closeLabel: { type: String, default: "Close" },
|
|
38
47
|
width: { type: [Number, String], default: 440 },
|
|
39
48
|
busy: { type: Boolean, default: false },
|
|
49
|
+
variant: { type: String, default: "" },
|
|
40
50
|
},
|
|
41
51
|
emits: ["close"],
|
|
42
52
|
data() {
|
|
@@ -82,4 +92,9 @@ export default {
|
|
|
82
92
|
.lares-pc-dialog__close svg { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:1.7; stroke-linecap:round; }
|
|
83
93
|
.lares-pc-dialog__body { min-height:0; overflow:auto; padding:16px; }
|
|
84
94
|
.lares-pc-dialog__footer { display:flex; min-height:58px; flex-shrink:0; align-items:center; justify-content:flex-end; gap:8px; padding:0 16px; border-top:1px solid var(--q-separator); }
|
|
95
|
+
/* The directory variant only changes geometry; colors stay on the shared tokens. */
|
|
96
|
+
.lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__surface { border-radius:20px; }
|
|
97
|
+
.lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__header { padding:0; }
|
|
98
|
+
.lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__body { padding:0; }
|
|
99
|
+
.lares-pc-dialog[data-variant="directory"] .lares-pc-dialog__footer { min-height:62px; padding:0 22px; }
|
|
85
100
|
</style>
|
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 {
|
|
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
|
|
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
|
|
122
|
-
|
|
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 (
|
|
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
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
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:
|
|
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:
|
|
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
|
}>;
|
|
@@ -6,35 +6,6 @@
|
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div class="mobile-new-session__prompt">
|
|
9
|
-
<div class="mobile-new-session__selectors">
|
|
10
|
-
<button
|
|
11
|
-
type="button"
|
|
12
|
-
class="mobile-new-session__chip mobile-new-session__chip--workspace"
|
|
13
|
-
:disabled="workspaceBusy"
|
|
14
|
-
:aria-label="t('desktop.workspaces')"
|
|
15
|
-
@click="$emit('workspace')"
|
|
16
|
-
>
|
|
17
|
-
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
18
|
-
<path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" />
|
|
19
|
-
</svg>
|
|
20
|
-
<span>{{ workspaceLabel }}</span>
|
|
21
|
-
<svg class="mobile-new-session__chevron" viewBox="0 0 24 24" aria-hidden="true">
|
|
22
|
-
<path d="M7 10l5 5 5-5" />
|
|
23
|
-
</svg>
|
|
24
|
-
</button>
|
|
25
|
-
<button
|
|
26
|
-
type="button"
|
|
27
|
-
class="mobile-new-session__chip mobile-new-session__chip--model"
|
|
28
|
-
:disabled="modelBusy"
|
|
29
|
-
:aria-label="modelAria"
|
|
30
|
-
@click="$emit('model')"
|
|
31
|
-
>
|
|
32
|
-
<span>{{ modelLabel || t("model.select") }}</span>
|
|
33
|
-
<svg class="mobile-new-session__chevron" viewBox="0 0 24 24" aria-hidden="true">
|
|
34
|
-
<path d="M7 10l5 5 5-5" />
|
|
35
|
-
</svg>
|
|
36
|
-
</button>
|
|
37
|
-
</div>
|
|
38
9
|
<div class="mobile-new-session__composer">
|
|
39
10
|
<slot />
|
|
40
11
|
</div>
|
|
@@ -49,21 +20,11 @@ export default {
|
|
|
49
20
|
name: "LaresMobileNewSession",
|
|
50
21
|
props: {
|
|
51
22
|
t: { type: Function, required: true },
|
|
52
|
-
workspaceLabel: { type: String, default: "" },
|
|
53
|
-
workspaceBusy: { type: Boolean, default: false },
|
|
54
|
-
modelLabel: { type: String, default: "" },
|
|
55
|
-
modelBusy: { type: Boolean, default: false },
|
|
56
23
|
},
|
|
57
|
-
emits: ["workspace", "model"],
|
|
58
24
|
computed: {
|
|
59
25
|
logoStyle() {
|
|
60
26
|
return { backgroundImage: MARK_DATA_URI };
|
|
61
27
|
},
|
|
62
|
-
modelAria() {
|
|
63
|
-
return this.modelLabel
|
|
64
|
-
? this.t("model.switchAria", { label: this.modelLabel })
|
|
65
|
-
: this.t("model.select");
|
|
66
|
-
},
|
|
67
28
|
},
|
|
68
29
|
};
|
|
69
30
|
</script>
|
|
@@ -75,17 +36,17 @@ export default {
|
|
|
75
36
|
min-height: 0;
|
|
76
37
|
flex: 1;
|
|
77
38
|
flex-direction: column;
|
|
78
|
-
align-items:
|
|
79
|
-
justify-content: center;
|
|
80
|
-
padding: 20px 20px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
39
|
+
align-items: stretch;
|
|
81
40
|
}
|
|
82
41
|
|
|
83
42
|
.mobile-new-session__brand {
|
|
84
43
|
display: flex;
|
|
44
|
+
min-height: 0;
|
|
45
|
+
flex: 1;
|
|
85
46
|
flex-direction: column;
|
|
86
47
|
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
87
49
|
gap: 8px;
|
|
88
|
-
margin-bottom: 32px;
|
|
89
50
|
}
|
|
90
51
|
|
|
91
52
|
.mobile-new-session__logo {
|
|
@@ -121,78 +82,10 @@ export default {
|
|
|
121
82
|
}
|
|
122
83
|
|
|
123
84
|
.mobile-new-session__prompt {
|
|
124
|
-
display: flex;
|
|
125
|
-
width: min(100%, 420px);
|
|
126
|
-
flex-direction: column;
|
|
127
|
-
gap: 6px;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.mobile-new-session__selectors {
|
|
131
85
|
display: flex;
|
|
132
86
|
width: 100%;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
gap: 10px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.mobile-new-session__chip {
|
|
139
|
-
display: inline-flex;
|
|
140
|
-
min-width: 0;
|
|
141
|
-
align-items: center;
|
|
142
|
-
gap: 6px;
|
|
143
|
-
border: 0;
|
|
144
|
-
border-radius: 999px;
|
|
145
|
-
padding: 6px 10px;
|
|
146
|
-
background: transparent;
|
|
147
|
-
color: var(--q-ink-2);
|
|
148
|
-
font-size: 14px;
|
|
149
|
-
font-weight: 500;
|
|
150
|
-
line-height: 18px;
|
|
151
|
-
transition: background var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease),
|
|
152
|
-
color var(--lares-duration-fast, 120ms) var(--lares-ease-out, ease);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.mobile-new-session__chip--workspace {
|
|
156
|
-
flex: 0 1 auto;
|
|
157
|
-
max-width: 46%;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.mobile-new-session__chip--model {
|
|
161
|
-
flex: 1 1 auto;
|
|
162
|
-
justify-content: flex-end;
|
|
163
|
-
max-width: 54%;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.mobile-new-session__chip span {
|
|
167
|
-
min-width: 0;
|
|
168
|
-
overflow: hidden;
|
|
169
|
-
text-overflow: ellipsis;
|
|
170
|
-
white-space: nowrap;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
.mobile-new-session__chip svg:not(.mobile-new-session__chevron) {
|
|
174
|
-
width: 18px;
|
|
175
|
-
height: 18px;
|
|
176
|
-
flex-shrink: 0;
|
|
177
|
-
fill: none;
|
|
178
|
-
stroke: currentColor;
|
|
179
|
-
stroke-width: 1.75;
|
|
180
|
-
stroke-linejoin: round;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.mobile-new-session__chevron {
|
|
184
|
-
width: 16px;
|
|
185
|
-
height: 16px;
|
|
186
|
-
flex-shrink: 0;
|
|
187
|
-
fill: none;
|
|
188
|
-
stroke: currentColor;
|
|
189
|
-
stroke-width: 1.75;
|
|
190
|
-
stroke-linecap: round;
|
|
191
|
-
stroke-linejoin: round;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
.mobile-new-session__chip:active:not(:disabled) {
|
|
195
|
-
background: var(--q-btn-bg-pressed);
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
padding: 8px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
196
89
|
}
|
|
197
90
|
|
|
198
91
|
.mobile-new-session__composer {
|
|
@@ -200,10 +93,6 @@ export default {
|
|
|
200
93
|
}
|
|
201
94
|
|
|
202
95
|
@media (max-height: 640px) {
|
|
203
|
-
.mobile-new-session__brand {
|
|
204
|
-
margin-bottom: 24px;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
96
|
.mobile-new-session__logo {
|
|
208
97
|
width: 48px;
|
|
209
98
|
height: 48px;
|