@olares/lares-mobile 0.15.4 → 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 +924 -184
- 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,138 +1,156 @@
|
|
|
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>
|
|
111
104
|
|
|
112
105
|
<script>
|
|
106
|
+
import { pickHostPorts } from "@olares/lares-core/larepass/host";
|
|
113
107
|
import { connectChat } from "./runtime.js";
|
|
114
108
|
import { createT } from "./i18n.js";
|
|
115
109
|
import { failText, withPendingUser } from "./chat/format.js";
|
|
116
110
|
import { createVoiceCapture } from "./chat/voice.js";
|
|
117
111
|
import { appendDraftMentions } from "@olares/lares-core/files/mention";
|
|
118
|
-
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";
|
|
119
119
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@olares/lares-core/files/limits";
|
|
120
120
|
import { groupModelsByProvider, effortMenuRows, reasoningOfModel, currentEffortId, selectionKey } from "@olares/lares-core/router/session-model";
|
|
121
121
|
import { messageFromCode } from "@olares/lares-core/i18n/t";
|
|
122
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";
|
|
123
130
|
import { chatDevice as resolveChatDevice } from "./layout.js";
|
|
124
|
-
import
|
|
125
|
-
import
|
|
126
|
-
import LaresTranscript from "./chat/Transcript.vue";
|
|
127
|
-
import LaresComposer from "./chat/Composer.vue";
|
|
128
|
-
import LaresPreview from "./preview/Preview.vue";
|
|
129
|
-
import LaresSheet from "./settings/Sheet.vue";
|
|
130
|
-
import LaresSettingRow from "./settings/SettingRow.vue";
|
|
131
|
+
import LaresMobileShell from "./mobile/MobileShell.vue";
|
|
132
|
+
import LaresDesktopShell from "./desktop/DesktopShell.vue";
|
|
131
133
|
import "./styles.css";
|
|
132
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
|
+
|
|
133
151
|
export default {
|
|
134
152
|
name: "LaresApp",
|
|
135
|
-
components: {
|
|
153
|
+
components: { LaresMobileShell, LaresDesktopShell },
|
|
136
154
|
props: {
|
|
137
155
|
locale: { type: String, default: "en" },
|
|
138
156
|
device: { type: String, default: "mobile" },
|
|
@@ -140,22 +158,33 @@ export default {
|
|
|
140
158
|
proxyPrefix: { type: String, default: undefined },
|
|
141
159
|
request: { type: Function, default: undefined },
|
|
142
160
|
env: { type: Object, default: undefined },
|
|
161
|
+
// Vue drops an undeclared prop into attrs, so a port missing from this list
|
|
162
|
+
// never reaches `ports` below no matter what the host passes.
|
|
163
|
+
socketProtocol: { type: [Function, String, Array], default: undefined },
|
|
143
164
|
},
|
|
144
165
|
data() {
|
|
145
166
|
return {
|
|
146
167
|
sending: false,
|
|
168
|
+
stopping: false,
|
|
147
169
|
starting: false,
|
|
148
170
|
hostGen: 0,
|
|
171
|
+
modelGen: 0,
|
|
149
172
|
running: false,
|
|
150
173
|
sessionId: "",
|
|
151
174
|
draft: "",
|
|
175
|
+
draftBySession: {},
|
|
176
|
+
imagesBySession: {},
|
|
152
177
|
failed: "",
|
|
153
178
|
error: "",
|
|
154
179
|
items: [],
|
|
155
180
|
pendingUser: "",
|
|
156
181
|
unsub: null,
|
|
157
182
|
previews: {},
|
|
158
|
-
|
|
183
|
+
previewTabs: [],
|
|
184
|
+
previewLru: [],
|
|
185
|
+
previewPath: "",
|
|
186
|
+
previewMode: "chat",
|
|
187
|
+
previewContents: {},
|
|
159
188
|
panel: "",
|
|
160
189
|
sessions: [],
|
|
161
190
|
sessionsReady: false,
|
|
@@ -167,6 +196,7 @@ export default {
|
|
|
167
196
|
effortSheet: false,
|
|
168
197
|
effortByModel: {},
|
|
169
198
|
upload: { pending: 0, failures: [] },
|
|
199
|
+
draftImages: [],
|
|
170
200
|
voicePhase: "idle",
|
|
171
201
|
voiceElapsed: 0,
|
|
172
202
|
voiceError: "",
|
|
@@ -174,16 +204,49 @@ export default {
|
|
|
174
204
|
intakeUnsub: null,
|
|
175
205
|
question: null,
|
|
176
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
|
+
},
|
|
177
243
|
};
|
|
178
244
|
},
|
|
179
245
|
computed: {
|
|
180
246
|
ports() {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
request: this.request,
|
|
185
|
-
env: this.env,
|
|
186
|
-
};
|
|
247
|
+
// Built from the canonical key list rather than spelled out, so adding a
|
|
248
|
+
// port does not need this object edited in lockstep.
|
|
249
|
+
return pickHostPorts(this);
|
|
187
250
|
},
|
|
188
251
|
runtime() {
|
|
189
252
|
return connectChat(this.ports);
|
|
@@ -197,8 +260,116 @@ export default {
|
|
|
197
260
|
chatDevice() {
|
|
198
261
|
return resolveChatDevice(this.device);
|
|
199
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
|
+
},
|
|
200
366
|
canSend() {
|
|
201
|
-
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);
|
|
202
373
|
},
|
|
203
374
|
sessionTitle() {
|
|
204
375
|
const row = this.sessions.find((item) => item.sessionId === this.sessionId);
|
|
@@ -219,6 +390,9 @@ export default {
|
|
|
219
390
|
return reasoningOfModel(this.currentListedModel);
|
|
220
391
|
},
|
|
221
392
|
effortId() {
|
|
393
|
+
if (this.chatDevice === "desktop") {
|
|
394
|
+
return currentEffortId(this.models?.default, this.reasoning);
|
|
395
|
+
}
|
|
222
396
|
if (!(this.modelCurrent in this.effortByModel)) {
|
|
223
397
|
return currentEffortId(this.models?.default, this.reasoning);
|
|
224
398
|
}
|
|
@@ -240,6 +414,11 @@ export default {
|
|
|
240
414
|
failText() {
|
|
241
415
|
return failText(this.t, this.failed, this.error);
|
|
242
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
|
+
},
|
|
243
422
|
previewMediaSrc() {
|
|
244
423
|
const data = this.preview.data;
|
|
245
424
|
if (!data) return "";
|
|
@@ -271,6 +450,9 @@ export default {
|
|
|
271
450
|
modelBusy() {
|
|
272
451
|
return Boolean(this.modelPending) || (this.modelSheet && !this.models);
|
|
273
452
|
},
|
|
453
|
+
modelBlocked() {
|
|
454
|
+
return this.chatDevice === "desktop" && this.models?.routable === false;
|
|
455
|
+
},
|
|
274
456
|
voiceErrorText() {
|
|
275
457
|
return this.voiceError ? messageFromCode(this.t, this.voiceError, "error.voice_failed") : "";
|
|
276
458
|
},
|
|
@@ -290,7 +472,10 @@ export default {
|
|
|
290
472
|
},
|
|
291
473
|
},
|
|
292
474
|
created() {
|
|
475
|
+
// Per-path request tokens: reactive state would re-render on every read.
|
|
476
|
+
this.previewGen = new Map();
|
|
293
477
|
this.intake = new FileIntake((file, sessionId, options) => this.runtime.upload(file, options, sessionId));
|
|
478
|
+
this.imageUploadControllers = new Map();
|
|
294
479
|
this.voiceCap = createVoiceCapture({
|
|
295
480
|
transcribe: (blob, signal) => this.runtime.transcribe(blob, this.voiceLanguage, signal),
|
|
296
481
|
getDraft: () => this.draft,
|
|
@@ -311,8 +496,19 @@ export default {
|
|
|
311
496
|
} catch {
|
|
312
497
|
this.effortByModel = {};
|
|
313
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();
|
|
314
507
|
},
|
|
315
508
|
mounted() {
|
|
509
|
+
this.conversationUnsub = subscribeConversationSettings((section) => {
|
|
510
|
+
this.busyEnter = section.busyEnter;
|
|
511
|
+
});
|
|
316
512
|
this.retry();
|
|
317
513
|
},
|
|
318
514
|
activated() {
|
|
@@ -323,9 +519,14 @@ export default {
|
|
|
323
519
|
},
|
|
324
520
|
beforeUnmount() {
|
|
325
521
|
this.captureLog();
|
|
522
|
+
this.noteSeen(this.sessionId);
|
|
523
|
+
for (const controller of this.imageUploadControllers?.values?.() ?? []) controller.abort();
|
|
524
|
+
this.imageUploadControllers?.clear?.();
|
|
326
525
|
this.voiceCap?.dispose();
|
|
526
|
+
this.conversationUnsub?.();
|
|
327
527
|
this.intakeUnsub?.();
|
|
328
528
|
if (this.sessionId) this.intake?.cancelSession(this.sessionId);
|
|
529
|
+
this.releaseDraftImages();
|
|
329
530
|
this.unsub?.();
|
|
330
531
|
this.unsub = null;
|
|
331
532
|
},
|
|
@@ -343,18 +544,36 @@ export default {
|
|
|
343
544
|
return this.runtime.sticking();
|
|
344
545
|
},
|
|
345
546
|
restoreLog() {
|
|
346
|
-
this.$refs.
|
|
547
|
+
this.$refs.shell?.restoreLog?.();
|
|
347
548
|
},
|
|
348
549
|
captureLog() {
|
|
349
|
-
this.$refs.
|
|
550
|
+
this.$refs.shell?.captureLog?.();
|
|
350
551
|
},
|
|
351
552
|
pinLog() {
|
|
352
|
-
this.$refs.
|
|
553
|
+
this.$refs.shell?.pinLog?.();
|
|
353
554
|
},
|
|
354
555
|
applySnap(snap) {
|
|
355
|
-
|
|
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);
|
|
356
572
|
this.pendingUser = "";
|
|
357
573
|
this.previews = {};
|
|
574
|
+
this.resetPreview();
|
|
575
|
+
this.commands = null;
|
|
576
|
+
this.commandsError = "";
|
|
358
577
|
}
|
|
359
578
|
this.sessionId = snap.sessionId;
|
|
360
579
|
this.items = snap.items;
|
|
@@ -363,14 +582,111 @@ export default {
|
|
|
363
582
|
this.error = snap.error;
|
|
364
583
|
this.sessions = snap.sessions ?? this.sessions;
|
|
365
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;
|
|
366
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 ?? [];
|
|
367
597
|
this.question = snap.question || null;
|
|
368
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
|
+
}
|
|
369
604
|
if (this.pendingUser && snap.items.some((row) => row.type === "user" && row.text === this.pendingUser)) {
|
|
370
605
|
this.pendingUser = "";
|
|
371
606
|
}
|
|
372
607
|
this.pinLog();
|
|
373
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
|
+
},
|
|
374
690
|
async hydrateFiles(paths) {
|
|
375
691
|
const missing = [...new Set(paths)].filter((path) => !(path in this.previews));
|
|
376
692
|
if (!missing.length) return;
|
|
@@ -399,30 +715,122 @@ export default {
|
|
|
399
715
|
this.questionBusy = false;
|
|
400
716
|
}
|
|
401
717
|
},
|
|
402
|
-
async
|
|
403
|
-
|
|
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
|
+
}
|
|
404
808
|
try {
|
|
405
809
|
const data = await this.runtime.preview(path);
|
|
810
|
+
if (this.previewGen.get(path) !== gen) return;
|
|
406
811
|
this.previews = { ...this.previews, [path]: data };
|
|
407
|
-
|
|
812
|
+
const canonical = this.adoptPreviewPath(path, data?.path);
|
|
813
|
+
this.previewGen.set(canonical, gen);
|
|
814
|
+
this.setPreviewContent(canonical, gen, { status: "ready", data, error: "" });
|
|
408
815
|
} catch (err) {
|
|
409
|
-
this.
|
|
410
|
-
path,
|
|
816
|
+
this.setPreviewContent(path, gen, {
|
|
411
817
|
status: "error",
|
|
412
818
|
data: null,
|
|
413
819
|
error: err instanceof Error ? err.message : "file_preview_failed",
|
|
414
|
-
};
|
|
820
|
+
});
|
|
415
821
|
}
|
|
416
822
|
},
|
|
417
|
-
|
|
418
|
-
this.
|
|
823
|
+
setPreviewContent(path, gen, content) {
|
|
824
|
+
if (this.previewGen.get(path) !== gen) return;
|
|
825
|
+
this.previewContents = { ...this.previewContents, [path]: content };
|
|
419
826
|
},
|
|
420
827
|
async retry() {
|
|
421
828
|
const gen = ++this.hostGen;
|
|
422
829
|
this.starting = true;
|
|
423
830
|
try {
|
|
424
|
-
await this.runtime.start();
|
|
831
|
+
await this.runtime.start({ workspaces: this.chatDevice === "desktop" });
|
|
425
832
|
if (gen !== this.hostGen) return;
|
|
833
|
+
if (this.chatDevice !== "desktop") await this.runtime.refreshWorkspaces();
|
|
426
834
|
await this.loadComposerMeta();
|
|
427
835
|
} finally {
|
|
428
836
|
if (gen === this.hostGen) this.starting = false;
|
|
@@ -437,6 +845,9 @@ export default {
|
|
|
437
845
|
},
|
|
438
846
|
switchHost() {
|
|
439
847
|
this.voiceCap?.stop?.(true);
|
|
848
|
+
for (const controller of this.imageUploadControllers.values()) controller.abort();
|
|
849
|
+
this.imageUploadControllers.clear();
|
|
850
|
+
this.releaseDraftImages();
|
|
440
851
|
this.intakeUnsub?.();
|
|
441
852
|
if (this.sessionId) this.intake?.cancelSession(this.sessionId);
|
|
442
853
|
this.sessionId = "";
|
|
@@ -444,7 +855,7 @@ export default {
|
|
|
444
855
|
this.sessions = [];
|
|
445
856
|
this.sessionsReady = false;
|
|
446
857
|
this.previews = {};
|
|
447
|
-
this.
|
|
858
|
+
this.resetPreview();
|
|
448
859
|
this.pendingUser = "";
|
|
449
860
|
this.failed = "";
|
|
450
861
|
this.error = "";
|
|
@@ -455,6 +866,34 @@ export default {
|
|
|
455
866
|
this.panel = "";
|
|
456
867
|
this.question = null;
|
|
457
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
|
+
};
|
|
458
897
|
this.bindIntake("");
|
|
459
898
|
this.bindRuntime();
|
|
460
899
|
this.retry();
|
|
@@ -474,11 +913,22 @@ export default {
|
|
|
474
913
|
async loadComposerMeta() {
|
|
475
914
|
const settings = this.runtime.settings;
|
|
476
915
|
if (!settings) return;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
}
|
|
482
932
|
}
|
|
483
933
|
try {
|
|
484
934
|
const voice = await settings.voice();
|
|
@@ -487,6 +937,34 @@ export default {
|
|
|
487
937
|
this.voiceLanguage = "";
|
|
488
938
|
}
|
|
489
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
|
+
},
|
|
490
968
|
openEffortSheet() {
|
|
491
969
|
if (this.effortDisabled) return;
|
|
492
970
|
this.effortSheet = true;
|
|
@@ -503,9 +981,29 @@ export default {
|
|
|
503
981
|
// storage may be blocked
|
|
504
982
|
}
|
|
505
983
|
},
|
|
506
|
-
chooseEffort(id) {
|
|
984
|
+
async chooseEffort(id) {
|
|
507
985
|
this.effortSheet = false;
|
|
508
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
|
+
}
|
|
509
1007
|
this.effortByModel = { ...this.effortByModel, [this.modelCurrent]: id ?? "" };
|
|
510
1008
|
this.persistEffort();
|
|
511
1009
|
},
|
|
@@ -523,26 +1021,121 @@ export default {
|
|
|
523
1021
|
this.modelPending = key;
|
|
524
1022
|
this.modelError = "";
|
|
525
1023
|
try {
|
|
526
|
-
this.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
}
|
|
530
1042
|
} catch (err) {
|
|
531
1043
|
this.modelError = err instanceof Error ? err.message : String(err);
|
|
532
1044
|
} finally {
|
|
533
1045
|
this.modelPending = "";
|
|
534
1046
|
}
|
|
535
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
|
+
},
|
|
536
1093
|
attachFiles(files) {
|
|
537
1094
|
const sessionId = this.sessionId;
|
|
538
1095
|
if (!sessionId || !files?.length) return;
|
|
539
|
-
const
|
|
540
|
-
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);
|
|
541
1104
|
for (const file of oversized) this.intake.reportFailure(sessionId, file, "file_too_large");
|
|
542
1105
|
void this.intake.uploadFiles(sessionId, accepted, (paths) => {
|
|
543
|
-
|
|
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;
|
|
544
1112
|
});
|
|
545
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
|
+
},
|
|
546
1139
|
onHoldStart() {
|
|
547
1140
|
if (this.voicePhase === "idle" || this.voicePhase === "error") this.voiceCap?.start();
|
|
548
1141
|
},
|
|
@@ -559,7 +1152,11 @@ export default {
|
|
|
559
1152
|
return;
|
|
560
1153
|
}
|
|
561
1154
|
this.panel = "history";
|
|
562
|
-
|
|
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);
|
|
563
1160
|
},
|
|
564
1161
|
pickSession(sessionId) {
|
|
565
1162
|
this.panel = "";
|
|
@@ -568,63 +1165,206 @@ export default {
|
|
|
568
1165
|
this.previews = {};
|
|
569
1166
|
this.runtime.openSession(sessionId);
|
|
570
1167
|
},
|
|
571
|
-
newChat() {
|
|
1168
|
+
newChat(workspaceId = null) {
|
|
572
1169
|
this.panel = "";
|
|
573
1170
|
this.pendingUser = "";
|
|
574
1171
|
this.previews = {};
|
|
575
|
-
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);
|
|
576
1180
|
},
|
|
577
|
-
|
|
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") {
|
|
578
1294
|
const text = this.draft.trim();
|
|
579
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;
|
|
580
1299
|
this.sending = true;
|
|
581
1300
|
this.draft = "";
|
|
582
|
-
this.
|
|
1301
|
+
this.draftImages = [];
|
|
1302
|
+
this.draftBySession = { ...this.draftBySession, [sessionId]: "" };
|
|
1303
|
+
this.imagesBySession = { ...this.imagesBySession, [sessionId]: [] };
|
|
1304
|
+
this.pendingUser = wasRunning ? "" : text;
|
|
583
1305
|
this.runtime.pinToBottom();
|
|
584
1306
|
this.pinLog();
|
|
585
1307
|
try {
|
|
586
|
-
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");
|
|
587
1313
|
if (!sent.ok) {
|
|
588
1314
|
this.pendingUser = "";
|
|
589
|
-
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
|
+
}
|
|
590
1321
|
this.failed = sent.error?.message || sent.error?.code || "send";
|
|
1322
|
+
} else {
|
|
1323
|
+
for (const image of images) URL.revokeObjectURL(image.previewUrl);
|
|
591
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;
|
|
1332
|
+
}
|
|
1333
|
+
this.failed = err instanceof Error ? err.message : "attachment";
|
|
592
1334
|
} finally {
|
|
593
1335
|
this.sending = false;
|
|
594
1336
|
}
|
|
595
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
|
+
},
|
|
596
1368
|
},
|
|
597
1369
|
};
|
|
598
1370
|
</script>
|
|
599
|
-
|
|
600
|
-
<style scoped>
|
|
601
|
-
.lares-shell__status,
|
|
602
|
-
.lares-shell__hint {
|
|
603
|
-
margin: 0 20px;
|
|
604
|
-
font-size: 13px;
|
|
605
|
-
color: var(--q-ink-3);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
.lares-shell__status {
|
|
609
|
-
color: var(--q-orange-default);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
.lares-shell__retry {
|
|
613
|
-
align-self: flex-start;
|
|
614
|
-
margin: 0 20px;
|
|
615
|
-
border: 0;
|
|
616
|
-
border-radius: 8px;
|
|
617
|
-
padding: 8px 12px;
|
|
618
|
-
background: var(--q-blue-alpha);
|
|
619
|
-
color: var(--q-blue-default);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
.lares-shell__heading {
|
|
623
|
-
margin: 0;
|
|
624
|
-
padding: 12px 16px 4px;
|
|
625
|
-
color: var(--q-ink-3);
|
|
626
|
-
font-size: 12px;
|
|
627
|
-
font-weight: 500;
|
|
628
|
-
line-height: 16px;
|
|
629
|
-
}
|
|
630
|
-
</style>
|