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