@olares/lares-mobile 0.15.5 → 0.15.7
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.md +6 -2
- package/package.json +2 -2
- package/src/App.vue +918 -178
- package/src/chat/ChatBar.vue +2 -2
- package/src/chat/Composer.vue +467 -16
- package/src/chat/Deliverables.vue +22 -10
- package/src/chat/HistoryPanel.vue +747 -55
- package/src/chat/MessageActions.vue +88 -34
- package/src/chat/QuestionCard.vue +5 -5
- package/src/chat/StageRow.vue +8 -5
- package/src/chat/Transcript.vue +20 -6
- package/src/chat/composer-tokens.js +32 -0
- package/src/chat/format.js +37 -0
- package/src/desktop/DesktopComposer.vue +942 -0
- package/src/desktop/DesktopContextMeter.vue +81 -0
- package/src/desktop/DesktopGoal.vue +88 -0
- package/src/desktop/DesktopPreview.vue +58 -0
- package/src/desktop/DesktopPreviewTabs.vue +82 -0
- package/src/desktop/DesktopQuestions.vue +118 -0
- package/src/desktop/DesktopQueue.vue +203 -0
- package/src/desktop/DesktopShell.vue +229 -0
- package/src/desktop/DesktopSidebar.vue +645 -0
- package/src/desktop/DesktopTodos.vue +52 -0
- package/src/desktop/DirectoryBrowser.vue +231 -0
- package/src/desktop/DirectoryNewFolder.vue +124 -0
- package/src/desktop/ui/PcDialog.vue +100 -0
- package/src/desktop/ui/PcPopover.vue +110 -0
- package/src/desktop/ui/PcTooltip.vue +95 -0
- package/src/desktop/ui/place.js +24 -0
- package/src/desktop/workspace-view.js +121 -0
- package/src/host.js +20 -2
- package/src/index.d.ts +2 -0
- package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
- package/src/mobile/MobileDirectorySheet.vue +162 -0
- package/src/mobile/MobileNewSession.vue +217 -0
- package/src/mobile/MobileShell.vue +367 -0
- package/src/motion.css +166 -0
- package/src/preview/Preview.vue +91 -150
- package/src/preview/PreviewBody.vue +185 -0
- package/src/settings/AgentSettings.vue +126 -36
- package/src/settings/Select.vue +183 -0
- package/src/settings/SettingRow.vue +5 -4
- package/src/settings/Sheet.vue +4 -4
- package/src/styles.css +9 -0
package/src/App.vue
CHANGED
|
@@ -1,110 +1,104 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="lares-shell" :data-device="chatDevice">
|
|
3
|
-
<
|
|
3
|
+
<LaresDesktopShell
|
|
4
|
+
v-if="chatDevice === 'desktop'"
|
|
5
|
+
ref="shell"
|
|
6
|
+
:state="shellState"
|
|
4
7
|
:t="t"
|
|
5
|
-
:
|
|
6
|
-
:
|
|
7
|
-
|
|
8
|
+
:media-url="mediaUrl"
|
|
9
|
+
:scroll-top="scrollTop"
|
|
10
|
+
:remember-scroll="rememberScroll"
|
|
11
|
+
:sticking="sticking"
|
|
12
|
+
:preview-href="previewHref"
|
|
13
|
+
:model-key="modelKey"
|
|
14
|
+
:effort-name="effortName"
|
|
8
15
|
@create="newChat"
|
|
16
|
+
@pick-session="pickSession"
|
|
17
|
+
@pick-workspace="pickWorkspace"
|
|
18
|
+
@retry="retry"
|
|
19
|
+
@open-file="openFile"
|
|
20
|
+
@answer="answerQuestion"
|
|
21
|
+
@update-draft="updateDraft"
|
|
22
|
+
@send="send"
|
|
23
|
+
@stop="stop"
|
|
24
|
+
@steer-queue="steerQueue"
|
|
25
|
+
@choose-model="chooseModel"
|
|
26
|
+
@choose-effort="chooseEffort"
|
|
27
|
+
@choose-permission="choosePermission"
|
|
28
|
+
@disable-plan="disablePlan"
|
|
29
|
+
@goal-action="goalAction"
|
|
30
|
+
@answer-approval="answerApproval"
|
|
31
|
+
@answer-questions="answerQuestions"
|
|
32
|
+
@open-commands="loadCommands"
|
|
33
|
+
@open-references="loadReferences"
|
|
34
|
+
@pick-command="pickCommand"
|
|
35
|
+
@queue-action="updateQueue"
|
|
36
|
+
@files="attachFiles"
|
|
37
|
+
@remove-image="removeDraftImage"
|
|
38
|
+
@show-chat="showChat"
|
|
39
|
+
@close-preview-tab="closePreviewTab"
|
|
40
|
+
@refresh-workspaces="refreshWorkspaces"
|
|
41
|
+
@browse-workspaces="openWorkspaceBrowser"
|
|
42
|
+
@browse-directory="browseDirectory"
|
|
43
|
+
@close-directory="closeWorkspaceBrowser"
|
|
44
|
+
@choose-directory="chooseWorkspaceDirectory"
|
|
45
|
+
@create-folder="createDirectoryFolder"
|
|
46
|
+
@toggle-hidden="toggleDirectoryHidden"
|
|
47
|
+
@rename-workspace="renameWorkspace"
|
|
48
|
+
@delete-workspace="deleteWorkspace"
|
|
49
|
+
@move-workspace="moveWorkspace"
|
|
50
|
+
@move-session="moveSession"
|
|
51
|
+
@rename-session="renameSession"
|
|
52
|
+
@fork-session="forkSession"
|
|
53
|
+
@archive-session="archiveSession"
|
|
9
54
|
/>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
:
|
|
14
|
-
:ready="sessionsReady"
|
|
15
|
-
:t="t"
|
|
16
|
-
@close="panel = ''"
|
|
17
|
-
@pick="pickSession"
|
|
18
|
-
/>
|
|
19
|
-
<div class="lares-shell__conversation">
|
|
20
|
-
<p v-if="failed" class="lares-shell__status">{{ failText }}</p>
|
|
21
|
-
<button v-if="failed" type="button" class="lares-shell__retry" :disabled="starting" @click="retry">
|
|
22
|
-
{{ t("probe.retry") }}
|
|
23
|
-
</button>
|
|
24
|
-
<LaresTranscript
|
|
25
|
-
ref="transcript"
|
|
26
|
-
:items="viewItems"
|
|
27
|
-
:running="running"
|
|
28
|
-
:loading="historyLoading || starting"
|
|
29
|
-
:previews="previews"
|
|
30
|
-
:media-url="mediaUrl"
|
|
31
|
-
:scroll-top="scrollTop"
|
|
32
|
-
:remember-scroll="rememberScroll"
|
|
33
|
-
:sticking="sticking"
|
|
34
|
-
:session-id="sessionId"
|
|
35
|
-
:question="question"
|
|
36
|
-
:question-busy="questionBusy"
|
|
37
|
-
:t="t"
|
|
38
|
-
@open="openFile"
|
|
39
|
-
@media="pinLog"
|
|
40
|
-
@answer="answerQuestion"
|
|
41
|
-
/>
|
|
42
|
-
<LaresComposer
|
|
43
|
-
:draft="draft"
|
|
44
|
-
:sending="sending"
|
|
45
|
-
:can-send="canSend"
|
|
46
|
-
:model-label="modelLabel"
|
|
47
|
-
:model-busy="modelBusy"
|
|
48
|
-
:effort-label="effortLabel"
|
|
49
|
-
:effort-disabled="effortDisabled"
|
|
50
|
-
:attach-pending="upload.pending > 0"
|
|
51
|
-
:attach-disabled="!sessionId || historyLoading || starting"
|
|
52
|
-
:voice-phase="voicePhase"
|
|
53
|
-
:voice-elapsed="voiceElapsed"
|
|
54
|
-
:voice-error="voiceErrorText"
|
|
55
|
-
:failures="upload.failures"
|
|
56
|
-
:t="t"
|
|
57
|
-
@update:draft="draft = $event"
|
|
58
|
-
@send="send"
|
|
59
|
-
@model="openModelSheet"
|
|
60
|
-
@effort="openEffortSheet"
|
|
61
|
-
@files="attachFiles"
|
|
62
|
-
@voice="toggleVoice"
|
|
63
|
-
@hold-start="onHoldStart"
|
|
64
|
-
@hold-end="onHoldEnd"
|
|
65
|
-
/>
|
|
66
|
-
</div>
|
|
67
|
-
<LaresSheet :open="modelSheet" :title="t('model.menuAria')" @close="modelSheet = false">
|
|
68
|
-
<p v-if="modelError" class="lares-shell__status">{{ modelError }}</p>
|
|
69
|
-
<p v-else-if="!models" class="lares-shell__hint">{{ t("agent.loading") }}</p>
|
|
70
|
-
<p v-else-if="!modelGroups.length" class="lares-shell__hint">{{ t("model.empty") }}</p>
|
|
71
|
-
<template v-for="section in modelGroups" :key="section.provider">
|
|
72
|
-
<p class="lares-shell__heading">{{ section.provider }}</p>
|
|
73
|
-
<LaresSettingRow
|
|
74
|
-
v-for="item in section.models"
|
|
75
|
-
:key="modelKey(item)"
|
|
76
|
-
:label="item.name"
|
|
77
|
-
:checked="modelKey(item) === modelCurrent"
|
|
78
|
-
:disabled="modelBusy"
|
|
79
|
-
@click="chooseModel(item)"
|
|
80
|
-
/>
|
|
81
|
-
</template>
|
|
82
|
-
</LaresSheet>
|
|
83
|
-
<LaresSheet :open="effortSheet" :title="t('reasoning.title')" @close="effortSheet = false">
|
|
84
|
-
<p v-if="!effortRows.length" class="lares-shell__hint">{{ t("reasoning.default") }}</p>
|
|
85
|
-
<LaresSettingRow
|
|
86
|
-
v-for="row in effortRows"
|
|
87
|
-
:key="row.key"
|
|
88
|
-
:label="effortName(row)"
|
|
89
|
-
:checked="row.id === effortId"
|
|
90
|
-
:disabled="modelBusy"
|
|
91
|
-
@click="chooseEffort(row.id)"
|
|
92
|
-
/>
|
|
93
|
-
</LaresSheet>
|
|
94
|
-
<LaresPreview
|
|
95
|
-
v-if="preview.path"
|
|
96
|
-
:path="preview.path"
|
|
97
|
-
:session-id="sessionId"
|
|
98
|
-
:status="preview.status"
|
|
99
|
-
:data="preview.data"
|
|
100
|
-
:error="preview.error"
|
|
101
|
-
:media-src="previewMediaSrc"
|
|
102
|
-
:download-href="previewDownloadHref"
|
|
103
|
-
:href-for="previewHref"
|
|
55
|
+
<LaresMobileShell
|
|
56
|
+
v-else
|
|
57
|
+
ref="shell"
|
|
58
|
+
:state="shellState"
|
|
104
59
|
:t="t"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
60
|
+
:media-url="mediaUrl"
|
|
61
|
+
:scroll-top="scrollTop"
|
|
62
|
+
:remember-scroll="rememberScroll"
|
|
63
|
+
:sticking="sticking"
|
|
64
|
+
:preview-href="previewHref"
|
|
65
|
+
:model-key="modelKey"
|
|
66
|
+
:effort-name="effortName"
|
|
67
|
+
@history="toggleHistory"
|
|
68
|
+
@create="newChat"
|
|
69
|
+
@close-history="panel = ''"
|
|
70
|
+
@pick-session="pickSession"
|
|
71
|
+
@refresh-workspaces="refreshWorkspaces"
|
|
72
|
+
@browse-workspaces="openWorkspaceBrowser"
|
|
73
|
+
@browse-directory="browseDirectory"
|
|
74
|
+
@close-directory="closeWorkspaceBrowser"
|
|
75
|
+
@choose-directory="chooseWorkspaceDirectory"
|
|
76
|
+
@create-folder="createDirectoryFolder"
|
|
77
|
+
@toggle-hidden="toggleDirectoryHidden"
|
|
78
|
+
@move-session="moveSession"
|
|
79
|
+
@rename-session="renameSession"
|
|
80
|
+
@archive-session="archiveSession"
|
|
81
|
+
@pick-workspace="pickWorkspace"
|
|
82
|
+
@retry="retry"
|
|
83
|
+
@open-file="openFile"
|
|
84
|
+
@answer="answerQuestion"
|
|
85
|
+
@update-draft="updateDraft"
|
|
86
|
+
@send="send"
|
|
87
|
+
@open-model="openModelSheet"
|
|
88
|
+
@open-effort="openEffortSheet"
|
|
89
|
+
@files="attachFiles"
|
|
90
|
+
@voice="toggleVoice"
|
|
91
|
+
@hold-start="onHoldStart"
|
|
92
|
+
@hold-end="onHoldEnd"
|
|
93
|
+
@close-model="modelSheet = false"
|
|
94
|
+
@choose-model="chooseModel"
|
|
95
|
+
@close-effort="effortSheet = false"
|
|
96
|
+
@choose-effort="chooseEffort"
|
|
97
|
+
@close-preview="closePreview"
|
|
98
|
+
@remove-image="removeDraftImage"
|
|
99
|
+
@open-commands="loadCommands"
|
|
100
|
+
@open-references="loadReferences"
|
|
101
|
+
@pick-command="pickCommand"
|
|
108
102
|
/>
|
|
109
103
|
</div>
|
|
110
104
|
</template>
|
|
@@ -116,24 +110,48 @@ import { createT } from "./i18n.js";
|
|
|
116
110
|
import { failText, withPendingUser } from "./chat/format.js";
|
|
117
111
|
import { createVoiceCapture } from "./chat/voice.js";
|
|
118
112
|
import { appendDraftMentions } from "@olares/lares-core/files/mention";
|
|
119
|
-
import { FileIntake, partitionDocumentsBySize } from "@olares/lares-core/files/intake";
|
|
113
|
+
import { FileIntake, partitionDocumentsBySize, splitComposerFiles } from "@olares/lares-core/files/intake";
|
|
114
|
+
import {
|
|
115
|
+
createDraftImageEntry,
|
|
116
|
+
draftHasSendableContent,
|
|
117
|
+
draftImageStatus,
|
|
118
|
+
draftImagesUploading,
|
|
119
|
+
} from "@olares/lares-core/files/draft-images";
|
|
120
120
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@olares/lares-core/files/limits";
|
|
121
121
|
import { groupModelsByProvider, effortMenuRows, reasoningOfModel, currentEffortId, selectionKey } from "@olares/lares-core/router/session-model";
|
|
122
122
|
import { messageFromCode } from "@olares/lares-core/i18n/t";
|
|
123
123
|
import { parseAskUserQuestions, singleSelectAnswer } from "@olares/lares-core/larepass/questions";
|
|
124
|
+
import { applyCommand } from "@olares/lares-core/larepass/commands";
|
|
125
|
+
import { permissionOption, presetDescription, presetLabel } from "@olares/lares-core/larepass/permissions";
|
|
126
|
+
import { DEFAULT_BUSY_ENTER } from "@olares/lares-core/larepass/submission-settings";
|
|
127
|
+
import { subscribeConversationSettings } from "@olares/lares-core/larepass/settings";
|
|
128
|
+
import { markSeen, pruneSeen, readSeen, SEEN_STORAGE_KEY, unseenSessions } from "@olares/lares-core/larepass/unread";
|
|
129
|
+
import { fileName } from "@olares/lares-core/files/filename";
|
|
130
|
+
import { closeTab, openTab, touchTab } from "@olares/lares-core/files/preview-tabs";
|
|
124
131
|
import { chatDevice as resolveChatDevice } from "./layout.js";
|
|
125
|
-
import
|
|
126
|
-
import
|
|
127
|
-
import LaresTranscript from "./chat/Transcript.vue";
|
|
128
|
-
import LaresComposer from "./chat/Composer.vue";
|
|
129
|
-
import LaresPreview from "./preview/Preview.vue";
|
|
130
|
-
import LaresSheet from "./settings/Sheet.vue";
|
|
131
|
-
import LaresSettingRow from "./settings/SettingRow.vue";
|
|
132
|
+
import LaresMobileShell from "./mobile/MobileShell.vue";
|
|
133
|
+
import LaresDesktopShell from "./desktop/DesktopShell.vue";
|
|
132
134
|
import "./styles.css";
|
|
133
135
|
|
|
136
|
+
function imageData(file) {
|
|
137
|
+
return file.arrayBuffer().then((buffer) => {
|
|
138
|
+
const bytes = new Uint8Array(buffer);
|
|
139
|
+
let binary = "";
|
|
140
|
+
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
|
141
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
type: "image",
|
|
145
|
+
mediaType: file.type,
|
|
146
|
+
data: btoa(binary),
|
|
147
|
+
...(file.name ? { name: file.name } : {}),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
export default {
|
|
135
153
|
name: "LaresApp",
|
|
136
|
-
components: {
|
|
154
|
+
components: { LaresMobileShell, LaresDesktopShell },
|
|
137
155
|
props: {
|
|
138
156
|
locale: { type: String, default: "en" },
|
|
139
157
|
device: { type: String, default: "mobile" },
|
|
@@ -148,18 +166,26 @@ export default {
|
|
|
148
166
|
data() {
|
|
149
167
|
return {
|
|
150
168
|
sending: false,
|
|
169
|
+
stopping: false,
|
|
151
170
|
starting: false,
|
|
152
171
|
hostGen: 0,
|
|
172
|
+
modelGen: 0,
|
|
153
173
|
running: false,
|
|
154
174
|
sessionId: "",
|
|
155
175
|
draft: "",
|
|
176
|
+
draftBySession: {},
|
|
177
|
+
imagesBySession: {},
|
|
156
178
|
failed: "",
|
|
157
179
|
error: "",
|
|
158
180
|
items: [],
|
|
159
181
|
pendingUser: "",
|
|
160
182
|
unsub: null,
|
|
161
183
|
previews: {},
|
|
162
|
-
|
|
184
|
+
previewTabs: [],
|
|
185
|
+
previewLru: [],
|
|
186
|
+
previewPath: "",
|
|
187
|
+
previewMode: "chat",
|
|
188
|
+
previewContents: {},
|
|
163
189
|
panel: "",
|
|
164
190
|
sessions: [],
|
|
165
191
|
sessionsReady: false,
|
|
@@ -171,6 +197,7 @@ export default {
|
|
|
171
197
|
effortSheet: false,
|
|
172
198
|
effortByModel: {},
|
|
173
199
|
upload: { pending: 0, failures: [] },
|
|
200
|
+
draftImages: [],
|
|
174
201
|
voicePhase: "idle",
|
|
175
202
|
voiceElapsed: 0,
|
|
176
203
|
voiceError: "",
|
|
@@ -178,6 +205,42 @@ export default {
|
|
|
178
205
|
intakeUnsub: null,
|
|
179
206
|
question: null,
|
|
180
207
|
questionBusy: false,
|
|
208
|
+
approval: null,
|
|
209
|
+
approvalBusy: false,
|
|
210
|
+
workspaces: [],
|
|
211
|
+
workspacesReady: false,
|
|
212
|
+
workspaceError: "",
|
|
213
|
+
archivedSessionIds: [],
|
|
214
|
+
seen: { since: 0, sessions: {} },
|
|
215
|
+
permissions: null,
|
|
216
|
+
plan: null,
|
|
217
|
+
contextPressure: null,
|
|
218
|
+
contextBreakdown: null,
|
|
219
|
+
goal: null,
|
|
220
|
+
goalBusy: false,
|
|
221
|
+
todos: null,
|
|
222
|
+
permissionBusy: false,
|
|
223
|
+
commands: null,
|
|
224
|
+
commandsSessionId: "",
|
|
225
|
+
commandsError: "",
|
|
226
|
+
references: [],
|
|
227
|
+
referencesLoading: false,
|
|
228
|
+
referencesError: "",
|
|
229
|
+
referenceGen: 0,
|
|
230
|
+
queue: [],
|
|
231
|
+
queueBusy: "",
|
|
232
|
+
busyEnter: DEFAULT_BUSY_ENTER,
|
|
233
|
+
conversationUnsub: null,
|
|
234
|
+
directory: {
|
|
235
|
+
open: false,
|
|
236
|
+
loading: false,
|
|
237
|
+
listing: null,
|
|
238
|
+
error: "",
|
|
239
|
+
creating: false,
|
|
240
|
+
creatingFolder: false,
|
|
241
|
+
showHidden: false,
|
|
242
|
+
selectPath: "",
|
|
243
|
+
},
|
|
181
244
|
};
|
|
182
245
|
},
|
|
183
246
|
computed: {
|
|
@@ -198,8 +261,116 @@ export default {
|
|
|
198
261
|
chatDevice() {
|
|
199
262
|
return resolveChatDevice(this.device);
|
|
200
263
|
},
|
|
264
|
+
currentWorkspaceId() {
|
|
265
|
+
return this.workspaces.find((workspace) => (
|
|
266
|
+
workspace.sessionIds?.includes(this.sessionId)
|
|
267
|
+
))?.workspaceId || "";
|
|
268
|
+
},
|
|
269
|
+
shellState() {
|
|
270
|
+
return {
|
|
271
|
+
sending: this.sending,
|
|
272
|
+
stopping: this.stopping,
|
|
273
|
+
starting: this.starting,
|
|
274
|
+
running: this.running,
|
|
275
|
+
sessionId: this.sessionId,
|
|
276
|
+
sessionTitle: this.sessionTitle,
|
|
277
|
+
draft: this.draft,
|
|
278
|
+
failed: this.failed,
|
|
279
|
+
failText: this.failText,
|
|
280
|
+
viewItems: this.viewItems,
|
|
281
|
+
historyLoading: this.historyLoading,
|
|
282
|
+
previews: this.previews,
|
|
283
|
+
preview: this.preview,
|
|
284
|
+
previewTabs: this.previewTabs,
|
|
285
|
+
previewMode: this.previewMode,
|
|
286
|
+
previewMediaSrc: this.previewMediaSrc,
|
|
287
|
+
previewDownloadHref: this.previewDownloadHref,
|
|
288
|
+
panel: this.panel,
|
|
289
|
+
sessions: this.sessions,
|
|
290
|
+
sessionsReady: this.sessionsReady,
|
|
291
|
+
models: this.models,
|
|
292
|
+
modelError: this.modelError,
|
|
293
|
+
modelGroups: this.modelGroups,
|
|
294
|
+
modelCurrent: this.modelCurrent,
|
|
295
|
+
modelLabel: this.modelLabel,
|
|
296
|
+
modelBusy: this.modelBusy,
|
|
297
|
+
modelBlocked: this.modelBlocked,
|
|
298
|
+
modelSheet: this.modelSheet,
|
|
299
|
+
effortSheet: this.effortSheet,
|
|
300
|
+
effortRows: this.effortRows,
|
|
301
|
+
effortId: this.effortId,
|
|
302
|
+
effortLabel: this.effortLabel,
|
|
303
|
+
effortDisabled: this.effortDisabled,
|
|
304
|
+
upload: this.upload,
|
|
305
|
+
draftImages: this.draftImages,
|
|
306
|
+
voicePhase: this.voicePhase,
|
|
307
|
+
voiceElapsed: this.voiceElapsed,
|
|
308
|
+
voiceErrorText: this.voiceErrorText,
|
|
309
|
+
question: this.question,
|
|
310
|
+
questionBusy: this.questionBusy,
|
|
311
|
+
approval: this.approval,
|
|
312
|
+
approvalBusy: this.approvalBusy,
|
|
313
|
+
canSend: this.canSend,
|
|
314
|
+
workspaces: this.workspaces,
|
|
315
|
+
workspacesReady: this.workspacesReady,
|
|
316
|
+
workspaceError: this.workspaceError,
|
|
317
|
+
archivedSessionIds: this.archivedSessionIds,
|
|
318
|
+
currentWorkspaceId: this.currentWorkspaceId,
|
|
319
|
+
unseen: this.unseen,
|
|
320
|
+
permissionRows: this.permissionRows,
|
|
321
|
+
permissionCurrent: this.permissionCurrent,
|
|
322
|
+
permissionLabel: this.permissionLabel,
|
|
323
|
+
permissionHint: this.permissionHint,
|
|
324
|
+
permissionBusy: this.permissionBusy,
|
|
325
|
+
plan: this.plan,
|
|
326
|
+
planActive: this.planActive,
|
|
327
|
+
contextPressure: this.contextPressure,
|
|
328
|
+
contextBreakdown: this.contextBreakdown,
|
|
329
|
+
goal: this.goal,
|
|
330
|
+
goalBusy: this.goalBusy,
|
|
331
|
+
todos: this.todos,
|
|
332
|
+
commands: this.commands,
|
|
333
|
+
commandsError: this.commandsError,
|
|
334
|
+
references: this.references,
|
|
335
|
+
referencesLoading: this.referencesLoading,
|
|
336
|
+
referencesError: this.referencesError,
|
|
337
|
+
queue: this.queue,
|
|
338
|
+
queueBusy: this.queueBusy,
|
|
339
|
+
busyEnter: this.busyEnter,
|
|
340
|
+
directory: this.directory,
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
unseen() {
|
|
344
|
+
return unseenSessions(this.sessions, this.seen, this.sessionId);
|
|
345
|
+
},
|
|
346
|
+
permissionRows() {
|
|
347
|
+
return (this.permissions?.options ?? [])
|
|
348
|
+
.filter((option) => option.value !== "custom")
|
|
349
|
+
.map((option) => ({
|
|
350
|
+
value: option.value,
|
|
351
|
+
label: presetLabel(this.t, option),
|
|
352
|
+
hint: presetDescription(this.t, option),
|
|
353
|
+
}));
|
|
354
|
+
},
|
|
355
|
+
planActive() {
|
|
356
|
+
return Boolean(this.plan && (this.plan.pending ? !this.plan.active : this.plan.active));
|
|
357
|
+
},
|
|
358
|
+
permissionCurrent() {
|
|
359
|
+
return this.permissions?.currentValue || "";
|
|
360
|
+
},
|
|
361
|
+
permissionLabel() {
|
|
362
|
+
return presetLabel(this.t, permissionOption(this.permissions));
|
|
363
|
+
},
|
|
364
|
+
permissionHint() {
|
|
365
|
+
return presetDescription(this.t, permissionOption(this.permissions));
|
|
366
|
+
},
|
|
201
367
|
canSend() {
|
|
202
|
-
return
|
|
368
|
+
return draftHasSendableContent(this.draft, this.draftImages)
|
|
369
|
+
&& Boolean(this.sessionId)
|
|
370
|
+
&& !this.modelBlocked
|
|
371
|
+
&& !this.historyLoading
|
|
372
|
+
&& this.upload.pending === 0
|
|
373
|
+
&& !draftImagesUploading(this.draftImages);
|
|
203
374
|
},
|
|
204
375
|
sessionTitle() {
|
|
205
376
|
const row = this.sessions.find((item) => item.sessionId === this.sessionId);
|
|
@@ -220,6 +391,9 @@ export default {
|
|
|
220
391
|
return reasoningOfModel(this.currentListedModel);
|
|
221
392
|
},
|
|
222
393
|
effortId() {
|
|
394
|
+
if (this.chatDevice === "desktop") {
|
|
395
|
+
return currentEffortId(this.models?.default, this.reasoning);
|
|
396
|
+
}
|
|
223
397
|
if (!(this.modelCurrent in this.effortByModel)) {
|
|
224
398
|
return currentEffortId(this.models?.default, this.reasoning);
|
|
225
399
|
}
|
|
@@ -241,6 +415,11 @@ export default {
|
|
|
241
415
|
failText() {
|
|
242
416
|
return failText(this.t, this.failed, this.error);
|
|
243
417
|
},
|
|
418
|
+
preview() {
|
|
419
|
+
if (!this.previewPath) return { path: "", status: "idle", data: null, error: "" };
|
|
420
|
+
const content = this.previewContents[this.previewPath];
|
|
421
|
+
return { path: this.previewPath, ...(content ?? { status: "loading", data: null, error: "" }) };
|
|
422
|
+
},
|
|
244
423
|
previewMediaSrc() {
|
|
245
424
|
const data = this.preview.data;
|
|
246
425
|
if (!data) return "";
|
|
@@ -272,6 +451,9 @@ export default {
|
|
|
272
451
|
modelBusy() {
|
|
273
452
|
return Boolean(this.modelPending) || (this.modelSheet && !this.models);
|
|
274
453
|
},
|
|
454
|
+
modelBlocked() {
|
|
455
|
+
return this.chatDevice === "desktop" && this.models?.routable === false;
|
|
456
|
+
},
|
|
275
457
|
voiceErrorText() {
|
|
276
458
|
return this.voiceError ? messageFromCode(this.t, this.voiceError, "error.voice_failed") : "";
|
|
277
459
|
},
|
|
@@ -291,7 +473,10 @@ export default {
|
|
|
291
473
|
},
|
|
292
474
|
},
|
|
293
475
|
created() {
|
|
476
|
+
// Per-path request tokens: reactive state would re-render on every read.
|
|
477
|
+
this.previewGen = new Map();
|
|
294
478
|
this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
|
|
479
|
+
this.imageUploadControllers = new Map();
|
|
295
480
|
this.voiceCap = createVoiceCapture({
|
|
296
481
|
transcribe: (blob, signal) => this.runtime.transcribe(blob, this.voiceLanguage, signal),
|
|
297
482
|
getDraft: () => this.draft,
|
|
@@ -312,8 +497,19 @@ export default {
|
|
|
312
497
|
} catch {
|
|
313
498
|
this.effortByModel = {};
|
|
314
499
|
}
|
|
500
|
+
try {
|
|
501
|
+
this.seen = readSeen(JSON.parse(localStorage.getItem(SEEN_STORAGE_KEY) || "null"));
|
|
502
|
+
} catch {
|
|
503
|
+
this.seen = readSeen(null);
|
|
504
|
+
}
|
|
505
|
+
// `since` is the floor for sessions this device has never opened, so pin it
|
|
506
|
+
// on the first launch instead of letting every launch move it forward.
|
|
507
|
+
this.persistSeen();
|
|
315
508
|
},
|
|
316
509
|
mounted() {
|
|
510
|
+
this.conversationUnsub = subscribeConversationSettings((section) => {
|
|
511
|
+
this.busyEnter = section.busyEnter;
|
|
512
|
+
});
|
|
317
513
|
this.retry();
|
|
318
514
|
},
|
|
319
515
|
activated() {
|
|
@@ -324,9 +520,14 @@ export default {
|
|
|
324
520
|
},
|
|
325
521
|
beforeUnmount() {
|
|
326
522
|
this.captureLog();
|
|
523
|
+
this.noteSeen(this.sessionId);
|
|
524
|
+
for (const controller of this.imageUploadControllers?.values?.() ?? []) controller.abort();
|
|
525
|
+
this.imageUploadControllers?.clear?.();
|
|
327
526
|
this.voiceCap?.dispose();
|
|
527
|
+
this.conversationUnsub?.();
|
|
328
528
|
this.intakeUnsub?.();
|
|
329
529
|
if (this.sessionId) this.intake?.cancelSession(this.sessionId);
|
|
530
|
+
this.releaseDraftImages();
|
|
330
531
|
this.unsub?.();
|
|
331
532
|
this.unsub = null;
|
|
332
533
|
},
|
|
@@ -344,18 +545,36 @@ export default {
|
|
|
344
545
|
return this.runtime.sticking();
|
|
345
546
|
},
|
|
346
547
|
restoreLog() {
|
|
347
|
-
this.$refs.
|
|
548
|
+
this.$refs.shell?.restoreLog?.();
|
|
348
549
|
},
|
|
349
550
|
captureLog() {
|
|
350
|
-
this.$refs.
|
|
551
|
+
this.$refs.shell?.captureLog?.();
|
|
351
552
|
},
|
|
352
553
|
pinLog() {
|
|
353
|
-
this.$refs.
|
|
554
|
+
this.$refs.shell?.pinLog?.();
|
|
354
555
|
},
|
|
355
556
|
applySnap(snap) {
|
|
356
|
-
|
|
557
|
+
const sessionChanged = snap.sessionId !== this.sessionId;
|
|
558
|
+
if (sessionChanged) {
|
|
559
|
+
const previousKey = this.sessionId || "@pending";
|
|
560
|
+
const nextKey = snap.sessionId || "@pending";
|
|
561
|
+
this.draftBySession = { ...this.draftBySession, [previousKey]: this.draft };
|
|
562
|
+
this.imagesBySession = { ...this.imagesBySession, [previousKey]: this.draftImages };
|
|
563
|
+
if (previousKey === "@pending" && snap.sessionId && !(nextKey in this.draftBySession)) {
|
|
564
|
+
this.draftBySession = { ...this.draftBySession, [nextKey]: this.draft };
|
|
565
|
+
this.imagesBySession = { ...this.imagesBySession, [nextKey]: this.draftImages };
|
|
566
|
+
}
|
|
567
|
+
this.draft = this.draftBySession[nextKey] ?? "";
|
|
568
|
+
this.draftImages = this.imagesBySession[nextKey] ?? [];
|
|
569
|
+
// Leaving a session marks everything in it as watched, and arriving
|
|
570
|
+
// clears the dot on the one now on screen.
|
|
571
|
+
this.noteSeen(this.sessionId);
|
|
572
|
+
this.noteSeen(snap.sessionId);
|
|
357
573
|
this.pendingUser = "";
|
|
358
574
|
this.previews = {};
|
|
575
|
+
this.resetPreview();
|
|
576
|
+
this.commands = null;
|
|
577
|
+
this.commandsError = "";
|
|
359
578
|
}
|
|
360
579
|
this.sessionId = snap.sessionId;
|
|
361
580
|
this.items = snap.items;
|
|
@@ -364,14 +583,111 @@ export default {
|
|
|
364
583
|
this.error = snap.error;
|
|
365
584
|
this.sessions = snap.sessions ?? this.sessions;
|
|
366
585
|
this.sessionsReady = Boolean(snap.sessionsReady);
|
|
586
|
+
this.workspaces = snap.workspaces ?? this.workspaces;
|
|
587
|
+
this.workspacesReady = Boolean(snap.workspacesReady);
|
|
588
|
+
this.workspaceError = snap.workspaceError || "";
|
|
589
|
+
this.archivedSessionIds = snap.archivedSessionIds ?? this.archivedSessionIds;
|
|
367
590
|
this.historyLoading = Boolean(snap.historyLoading);
|
|
591
|
+
this.permissions = snap.permissions ?? null;
|
|
592
|
+
this.plan = snap.plan ?? null;
|
|
593
|
+
this.contextPressure = snap.contextPressure ?? null;
|
|
594
|
+
this.contextBreakdown = snap.contextBreakdown ?? null;
|
|
595
|
+
this.goal = snap.goal ?? null;
|
|
596
|
+
this.todos = snap.todos ?? null;
|
|
597
|
+
this.queue = snap.queue ?? [];
|
|
368
598
|
this.question = snap.question || null;
|
|
369
599
|
if (!this.question) this.questionBusy = false;
|
|
600
|
+
this.approval = snap.approval || null;
|
|
601
|
+
if (!this.approval) this.approvalBusy = false;
|
|
602
|
+
if (sessionChanged && this.chatDevice === "desktop") {
|
|
603
|
+
void this.loadSessionModels(snap.sessionId);
|
|
604
|
+
}
|
|
370
605
|
if (this.pendingUser && snap.items.some((row) => row.type === "user" && row.text === this.pendingUser)) {
|
|
371
606
|
this.pendingUser = "";
|
|
372
607
|
}
|
|
373
608
|
this.pinLog();
|
|
374
609
|
},
|
|
610
|
+
noteSeen(sessionId) {
|
|
611
|
+
if (!sessionId) return;
|
|
612
|
+
const next = markSeen(this.seen, sessionId);
|
|
613
|
+
if (next === this.seen) return;
|
|
614
|
+
this.seen = next;
|
|
615
|
+
this.persistSeen();
|
|
616
|
+
},
|
|
617
|
+
updateDraft(value) {
|
|
618
|
+
this.draft = String(value ?? "");
|
|
619
|
+
const key = this.sessionId || "@pending";
|
|
620
|
+
this.draftBySession = { ...this.draftBySession, [key]: this.draft };
|
|
621
|
+
},
|
|
622
|
+
persistSeen() {
|
|
623
|
+
if (this.sessionsReady && this.sessions.length) {
|
|
624
|
+
this.seen = pruneSeen(this.seen, this.sessions);
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
localStorage.setItem(SEEN_STORAGE_KEY, JSON.stringify(this.seen));
|
|
628
|
+
} catch {
|
|
629
|
+
// storage may be blocked
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
async choosePermission(value) {
|
|
633
|
+
if (!value || value === this.permissionCurrent || this.permissionBusy) return;
|
|
634
|
+
this.permissionBusy = true;
|
|
635
|
+
try {
|
|
636
|
+
const switched = await this.runtime.setPermission(value);
|
|
637
|
+
if (!switched.ok) {
|
|
638
|
+
this.failed = switched.error?.message || switched.error?.code || "permission";
|
|
639
|
+
}
|
|
640
|
+
} finally {
|
|
641
|
+
this.permissionBusy = false;
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
async disablePlan() {
|
|
645
|
+
if (!this.planActive) return;
|
|
646
|
+
const result = await this.runtime.runCommand("/plan off");
|
|
647
|
+
if (!result.ok) this.failed = result.error?.message || result.error?.code || "plan";
|
|
648
|
+
},
|
|
649
|
+
async goalAction(kind, value) {
|
|
650
|
+
if (this.goalBusy) return;
|
|
651
|
+
this.goalBusy = true;
|
|
652
|
+
try {
|
|
653
|
+
const result = await this.runtime.goalAction(kind, value);
|
|
654
|
+
if (!result.ok) this.failed = result.error?.message || result.error?.code || "goal";
|
|
655
|
+
} finally {
|
|
656
|
+
this.goalBusy = false;
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
async loadCommands() {
|
|
660
|
+
if (this.commands && this.commandsSessionId === this.sessionId) return;
|
|
661
|
+
this.commandsError = "";
|
|
662
|
+
try {
|
|
663
|
+
this.commands = await this.runtime.listCommands();
|
|
664
|
+
this.commandsSessionId = this.sessionId;
|
|
665
|
+
} catch (err) {
|
|
666
|
+
this.commands = [];
|
|
667
|
+
this.commandsError = err instanceof Error ? err.message : String(err);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
async loadReferences({ query = "", quoted = false } = {}) {
|
|
671
|
+
const gen = ++this.referenceGen;
|
|
672
|
+
this.referencesLoading = true;
|
|
673
|
+
this.referencesError = "";
|
|
674
|
+
try {
|
|
675
|
+
const rows = await this.runtime.listReferences(query, quoted);
|
|
676
|
+
if (gen !== this.referenceGen) return;
|
|
677
|
+
this.references = rows;
|
|
678
|
+
} catch (err) {
|
|
679
|
+
if (gen !== this.referenceGen) return;
|
|
680
|
+
this.references = [];
|
|
681
|
+
this.referencesError = err instanceof Error ? err.message : String(err);
|
|
682
|
+
} finally {
|
|
683
|
+
if (gen === this.referenceGen) this.referencesLoading = false;
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
pickCommand(command) {
|
|
687
|
+
if (!command?.name) return;
|
|
688
|
+
this.updateDraft(applyCommand(this.draft, command));
|
|
689
|
+
this.$refs.shell?.focusComposer?.();
|
|
690
|
+
},
|
|
375
691
|
async hydrateFiles(paths) {
|
|
376
692
|
const missing = [...new Set(paths)].filter((path) => !(path in this.previews));
|
|
377
693
|
if (!missing.length) return;
|
|
@@ -400,30 +716,122 @@ export default {
|
|
|
400
716
|
this.questionBusy = false;
|
|
401
717
|
}
|
|
402
718
|
},
|
|
403
|
-
async
|
|
404
|
-
|
|
719
|
+
async answerQuestions(answer) {
|
|
720
|
+
if (!answer?.answers?.length || this.questionBusy) return;
|
|
721
|
+
this.questionBusy = true;
|
|
722
|
+
try {
|
|
723
|
+
const result = await this.runtime.answerQuestion(answer.answers);
|
|
724
|
+
if (!result?.ok) this.questionBusy = false;
|
|
725
|
+
} catch {
|
|
726
|
+
this.questionBusy = false;
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
async answerApproval(outcome) {
|
|
730
|
+
if (!this.approval || this.approvalBusy) return;
|
|
731
|
+
this.approvalBusy = true;
|
|
732
|
+
try {
|
|
733
|
+
const result = await this.runtime.answerApproval(outcome);
|
|
734
|
+
if (!result?.ok) this.approvalBusy = false;
|
|
735
|
+
} catch {
|
|
736
|
+
this.approvalBusy = false;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
openFile(path) {
|
|
740
|
+
const { tabs, lru, evicted } = openTab({ tabs: this.previewTabs, lru: this.previewLru }, path);
|
|
741
|
+
this.previewTabs = tabs;
|
|
742
|
+
this.previewLru = lru;
|
|
743
|
+
if (evicted) this.dropPreviewContent(evicted.path);
|
|
744
|
+
this.previewPath = path;
|
|
745
|
+
this.previewMode = "preview";
|
|
746
|
+
return this.loadPreview(path);
|
|
747
|
+
},
|
|
748
|
+
showChat() {
|
|
749
|
+
this.previewMode = "chat";
|
|
750
|
+
},
|
|
751
|
+
closePreviewTab(path) {
|
|
752
|
+
const closed = closeTab(
|
|
753
|
+
{ tabs: this.previewTabs, lru: this.previewLru, activePath: this.previewPath },
|
|
754
|
+
path,
|
|
755
|
+
);
|
|
756
|
+
if (!closed) return;
|
|
757
|
+
this.previewTabs = closed.tabs;
|
|
758
|
+
this.previewLru = closed.lru;
|
|
759
|
+
this.previewPath = closed.activePath ?? "";
|
|
760
|
+
if (!closed.activePath) this.previewMode = "chat";
|
|
761
|
+
this.dropPreviewContent(path);
|
|
762
|
+
},
|
|
763
|
+
closePreview() {
|
|
764
|
+
if (this.previewPath) this.closePreviewTab(this.previewPath);
|
|
765
|
+
},
|
|
766
|
+
resetPreview() {
|
|
767
|
+
this.previewTabs = [];
|
|
768
|
+
this.previewLru = [];
|
|
769
|
+
this.previewPath = "";
|
|
770
|
+
this.previewMode = "chat";
|
|
771
|
+
this.previewContents = {};
|
|
772
|
+
this.previewGen.clear();
|
|
773
|
+
},
|
|
774
|
+
dropPreviewContent(path) {
|
|
775
|
+
const rest = { ...this.previewContents };
|
|
776
|
+
delete rest[path];
|
|
777
|
+
this.previewContents = rest;
|
|
778
|
+
this.previewGen.delete(path);
|
|
779
|
+
},
|
|
780
|
+
/**
|
|
781
|
+
* The Host answers with its own spelling of the path, so a file opened from
|
|
782
|
+
* a produced-file chip and the same file reached through a markdown link
|
|
783
|
+
* end up on one tab instead of two.
|
|
784
|
+
*/
|
|
785
|
+
adoptPreviewPath(requested, answered) {
|
|
786
|
+
const canonical = answered || requested;
|
|
787
|
+
if (canonical === requested) return requested;
|
|
788
|
+
this.previewTabs = this.previewTabs.some((tab) => tab.path === canonical)
|
|
789
|
+
? this.previewTabs.filter((tab) => tab.path !== requested)
|
|
790
|
+
: this.previewTabs.map((tab) => (
|
|
791
|
+
tab.path === requested ? { path: canonical, name: fileName(canonical) } : tab
|
|
792
|
+
));
|
|
793
|
+
this.previewLru = touchTab(this.previewLru.filter((item) => item !== requested), canonical);
|
|
794
|
+
if (this.previewPath === requested) this.previewPath = canonical;
|
|
795
|
+
this.dropPreviewContent(requested);
|
|
796
|
+
return canonical;
|
|
797
|
+
},
|
|
798
|
+
/**
|
|
799
|
+
* Reopening a tab re-reads the file: the agent may have rewritten it since.
|
|
800
|
+
* Cached content stays on screen until the fresh read lands, so switching
|
|
801
|
+
* tabs does not flash a spinner over a file that is already there.
|
|
802
|
+
*/
|
|
803
|
+
async loadPreview(path) {
|
|
804
|
+
const gen = (this.previewGen.get(path) ?? 0) + 1;
|
|
805
|
+
this.previewGen.set(path, gen);
|
|
806
|
+
if (this.previewContents[path]?.status !== "ready") {
|
|
807
|
+
this.setPreviewContent(path, gen, { status: "loading", data: null, error: "" });
|
|
808
|
+
}
|
|
405
809
|
try {
|
|
406
810
|
const data = await this.runtime.preview(path);
|
|
811
|
+
if (this.previewGen.get(path) !== gen) return;
|
|
407
812
|
this.previews = { ...this.previews, [path]: data };
|
|
408
|
-
|
|
813
|
+
const canonical = this.adoptPreviewPath(path, data?.path);
|
|
814
|
+
this.previewGen.set(canonical, gen);
|
|
815
|
+
this.setPreviewContent(canonical, gen, { status: "ready", data, error: "" });
|
|
409
816
|
} catch (err) {
|
|
410
|
-
this.
|
|
411
|
-
path,
|
|
817
|
+
this.setPreviewContent(path, gen, {
|
|
412
818
|
status: "error",
|
|
413
819
|
data: null,
|
|
414
820
|
error: err instanceof Error ? err.message : "file_preview_failed",
|
|
415
|
-
};
|
|
821
|
+
});
|
|
416
822
|
}
|
|
417
823
|
},
|
|
418
|
-
|
|
419
|
-
this.
|
|
824
|
+
setPreviewContent(path, gen, content) {
|
|
825
|
+
if (this.previewGen.get(path) !== gen) return;
|
|
826
|
+
this.previewContents = { ...this.previewContents, [path]: content };
|
|
420
827
|
},
|
|
421
828
|
async retry() {
|
|
422
829
|
const gen = ++this.hostGen;
|
|
423
830
|
this.starting = true;
|
|
424
831
|
try {
|
|
425
|
-
await this.runtime.start();
|
|
832
|
+
await this.runtime.start({ workspaces: this.chatDevice === "desktop" });
|
|
426
833
|
if (gen !== this.hostGen) return;
|
|
834
|
+
if (this.chatDevice !== "desktop") await this.runtime.refreshWorkspaces();
|
|
427
835
|
await this.loadComposerMeta();
|
|
428
836
|
} finally {
|
|
429
837
|
if (gen === this.hostGen) this.starting = false;
|
|
@@ -438,6 +846,9 @@ export default {
|
|
|
438
846
|
},
|
|
439
847
|
switchHost() {
|
|
440
848
|
this.voiceCap?.stop?.(true);
|
|
849
|
+
for (const controller of this.imageUploadControllers.values()) controller.abort();
|
|
850
|
+
this.imageUploadControllers.clear();
|
|
851
|
+
this.releaseDraftImages();
|
|
441
852
|
this.intakeUnsub?.();
|
|
442
853
|
if (this.sessionId) this.intake?.cancelSession(this.sessionId);
|
|
443
854
|
this.sessionId = "";
|
|
@@ -445,7 +856,7 @@ export default {
|
|
|
445
856
|
this.sessions = [];
|
|
446
857
|
this.sessionsReady = false;
|
|
447
858
|
this.previews = {};
|
|
448
|
-
this.
|
|
859
|
+
this.resetPreview();
|
|
449
860
|
this.pendingUser = "";
|
|
450
861
|
this.failed = "";
|
|
451
862
|
this.error = "";
|
|
@@ -456,6 +867,34 @@ export default {
|
|
|
456
867
|
this.panel = "";
|
|
457
868
|
this.question = null;
|
|
458
869
|
this.questionBusy = false;
|
|
870
|
+
this.approval = null;
|
|
871
|
+
this.approvalBusy = false;
|
|
872
|
+
this.workspaces = [];
|
|
873
|
+
this.workspacesReady = false;
|
|
874
|
+
this.workspaceError = "";
|
|
875
|
+
this.archivedSessionIds = [];
|
|
876
|
+
this.permissions = null;
|
|
877
|
+
this.permissionBusy = false;
|
|
878
|
+
this.goal = null;
|
|
879
|
+
this.goalBusy = false;
|
|
880
|
+
this.todos = null;
|
|
881
|
+
this.commands = null;
|
|
882
|
+
this.commandsSessionId = "";
|
|
883
|
+
this.commandsError = "";
|
|
884
|
+
this.references = [];
|
|
885
|
+
this.referencesLoading = false;
|
|
886
|
+
this.referencesError = "";
|
|
887
|
+
this.referenceGen += 1;
|
|
888
|
+
this.queue = [];
|
|
889
|
+
this.queueBusy = "";
|
|
890
|
+
this.busyEnter = DEFAULT_BUSY_ENTER;
|
|
891
|
+
this.directory = {
|
|
892
|
+
open: false,
|
|
893
|
+
loading: false,
|
|
894
|
+
listing: null,
|
|
895
|
+
error: "",
|
|
896
|
+
creating: false,
|
|
897
|
+
};
|
|
459
898
|
this.bindIntake("");
|
|
460
899
|
this.bindRuntime();
|
|
461
900
|
this.retry();
|
|
@@ -475,11 +914,22 @@ export default {
|
|
|
475
914
|
async loadComposerMeta() {
|
|
476
915
|
const settings = this.runtime.settings;
|
|
477
916
|
if (!settings) return;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
917
|
+
if (this.chatDevice === "desktop") {
|
|
918
|
+
await Promise.all([
|
|
919
|
+
this.loadSessionModels(this.sessionId),
|
|
920
|
+
settings.conversation().then((section) => {
|
|
921
|
+
this.busyEnter = section.busyEnter;
|
|
922
|
+
}).catch(() => {
|
|
923
|
+
this.busyEnter = DEFAULT_BUSY_ENTER;
|
|
924
|
+
}),
|
|
925
|
+
]);
|
|
926
|
+
} else {
|
|
927
|
+
try {
|
|
928
|
+
this.models = await settings.models();
|
|
929
|
+
this.modelError = "";
|
|
930
|
+
} catch (err) {
|
|
931
|
+
this.modelError = err instanceof Error ? err.message : String(err);
|
|
932
|
+
}
|
|
483
933
|
}
|
|
484
934
|
try {
|
|
485
935
|
const voice = await settings.voice();
|
|
@@ -488,6 +938,34 @@ export default {
|
|
|
488
938
|
this.voiceLanguage = "";
|
|
489
939
|
}
|
|
490
940
|
},
|
|
941
|
+
async loadSessionModels(sessionId) {
|
|
942
|
+
const gen = ++this.modelGen;
|
|
943
|
+
if (!sessionId) {
|
|
944
|
+
this.models = null;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
this.modelError = "";
|
|
948
|
+
const loaded = await this.runtime.sessionModels(sessionId);
|
|
949
|
+
if (gen !== this.modelGen || sessionId !== this.sessionId) return;
|
|
950
|
+
if (!loaded.ok) {
|
|
951
|
+
this.models = null;
|
|
952
|
+
this.modelError = loaded.error?.message || loaded.error?.code || "models";
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const value = loaded.value ?? {};
|
|
956
|
+
this.models = {
|
|
957
|
+
default: value.current,
|
|
958
|
+
routable: value.routable !== false,
|
|
959
|
+
failures: value.failures ?? [],
|
|
960
|
+
models: (value.groups ?? []).flatMap((group) => (
|
|
961
|
+
(group.models ?? []).map((model) => ({
|
|
962
|
+
...model,
|
|
963
|
+
provider: group.id,
|
|
964
|
+
providerName: group.name || group.id,
|
|
965
|
+
}))
|
|
966
|
+
)),
|
|
967
|
+
};
|
|
968
|
+
},
|
|
491
969
|
openEffortSheet() {
|
|
492
970
|
if (this.effortDisabled) return;
|
|
493
971
|
this.effortSheet = true;
|
|
@@ -504,9 +982,29 @@ export default {
|
|
|
504
982
|
// storage may be blocked
|
|
505
983
|
}
|
|
506
984
|
},
|
|
507
|
-
chooseEffort(id) {
|
|
985
|
+
async chooseEffort(id) {
|
|
508
986
|
this.effortSheet = false;
|
|
509
987
|
if (!this.modelCurrent) return;
|
|
988
|
+
if (this.chatDevice === "desktop") {
|
|
989
|
+
if (id === this.effortId || this.modelBusy) return;
|
|
990
|
+
this.modelPending = `${this.modelCurrent}\u0000${id ?? ""}`;
|
|
991
|
+
const current = this.models?.default;
|
|
992
|
+
try {
|
|
993
|
+
const selected = await this.runtime.selectModel({
|
|
994
|
+
provider: current.provider,
|
|
995
|
+
model: current.model,
|
|
996
|
+
...(id === undefined ? {} : { reasoningEffort: id }),
|
|
997
|
+
});
|
|
998
|
+
if (!selected.ok) {
|
|
999
|
+
this.modelError = selected.error?.message || selected.error?.code || "model";
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.models = { ...this.models, default: selected.value?.selected ?? current };
|
|
1003
|
+
} finally {
|
|
1004
|
+
this.modelPending = "";
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
510
1008
|
this.effortByModel = { ...this.effortByModel, [this.modelCurrent]: id ?? "" };
|
|
511
1009
|
this.persistEffort();
|
|
512
1010
|
},
|
|
@@ -524,26 +1022,121 @@ export default {
|
|
|
524
1022
|
this.modelPending = key;
|
|
525
1023
|
this.modelError = "";
|
|
526
1024
|
try {
|
|
527
|
-
this.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1025
|
+
if (this.chatDevice === "desktop") {
|
|
1026
|
+
const reasoning = reasoningOfModel(model);
|
|
1027
|
+
const selection = {
|
|
1028
|
+
provider: model.provider,
|
|
1029
|
+
model: model.id,
|
|
1030
|
+
...(reasoning?.defaultEffort === undefined
|
|
1031
|
+
? {}
|
|
1032
|
+
: { reasoningEffort: reasoning.defaultEffort }),
|
|
1033
|
+
};
|
|
1034
|
+
const selected = await this.runtime.selectModel(selection);
|
|
1035
|
+
if (!selected.ok) throw new Error(selected.error?.message || selected.error?.code || "model");
|
|
1036
|
+
this.models = { ...this.models, default: selected.value?.selected ?? selection };
|
|
1037
|
+
} else {
|
|
1038
|
+
this.models = await this.runtime.settings.setDefaultModel({
|
|
1039
|
+
provider: model.provider,
|
|
1040
|
+
model: model.id,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
531
1043
|
} catch (err) {
|
|
532
1044
|
this.modelError = err instanceof Error ? err.message : String(err);
|
|
533
1045
|
} finally {
|
|
534
1046
|
this.modelPending = "";
|
|
535
1047
|
}
|
|
536
1048
|
},
|
|
1049
|
+
patchDraftImage(sessionId, imageId, patch) {
|
|
1050
|
+
const rows = sessionId === this.sessionId
|
|
1051
|
+
? this.draftImages
|
|
1052
|
+
: (this.imagesBySession[sessionId] ?? []);
|
|
1053
|
+
const index = rows.findIndex((row) => row.id === imageId);
|
|
1054
|
+
if (index < 0) return;
|
|
1055
|
+
const previous = rows[index];
|
|
1056
|
+
if (patch.previewUrl && previous.previewUrl?.startsWith("blob:")) {
|
|
1057
|
+
URL.revokeObjectURL(previous.previewUrl);
|
|
1058
|
+
}
|
|
1059
|
+
const next = [...rows];
|
|
1060
|
+
next[index] = { ...previous, ...patch };
|
|
1061
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: next };
|
|
1062
|
+
if (sessionId === this.sessionId) this.draftImages = next;
|
|
1063
|
+
},
|
|
1064
|
+
async uploadDraftImage(sessionId, imageId) {
|
|
1065
|
+
const rows = sessionId === this.sessionId
|
|
1066
|
+
? this.draftImages
|
|
1067
|
+
: (this.imagesBySession[sessionId] ?? []);
|
|
1068
|
+
const image = rows.find((row) => row.id === imageId);
|
|
1069
|
+
if (!image?.file || draftImageStatus(image) !== "uploading") return;
|
|
1070
|
+
const controller = new AbortController();
|
|
1071
|
+
this.imageUploadControllers.set(imageId, controller);
|
|
1072
|
+
try {
|
|
1073
|
+
const result = await this.runtime.upload(
|
|
1074
|
+
image.file,
|
|
1075
|
+
{ signal: controller.signal, requestId: imageId },
|
|
1076
|
+
sessionId,
|
|
1077
|
+
);
|
|
1078
|
+
if (controller.signal.aborted) return;
|
|
1079
|
+
this.patchDraftImage(sessionId, imageId, {
|
|
1080
|
+
status: "ready",
|
|
1081
|
+
path: result.path,
|
|
1082
|
+
previewUrl: this.runtime.mediaUrl(result.path),
|
|
1083
|
+
});
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
if (controller.signal.aborted) return;
|
|
1086
|
+
this.patchDraftImage(sessionId, imageId, {
|
|
1087
|
+
status: "failed",
|
|
1088
|
+
error: err instanceof Error ? err.message : "file_upload_failed",
|
|
1089
|
+
});
|
|
1090
|
+
} finally {
|
|
1091
|
+
this.imageUploadControllers.delete(imageId);
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
537
1094
|
attachFiles(files) {
|
|
538
1095
|
const sessionId = this.sessionId;
|
|
539
1096
|
if (!sessionId || !files?.length) return;
|
|
540
|
-
const
|
|
541
|
-
const
|
|
1097
|
+
const { images, documents } = splitComposerFiles(files);
|
|
1098
|
+
const added = images.map((file) => createDraftImageEntry(file));
|
|
1099
|
+
if (added.length) {
|
|
1100
|
+
this.draftImages = [...this.draftImages, ...added];
|
|
1101
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: this.draftImages };
|
|
1102
|
+
for (const image of added) void this.uploadDraftImage(sessionId, image.id);
|
|
1103
|
+
}
|
|
1104
|
+
const { accepted, oversized } = partitionDocumentsBySize(documents, DEFAULT_MAX_UPLOAD_BYTES);
|
|
542
1105
|
for (const file of oversized) this.intake.reportFailure(sessionId, file, "file_too_large");
|
|
543
1106
|
void this.intake.uploadFiles(sessionId, accepted, (paths) => {
|
|
544
|
-
|
|
1107
|
+
const previous = sessionId === this.sessionId
|
|
1108
|
+
? this.draft
|
|
1109
|
+
: this.draftBySession[sessionId] ?? "";
|
|
1110
|
+
const next = appendDraftMentions(previous, paths);
|
|
1111
|
+
this.draftBySession = { ...this.draftBySession, [sessionId]: next };
|
|
1112
|
+
if (sessionId === this.sessionId) this.draft = next;
|
|
545
1113
|
});
|
|
546
1114
|
},
|
|
1115
|
+
removeDraftImage(id) {
|
|
1116
|
+
this.imageUploadControllers.get(id)?.abort();
|
|
1117
|
+
this.imageUploadControllers.delete(id);
|
|
1118
|
+
const image = this.draftImages.find((row) => row.id === id);
|
|
1119
|
+
if (image?.previewUrl?.startsWith("blob:")) URL.revokeObjectURL(image.previewUrl);
|
|
1120
|
+
this.draftImages = this.draftImages.filter((row) => row.id !== id);
|
|
1121
|
+
this.imagesBySession = { ...this.imagesBySession, [this.sessionId || "@pending"]: this.draftImages };
|
|
1122
|
+
},
|
|
1123
|
+
releaseDraftImages() {
|
|
1124
|
+
const seen = new Set();
|
|
1125
|
+
for (const rows of Object.values(this.imagesBySession)) {
|
|
1126
|
+
for (const image of rows) {
|
|
1127
|
+
if (seen.has(image.id)) continue;
|
|
1128
|
+
seen.add(image.id);
|
|
1129
|
+
if (image.previewUrl?.startsWith("blob:")) URL.revokeObjectURL(image.previewUrl);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
for (const image of this.draftImages) {
|
|
1133
|
+
if (!seen.has(image.id) && image.previewUrl?.startsWith("blob:")) {
|
|
1134
|
+
URL.revokeObjectURL(image.previewUrl);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
this.draftImages = [];
|
|
1138
|
+
this.imagesBySession = {};
|
|
1139
|
+
},
|
|
547
1140
|
onHoldStart() {
|
|
548
1141
|
if (this.voicePhase === "idle" || this.voicePhase === "error") this.voiceCap?.start();
|
|
549
1142
|
},
|
|
@@ -560,7 +1153,11 @@ export default {
|
|
|
560
1153
|
return;
|
|
561
1154
|
}
|
|
562
1155
|
this.panel = "history";
|
|
563
|
-
|
|
1156
|
+
// Refresh after the sheet slide so list work does not block the first paint.
|
|
1157
|
+
window.setTimeout(() => {
|
|
1158
|
+
if (this.panel !== "history") return;
|
|
1159
|
+
void Promise.all([this.runtime.refreshSessions(), this.runtime.refreshWorkspaces()]);
|
|
1160
|
+
}, 320);
|
|
564
1161
|
},
|
|
565
1162
|
pickSession(sessionId) {
|
|
566
1163
|
this.panel = "";
|
|
@@ -569,63 +1166,206 @@ export default {
|
|
|
569
1166
|
this.previews = {};
|
|
570
1167
|
this.runtime.openSession(sessionId);
|
|
571
1168
|
},
|
|
572
|
-
newChat() {
|
|
1169
|
+
newChat(workspaceId = null) {
|
|
573
1170
|
this.panel = "";
|
|
574
1171
|
this.pendingUser = "";
|
|
575
1172
|
this.previews = {};
|
|
576
|
-
this.
|
|
1173
|
+
this.draftBySession = { ...this.draftBySession, "@pending": "" };
|
|
1174
|
+
this.imagesBySession = { ...this.imagesBySession, "@pending": [] };
|
|
1175
|
+
const scoped = workspaceId || (this.chatDevice === "desktop" ? "" : this.currentWorkspaceId) || null;
|
|
1176
|
+
this.runtime.createSession(scoped);
|
|
1177
|
+
},
|
|
1178
|
+
pickWorkspace(workspaceId) {
|
|
1179
|
+
if (!workspaceId || workspaceId === this.currentWorkspaceId) return;
|
|
1180
|
+
this.newChat(workspaceId);
|
|
577
1181
|
},
|
|
578
|
-
|
|
1182
|
+
refreshWorkspaces() {
|
|
1183
|
+
return this.runtime.refreshWorkspaces();
|
|
1184
|
+
},
|
|
1185
|
+
async openWorkspaceBrowser() {
|
|
1186
|
+
this.directory = {
|
|
1187
|
+
open: true,
|
|
1188
|
+
loading: true,
|
|
1189
|
+
listing: null,
|
|
1190
|
+
error: "",
|
|
1191
|
+
creating: false,
|
|
1192
|
+
creatingFolder: false,
|
|
1193
|
+
showHidden: false,
|
|
1194
|
+
selectPath: "",
|
|
1195
|
+
};
|
|
1196
|
+
await this.browseDirectory();
|
|
1197
|
+
},
|
|
1198
|
+
closeWorkspaceBrowser() {
|
|
1199
|
+
if (this.directory.creating || this.directory.creatingFolder) return;
|
|
1200
|
+
this.directory = { ...this.directory, open: false, error: "", selectPath: "" };
|
|
1201
|
+
},
|
|
1202
|
+
toggleDirectoryHidden(showHidden) {
|
|
1203
|
+
this.directory = { ...this.directory, showHidden: Boolean(showHidden) };
|
|
1204
|
+
},
|
|
1205
|
+
async createDirectoryFolder(name) {
|
|
1206
|
+
const parent = this.directory.listing?.path;
|
|
1207
|
+
if (!parent || !name?.trim() || this.directory.creatingFolder) return;
|
|
1208
|
+
this.directory = { ...this.directory, creatingFolder: true, error: "" };
|
|
1209
|
+
const created = await this.runtime.createDirectory(parent, name.trim());
|
|
1210
|
+
if (!this.directory.open) return;
|
|
1211
|
+
if (!created.ok) {
|
|
1212
|
+
this.directory = {
|
|
1213
|
+
...this.directory,
|
|
1214
|
+
creatingFolder: false,
|
|
1215
|
+
error: created.error?.message || created.error?.code || "directory",
|
|
1216
|
+
};
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const selectPath = created.value?.path || "";
|
|
1220
|
+
await this.browseDirectory(parent);
|
|
1221
|
+
if (!this.directory.open) return;
|
|
1222
|
+
this.directory = {
|
|
1223
|
+
...this.directory,
|
|
1224
|
+
creatingFolder: false,
|
|
1225
|
+
selectPath,
|
|
1226
|
+
error: "",
|
|
1227
|
+
};
|
|
1228
|
+
},
|
|
1229
|
+
async browseDirectory(path) {
|
|
1230
|
+
this.directory = { ...this.directory, loading: true, error: "" };
|
|
1231
|
+
const listed = await this.runtime.listDirectory(path);
|
|
1232
|
+
if (!this.directory.open) return;
|
|
1233
|
+
if (!listed.ok) {
|
|
1234
|
+
this.directory = {
|
|
1235
|
+
...this.directory,
|
|
1236
|
+
loading: false,
|
|
1237
|
+
error: listed.error?.message || listed.error?.code || "directory",
|
|
1238
|
+
};
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
this.directory = {
|
|
1242
|
+
...this.directory,
|
|
1243
|
+
loading: false,
|
|
1244
|
+
listing: listed.value,
|
|
1245
|
+
error: "",
|
|
1246
|
+
};
|
|
1247
|
+
},
|
|
1248
|
+
async chooseWorkspaceDirectory(path) {
|
|
1249
|
+
if (!path || this.directory.creating) return;
|
|
1250
|
+
this.directory = { ...this.directory, creating: true, error: "" };
|
|
1251
|
+
const created = await this.runtime.createWorkspace(path);
|
|
1252
|
+
if (!created.ok) {
|
|
1253
|
+
this.directory = {
|
|
1254
|
+
...this.directory,
|
|
1255
|
+
creating: false,
|
|
1256
|
+
error: created.error?.message || created.error?.code || "workspace",
|
|
1257
|
+
};
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const workspaceId = created.value?.workspace?.workspaceId;
|
|
1261
|
+
this.directory = { ...this.directory, open: false, creating: false };
|
|
1262
|
+
if (workspaceId) this.newChat(workspaceId);
|
|
1263
|
+
},
|
|
1264
|
+
async renameWorkspace(workspaceId, title) {
|
|
1265
|
+
if (!workspaceId || !title?.trim()) return;
|
|
1266
|
+
await this.runtime.renameWorkspace(workspaceId, title.trim());
|
|
1267
|
+
},
|
|
1268
|
+
async deleteWorkspace(workspaceId) {
|
|
1269
|
+
if (!workspaceId) return;
|
|
1270
|
+
await this.runtime.deleteWorkspace(workspaceId);
|
|
1271
|
+
},
|
|
1272
|
+
moveWorkspace(workspaceId, beforeWorkspaceId) {
|
|
1273
|
+
if (!workspaceId) return;
|
|
1274
|
+
return this.runtime.insertWorkspaceBefore(workspaceId, beforeWorkspaceId);
|
|
1275
|
+
},
|
|
1276
|
+
moveSession(workspaceId, sessionId, beforeSessionId) {
|
|
1277
|
+
if (!workspaceId || !sessionId) return;
|
|
1278
|
+
return this.runtime.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
|
|
1279
|
+
},
|
|
1280
|
+
renameSession(sessionId, title) {
|
|
1281
|
+
if (!sessionId || !title?.trim()) return;
|
|
1282
|
+
return this.runtime.renameSession(sessionId, title.trim());
|
|
1283
|
+
},
|
|
1284
|
+
forkSession(sessionId) {
|
|
1285
|
+
if (!sessionId) return;
|
|
1286
|
+
this.pendingUser = "";
|
|
1287
|
+
this.previews = {};
|
|
1288
|
+
return this.runtime.forkSession(sessionId);
|
|
1289
|
+
},
|
|
1290
|
+
archiveSession(sessionId) {
|
|
1291
|
+
if (!sessionId) return;
|
|
1292
|
+
return this.runtime.archiveSession(sessionId);
|
|
1293
|
+
},
|
|
1294
|
+
async send(mode = "queue") {
|
|
579
1295
|
const text = this.draft.trim();
|
|
580
1296
|
if (!this.canSend) return;
|
|
1297
|
+
const sessionId = this.sessionId;
|
|
1298
|
+
const images = this.draftImages.filter((row) => draftImageStatus(row) === "ready");
|
|
1299
|
+
const wasRunning = this.running;
|
|
581
1300
|
this.sending = true;
|
|
582
1301
|
this.draft = "";
|
|
583
|
-
this.
|
|
1302
|
+
this.draftImages = [];
|
|
1303
|
+
this.draftBySession = { ...this.draftBySession, [sessionId]: "" };
|
|
1304
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: [] };
|
|
1305
|
+
this.pendingUser = wasRunning ? "" : text;
|
|
584
1306
|
this.runtime.pinToBottom();
|
|
585
1307
|
this.pinLog();
|
|
586
1308
|
try {
|
|
587
|
-
const
|
|
1309
|
+
const content = [
|
|
1310
|
+
...(text ? [{ type: "text", text }] : []),
|
|
1311
|
+
...(await Promise.all(images.map((row) => imageData(row.file)))),
|
|
1312
|
+
];
|
|
1313
|
+
const sent = await this.runtime.send(content, mode === "steer" ? "steer" : "queue");
|
|
588
1314
|
if (!sent.ok) {
|
|
589
1315
|
this.pendingUser = "";
|
|
590
|
-
this.
|
|
1316
|
+
this.draftBySession = { ...this.draftBySession, [sessionId]: text };
|
|
1317
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: images };
|
|
1318
|
+
if (this.sessionId === sessionId) {
|
|
1319
|
+
this.draft = text;
|
|
1320
|
+
this.draftImages = images;
|
|
1321
|
+
}
|
|
591
1322
|
this.failed = sent.error?.message || sent.error?.code || "send";
|
|
1323
|
+
} else {
|
|
1324
|
+
for (const image of images) URL.revokeObjectURL(image.previewUrl);
|
|
1325
|
+
}
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
this.pendingUser = "";
|
|
1328
|
+
this.draftBySession = { ...this.draftBySession, [sessionId]: text };
|
|
1329
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: images };
|
|
1330
|
+
if (this.sessionId === sessionId) {
|
|
1331
|
+
this.draft = text;
|
|
1332
|
+
this.draftImages = images;
|
|
592
1333
|
}
|
|
1334
|
+
this.failed = err instanceof Error ? err.message : "attachment";
|
|
593
1335
|
} finally {
|
|
594
1336
|
this.sending = false;
|
|
595
1337
|
}
|
|
596
1338
|
},
|
|
1339
|
+
async updateQueue(itemId, action) {
|
|
1340
|
+
if (!itemId || this.queueBusy) return;
|
|
1341
|
+
this.queueBusy = itemId;
|
|
1342
|
+
try {
|
|
1343
|
+
const changed = await this.runtime.updateQueue(itemId, action);
|
|
1344
|
+
if (!changed.ok) {
|
|
1345
|
+
this.failed = changed.error?.message || changed.error?.code || "queue";
|
|
1346
|
+
}
|
|
1347
|
+
} finally {
|
|
1348
|
+
this.queueBusy = "";
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
async steerQueue() {
|
|
1352
|
+
const rows = this.queue.filter((row) => row.placement === "queued");
|
|
1353
|
+
for (const row of rows) {
|
|
1354
|
+
await this.updateQueue(row.id, { kind: "steer" });
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
async stop() {
|
|
1358
|
+
if (!this.running || this.stopping) return;
|
|
1359
|
+
this.stopping = true;
|
|
1360
|
+
try {
|
|
1361
|
+
const stopped = await this.runtime.cancel();
|
|
1362
|
+
if (!stopped.ok) {
|
|
1363
|
+
this.failed = stopped.error?.message || stopped.error?.code || "cancel";
|
|
1364
|
+
}
|
|
1365
|
+
} finally {
|
|
1366
|
+
this.stopping = false;
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
597
1369
|
},
|
|
598
1370
|
};
|
|
599
1371
|
</script>
|
|
600
|
-
|
|
601
|
-
<style scoped>
|
|
602
|
-
.lares-shell__status,
|
|
603
|
-
.lares-shell__hint {
|
|
604
|
-
margin: 0 20px;
|
|
605
|
-
font-size: 13px;
|
|
606
|
-
color: var(--q-ink-3);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
.lares-shell__status {
|
|
610
|
-
color: var(--q-orange-default);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
.lares-shell__retry {
|
|
614
|
-
align-self: flex-start;
|
|
615
|
-
margin: 0 20px;
|
|
616
|
-
border: 0;
|
|
617
|
-
border-radius: 8px;
|
|
618
|
-
padding: 8px 12px;
|
|
619
|
-
background: var(--q-blue-alpha);
|
|
620
|
-
color: var(--q-blue-default);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
.lares-shell__heading {
|
|
624
|
-
margin: 0;
|
|
625
|
-
padding: 12px 16px 4px;
|
|
626
|
-
color: var(--q-ink-3);
|
|
627
|
-
font-size: 12px;
|
|
628
|
-
font-weight: 500;
|
|
629
|
-
line-height: 16px;
|
|
630
|
-
}
|
|
631
|
-
</style>
|