@olares/lares-mobile 0.15.5 → 0.15.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -2
  2. package/package.json +2 -2
  3. package/src/App.vue +918 -178
  4. package/src/chat/ChatBar.vue +2 -2
  5. package/src/chat/Composer.vue +467 -16
  6. package/src/chat/Deliverables.vue +22 -10
  7. package/src/chat/HistoryPanel.vue +747 -55
  8. package/src/chat/MessageActions.vue +88 -34
  9. package/src/chat/QuestionCard.vue +5 -5
  10. package/src/chat/StageRow.vue +8 -5
  11. package/src/chat/Transcript.vue +20 -6
  12. package/src/chat/composer-tokens.js +32 -0
  13. package/src/chat/format.js +37 -0
  14. package/src/desktop/DesktopComposer.vue +942 -0
  15. package/src/desktop/DesktopContextMeter.vue +81 -0
  16. package/src/desktop/DesktopGoal.vue +88 -0
  17. package/src/desktop/DesktopPreview.vue +58 -0
  18. package/src/desktop/DesktopPreviewTabs.vue +82 -0
  19. package/src/desktop/DesktopQuestions.vue +118 -0
  20. package/src/desktop/DesktopQueue.vue +203 -0
  21. package/src/desktop/DesktopShell.vue +229 -0
  22. package/src/desktop/DesktopSidebar.vue +645 -0
  23. package/src/desktop/DesktopTodos.vue +52 -0
  24. package/src/desktop/DirectoryBrowser.vue +231 -0
  25. package/src/desktop/DirectoryNewFolder.vue +124 -0
  26. package/src/desktop/ui/PcDialog.vue +100 -0
  27. package/src/desktop/ui/PcPopover.vue +110 -0
  28. package/src/desktop/ui/PcTooltip.vue +95 -0
  29. package/src/desktop/ui/place.js +24 -0
  30. package/src/desktop/workspace-view.js +121 -0
  31. package/src/host.js +20 -2
  32. package/src/index.d.ts +2 -0
  33. package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
  34. package/src/mobile/MobileDirectorySheet.vue +162 -0
  35. package/src/mobile/MobileNewSession.vue +217 -0
  36. package/src/mobile/MobileShell.vue +367 -0
  37. package/src/motion.css +166 -0
  38. package/src/preview/Preview.vue +91 -150
  39. package/src/preview/PreviewBody.vue +185 -0
  40. package/src/settings/AgentSettings.vue +126 -36
  41. package/src/settings/Select.vue +183 -0
  42. package/src/settings/SettingRow.vue +5 -4
  43. package/src/settings/Sheet.vue +4 -4
  44. package/src/styles.css +9 -0
@@ -0,0 +1,942 @@
1
+ <template>
2
+ <form class="desktop-composer" :data-hero="hero ? 'true' : undefined" @submit.prevent>
3
+ <div class="desktop-composer__model-row">
4
+ <LaresPcPopover
5
+ v-if="hero"
6
+ v-model="workspaceOpen"
7
+ class="desktop-composer__workspace-slot"
8
+ :width="240"
9
+ placement="top-start"
10
+ >
11
+ <template #trigger="{ toggle }">
12
+ <button
13
+ type="button"
14
+ class="desktop-composer__model desktop-composer__workspace"
15
+ :disabled="!state.workspacesReady"
16
+ aria-haspopup="menu"
17
+ :aria-expanded="workspaceOpen"
18
+ @mousedown.prevent
19
+ @click="toggle"
20
+ >
21
+ <svg viewBox="0 0 24 24" aria-hidden="true">
22
+ <path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z" />
23
+ </svg>
24
+ <span>{{ workspaceLabel }}</span>
25
+ <ChevronIcon />
26
+ </button>
27
+ </template>
28
+ <div class="desktop-composer__menu">
29
+ <button
30
+ v-for="workspace in state.workspaces"
31
+ :key="workspace.workspaceId"
32
+ type="button"
33
+ class="desktop-composer__option"
34
+ :data-selected="workspace.workspaceId === state.currentWorkspaceId"
35
+ @click="pickWorkspace(workspace.workspaceId)"
36
+ >
37
+ <span>{{ workspace.title || defaultWorkspaceTitle }}</span>
38
+ <CheckIcon v-if="workspace.workspaceId === state.currentWorkspaceId" />
39
+ </button>
40
+ </div>
41
+ </LaresPcPopover>
42
+ <LaresPcPopover
43
+ v-model="modelOpen"
44
+ class="desktop-composer__model-slot"
45
+ :width="260"
46
+ placement="top-end"
47
+ >
48
+ <template #trigger="{ toggle }">
49
+ <button
50
+ type="button"
51
+ class="desktop-composer__model"
52
+ :disabled="state.modelBusy"
53
+ aria-haspopup="menu"
54
+ :aria-expanded="modelOpen"
55
+ @mousedown.prevent
56
+ @click="toggle"
57
+ >
58
+ <span>{{ state.modelLabel }}</span>
59
+ <ChevronIcon />
60
+ </button>
61
+ </template>
62
+ <div class="desktop-composer__menu">
63
+ <p v-if="state.modelError" class="desktop-composer__menu-status">{{ state.modelError }}</p>
64
+ <p v-else-if="!state.models" class="desktop-composer__menu-status">{{ t("agent.loading") }}</p>
65
+ <template v-for="group in state.modelGroups" :key="group.provider">
66
+ <p class="desktop-composer__menu-label">{{ group.name || group.provider }}</p>
67
+ <button
68
+ v-for="model in group.models"
69
+ :key="modelKey(model)"
70
+ type="button"
71
+ class="desktop-composer__option"
72
+ :data-selected="modelKey(model) === state.modelCurrent"
73
+ @click="chooseModel(model)"
74
+ >
75
+ <span>{{ model.name }}</span>
76
+ <CheckIcon v-if="modelKey(model) === state.modelCurrent" />
77
+ </button>
78
+ </template>
79
+ </div>
80
+ </LaresPcPopover>
81
+ <LaresPcPopover v-if="!state.effortDisabled" v-model="effortOpen" :width="180" placement="top-end">
82
+ <template #trigger="{ toggle }">
83
+ <button
84
+ type="button"
85
+ class="desktop-composer__model"
86
+ :disabled="state.modelBusy"
87
+ aria-haspopup="menu"
88
+ :aria-expanded="effortOpen"
89
+ @mousedown.prevent
90
+ @click="toggle"
91
+ >
92
+ <span>{{ state.effortLabel }}</span>
93
+ <ChevronIcon />
94
+ </button>
95
+ </template>
96
+ <div class="desktop-composer__menu">
97
+ <button
98
+ v-for="row in state.effortRows"
99
+ :key="row.key"
100
+ type="button"
101
+ class="desktop-composer__option"
102
+ :data-selected="row.id === state.effortId"
103
+ @click="chooseEffort(row.id)"
104
+ >
105
+ <span>{{ effortName(row) }}</span>
106
+ <CheckIcon v-if="row.id === state.effortId" />
107
+ </button>
108
+ </div>
109
+ </LaresPcPopover>
110
+ </div>
111
+
112
+ <LaresDesktopTodos :rows="state.todos || []" :t="t" />
113
+ <LaresDesktopGoal
114
+ :projection="state.goal"
115
+ :busy="state.goalBusy"
116
+ :t="t"
117
+ @action="(kind, value) => $emit('goal-action', kind, value)"
118
+ />
119
+ <LaresDesktopQueue
120
+ :items="state.queue"
121
+ :running="state.running"
122
+ :busy="state.queueBusy"
123
+ :t="t"
124
+ @action="(itemId, action) => $emit('queue-action', itemId, action)"
125
+ />
126
+
127
+ <Transition name="lares-panel" mode="out-in">
128
+ <div v-if="state.approval" key="approval" class="desktop-composer__approval">
129
+ <div class="desktop-composer__approval-strip">
130
+ <span aria-hidden="true" />
131
+ {{ t("approval.waiting") }}
132
+ </div>
133
+ <div class="desktop-composer__approval-body">
134
+ {{ state.approval.reason || t("approval.escalation", { toolName: state.approval.toolName }) }}
135
+ </div>
136
+ <div class="desktop-composer__approval-actions">
137
+ <button type="button" :disabled="state.approvalBusy" @click="$emit('answer-approval', 'rejected')">
138
+ {{ t("approval.reject") }}
139
+ </button>
140
+ <button type="button" :disabled="state.approvalBusy" @click="$emit('answer-approval', 'allowed-once')">
141
+ {{ t("approval.allowOnce") }}
142
+ </button>
143
+ </div>
144
+ </div>
145
+ <LaresDesktopQuestions
146
+ v-else-if="state.question?.questions?.length"
147
+ key="questions"
148
+ :questions="state.question.questions"
149
+ :busy="state.questionBusy"
150
+ :t="t"
151
+ @answer="$emit('answer-questions', $event)"
152
+ />
153
+ <div
154
+ v-else
155
+ key="composer"
156
+ class="desktop-composer__card"
157
+ data-composer-card
158
+ @dragover.prevent
159
+ @drop.prevent="onDrop"
160
+ @paste="onPaste"
161
+ >
162
+ <div
163
+ v-if="commandTyped && commandsOpen"
164
+ ref="commandsMenu"
165
+ class="desktop-composer__reference-menu"
166
+ role="listbox"
167
+ >
168
+ <p class="desktop-composer__reference-section">{{ t("desktop.commands") }}</p>
169
+ <p v-if="state.commandsError" class="desktop-composer__reference-status">{{ state.commandsError }}</p>
170
+ <p v-else-if="!state.commands" class="desktop-composer__reference-status">{{ t("agent.loading") }}</p>
171
+ <p v-else-if="visibleCommands.length === 0" class="desktop-composer__reference-status">{{ t("desktop.noCommands") }}</p>
172
+ <div v-else class="desktop-composer__reference-viewport">
173
+ <button
174
+ v-for="(command, index) in visibleCommands"
175
+ :key="command.name"
176
+ type="button"
177
+ class="desktop-composer__reference-item desktop-composer__command-item"
178
+ role="option"
179
+ :aria-selected="index === commandIndex"
180
+ :data-selected="index === commandIndex"
181
+ @mousedown.prevent
182
+ @mouseenter="commandIndex = index"
183
+ @click="pickCommand(command)"
184
+ >
185
+ <span class="desktop-composer__reference-name">/{{ command.name }}</span>
186
+ <span class="desktop-composer__reference-path">{{ command.description || command.hint }}</span>
187
+ </button>
188
+ </div>
189
+ </div>
190
+ <div v-if="referenceOpen" ref="referenceMenu" class="desktop-composer__reference-menu" role="listbox">
191
+ <p v-if="state.referencesError" class="desktop-composer__reference-status">{{ state.referencesError }}</p>
192
+ <p v-else-if="state.referencesLoading" class="desktop-composer__reference-status">{{ t("agent.loading") }}</p>
193
+ <p v-else-if="!state.references.length" class="desktop-composer__reference-status">{{ t("references.empty") }}</p>
194
+ <div v-else class="desktop-composer__reference-viewport">
195
+ <template v-for="(row, index) in referenceRows" :key="row.id">
196
+ <p v-if="row.section" class="desktop-composer__reference-section">{{ row.section }}</p>
197
+ <button
198
+ type="button"
199
+ class="desktop-composer__reference-item"
200
+ role="option"
201
+ :aria-selected="index === referenceIndex"
202
+ :data-selected="index === referenceIndex"
203
+ @mousedown.prevent
204
+ @mouseenter="referenceIndex = index"
205
+ @click="pickReference(row)"
206
+ >
207
+ <span class="desktop-composer__reference-name">{{ row.name }}</span>
208
+ <span class="desktop-composer__reference-path">{{ row.description }}</span>
209
+ </button>
210
+ </template>
211
+ </div>
212
+ </div>
213
+ <div v-if="state.draftImages.length" class="desktop-composer__attachments">
214
+ <figure
215
+ v-for="image in state.draftImages"
216
+ :key="image.id"
217
+ :data-status="imageStatus(image)"
218
+ >
219
+ <button
220
+ type="button"
221
+ class="desktop-composer__attachment-open"
222
+ :aria-label="t('composer.previewAttachment', { name: image.file.name })"
223
+ :disabled="imageStatus(image) !== 'ready'"
224
+ @click="previewImageId = image.id"
225
+ >
226
+ <img :src="image.previewUrl" :alt="image.file.name" />
227
+ </button>
228
+ <div v-if="imageStatus(image) === 'uploading'" class="desktop-composer__attachment-overlay">
229
+ <svg class="desktop-composer__attachment-spin" viewBox="0 0 16 16" aria-hidden="true">
230
+ <circle cx="8" cy="8" r="6" />
231
+ </svg>
232
+ </div>
233
+ <div v-else-if="imageStatus(image) === 'failed'" class="desktop-composer__attachment-overlay" data-kind="error">
234
+ <span aria-hidden="true">!</span>
235
+ </div>
236
+ <button
237
+ type="button"
238
+ class="desktop-composer__attachment-remove"
239
+ :aria-label="t('composer.removeAttachment', { name: image.file.name })"
240
+ @click="$emit('remove-image', image.id)"
241
+ >
242
+ <CloseIcon />
243
+ </button>
244
+ </figure>
245
+ </div>
246
+ <textarea
247
+ ref="input"
248
+ :value="state.draft"
249
+ class="desktop-composer__input"
250
+ rows="1"
251
+ :placeholder="placeholder"
252
+ :disabled="state.modelBlocked"
253
+ :readonly="state.sending"
254
+ @input="onInput"
255
+ @keydown="onKeydown"
256
+ @wheel="onWheel"
257
+ />
258
+ <div class="desktop-composer__footer">
259
+ <input ref="picker" type="file" multiple class="desktop-composer__picker" @change="onFiles" />
260
+ <LaresPcPopover v-model="commandsButtonOpen" :width="380" placement="top-start">
261
+ <template #trigger="{ toggle }">
262
+ <LaresPcTooltip :label="t('desktop.commands')" placement="top">
263
+ <button
264
+ type="button"
265
+ class="desktop-composer__circle"
266
+ :disabled="attachDisabled"
267
+ aria-haspopup="menu"
268
+ :aria-expanded="commandsButtonOpen"
269
+ :aria-label="t('desktop.commands')"
270
+ @mousedown.prevent
271
+ @click="openCommands(toggle)"
272
+ >
273
+ <PlusIcon />
274
+ </button>
275
+ </LaresPcTooltip>
276
+ </template>
277
+ <div class="desktop-composer__menu">
278
+ <p class="desktop-composer__menu-label">{{ t("desktop.commands") }}</p>
279
+ <p v-if="state.commandsError" class="desktop-composer__menu-status">{{ state.commandsError }}</p>
280
+ <p v-else-if="!state.commands" class="desktop-composer__menu-status">{{ t("agent.loading") }}</p>
281
+ <p v-else-if="visibleCommands.length === 0" class="desktop-composer__menu-status">{{ t("desktop.noCommands") }}</p>
282
+ <button
283
+ v-for="(command, index) in visibleCommands"
284
+ :key="command.name"
285
+ type="button"
286
+ class="desktop-composer__command"
287
+ :data-selected="index === commandIndex"
288
+ @mouseenter="commandIndex = index"
289
+ @click="pickCommand(command)"
290
+ >
291
+ <span class="desktop-composer__command-name">/{{ command.name }}</span>
292
+ <span class="desktop-composer__command-hint">{{ command.description || command.hint }}</span>
293
+ </button>
294
+ </div>
295
+ </LaresPcPopover>
296
+ <LaresPcPopover
297
+ v-if="state.permissionRows.length"
298
+ v-model="permissionOpen"
299
+ class="desktop-composer__permission-slot"
300
+ :width="240"
301
+ placement="top-start"
302
+ >
303
+ <template #trigger="{ toggle }">
304
+ <LaresPcTooltip :label="state.permissionHint" placement="top">
305
+ <button
306
+ type="button"
307
+ class="desktop-composer__permission"
308
+ :disabled="state.permissionBusy"
309
+ :data-open="permissionOpen"
310
+ aria-haspopup="menu"
311
+ :aria-expanded="permissionOpen"
312
+ @mousedown.prevent
313
+ @click="toggle"
314
+ >
315
+ <PermissionIcon :preset="state.permissionCurrent" />
316
+ <span>{{ state.permissionLabel }}</span>
317
+ <ChevronIcon class="desktop-composer__caret" />
318
+ </button>
319
+ </LaresPcTooltip>
320
+ </template>
321
+ <div class="desktop-composer__menu">
322
+ <LaresPcTooltip
323
+ v-for="row in state.permissionRows"
324
+ :key="row.value"
325
+ :label="row.hint"
326
+ placement="right"
327
+ >
328
+ <button
329
+ type="button"
330
+ class="desktop-composer__option"
331
+ :data-selected="row.value === state.permissionCurrent"
332
+ @click="choosePermission(row.value)"
333
+ >
334
+ <span class="desktop-composer__option-main">
335
+ <PermissionIcon :preset="row.value" />
336
+ <span>{{ row.label }}</span>
337
+ </span>
338
+ <CheckIcon v-if="row.value === state.permissionCurrent" />
339
+ </button>
340
+ </LaresPcTooltip>
341
+ </div>
342
+ </LaresPcPopover>
343
+ <LaresPcTooltip v-if="state.planActive" :label="t('plan.disable')" placement="top">
344
+ <button
345
+ type="button"
346
+ class="desktop-composer__plan"
347
+ :disabled="state.sending"
348
+ :aria-label="t('plan.disable')"
349
+ @mousedown.prevent
350
+ @click="$emit('disable-plan')"
351
+ >
352
+ <span>{{ t("plan.active") }}</span>
353
+ <CloseIcon />
354
+ </button>
355
+ </LaresPcTooltip>
356
+ <LaresPcTooltip :label="t('composer.attach')" placement="top">
357
+ <button
358
+ type="button"
359
+ class="desktop-composer__circle"
360
+ :disabled="attachDisabled"
361
+ :aria-label="t('composer.attach')"
362
+ @mousedown.prevent
363
+ @click="$refs.picker?.click()"
364
+ >
365
+ <PaperclipIcon />
366
+ </button>
367
+ </LaresPcTooltip>
368
+ <LaresDesktopContextMeter
369
+ class="desktop-composer__meter"
370
+ :pressure="state.contextPressure"
371
+ :breakdown="state.contextBreakdown"
372
+ :t="t"
373
+ />
374
+ <LaresPcTooltip
375
+ class="desktop-composer__send-slot"
376
+ :label="state.running ? t('chat.stop') : t('chat.send')"
377
+ placement="top"
378
+ >
379
+ <button
380
+ type="button"
381
+ class="desktop-composer__send"
382
+ :disabled="state.running ? state.stopping : !state.canSend || state.sending"
383
+ :aria-label="state.running ? t('chat.stop') : t('chat.send')"
384
+ @mousedown.prevent
385
+ @click="primary"
386
+ >
387
+ <StopIcon v-if="state.running" />
388
+ <ArrowIcon v-else />
389
+ </button>
390
+ </LaresPcTooltip>
391
+ </div>
392
+ </div>
393
+ </Transition>
394
+ <p v-for="item in state.upload.failures" :key="item.id" class="desktop-composer__error">
395
+ {{ t("upload.failed", { name: item.name, reason: t(`error.${item.code}`) }) }}
396
+ </p>
397
+ <LaresPcDialog
398
+ :open="permissionConfirm"
399
+ :title="t('access.confirm.title')"
400
+ :description="t('access.confirm.description')"
401
+ :close-label="t('access.confirm.cancel')"
402
+ :width="460"
403
+ @close="closePermissionConfirm"
404
+ >
405
+ <label class="desktop-composer__risk-check">
406
+ <input v-model="permissionAcknowledged" type="checkbox" />
407
+ <span>{{ t("access.confirm.acknowledge") }}</span>
408
+ </label>
409
+ <template #footer>
410
+ <button type="button" class="desktop-composer__dialog-cancel" @click="closePermissionConfirm">
411
+ {{ t("access.confirm.cancel") }}
412
+ </button>
413
+ <button
414
+ type="button"
415
+ class="desktop-composer__dialog-confirm"
416
+ :disabled="!permissionAcknowledged || state.permissionBusy"
417
+ @click="confirmPermission"
418
+ >
419
+ {{ t("access.confirm.enable") }}
420
+ </button>
421
+ </template>
422
+ </LaresPcDialog>
423
+ <LaresPcDialog
424
+ :open="Boolean(previewImage)"
425
+ :title="previewImage?.file.name ?? ''"
426
+ :close-label="t('desktop.close')"
427
+ :width="720"
428
+ @close="previewImageId = ''"
429
+ >
430
+ <img
431
+ v-if="previewImage"
432
+ class="desktop-composer__preview"
433
+ :src="previewImage.previewUrl"
434
+ :alt="previewImage.file.name"
435
+ />
436
+ </LaresPcDialog>
437
+ </form>
438
+ </template>
439
+
440
+ <script>
441
+ import { h } from "vue";
442
+ import { resolveSubmitMode } from "@olares/lares-core/larepass/submission-settings";
443
+ import { DEFAULT_WORKSPACE_TITLE } from "@olares/lares-core/workspace/constants";
444
+ import { activeReferenceToken, insertReference } from "@olares/lares-core/larepass/references";
445
+ import { draftImageStatus } from "@olares/lares-core/files/draft-images";
446
+ import LaresDesktopQueue from "./DesktopQueue.vue";
447
+ import LaresDesktopQuestions from "./DesktopQuestions.vue";
448
+ import LaresDesktopContextMeter from "./DesktopContextMeter.vue";
449
+ import LaresDesktopGoal from "./DesktopGoal.vue";
450
+ import LaresDesktopTodos from "./DesktopTodos.vue";
451
+ import LaresPcDialog from "./ui/PcDialog.vue";
452
+ import LaresPcPopover from "./ui/PcPopover.vue";
453
+ import LaresPcTooltip from "./ui/PcTooltip.vue";
454
+
455
+ const icon = (name, ...paths) => ({
456
+ name,
457
+ render() {
458
+ return h("svg", { viewBox: "0 0 24 24", "aria-hidden": "true" }, paths.map((d) => h("path", { d })));
459
+ },
460
+ });
461
+
462
+ const ChevronIcon = icon("ChevronIcon", "m8 10 4 4 4-4");
463
+ const CheckIcon = icon("CheckIcon", "m5 12.5 4 4 10-10");
464
+ const PlusIcon = icon("PlusIcon", "M12 5v14M5 12h14");
465
+ const PaperclipIcon = icon("PaperclipIcon", "m9.5 12.5 5.8-5.8a3 3 0 0 1 4.2 4.2l-8.1 8.1a5 5 0 0 1-7.1-7.1l8-8");
466
+ const ArrowIcon = icon("ArrowIcon", "M12 19V7M6.5 12.5 12 7l5.5 5.5");
467
+ const StopIcon = {
468
+ name: "StopIcon",
469
+ render() {
470
+ return h("svg", { viewBox: "0 0 24 24", "aria-hidden": "true" }, [
471
+ h("rect", { x: 7, y: 7, width: 10, height: 10, rx: 2.5, fill: "currentColor", stroke: "none" }),
472
+ ]);
473
+ },
474
+ };
475
+ const CloseIcon = icon("CloseIcon", "m7 7 10 10M17 7 7 17");
476
+
477
+ const SHIELD = "M12 3.5 19 6v5.4c0 4.2-2.8 7.4-7 9.1-4.2-1.7-7-4.9-7-9.1V6l7-2.5Z";
478
+ /** One shield, three glyphs: what this preset lets a tool do. */
479
+ const PRESET_GLYPHS = {
480
+ "read-only": "m9.2 12 1.8 1.8 3.8-4",
481
+ "workspace-write": "m9.6 13.9 4.3-4.3 1.4 1.4-4.3 4.3H9.6z",
482
+ "danger-full-access": "M12 8.4v3.8M12 14.8v.1",
483
+ };
484
+
485
+ const PermissionIcon = {
486
+ name: "PermissionIcon",
487
+ props: { preset: { type: String, default: "" } },
488
+ render() {
489
+ const glyph = PRESET_GLYPHS[this.preset] ?? PRESET_GLYPHS["workspace-write"];
490
+ return h("svg", { viewBox: "0 0 24 24", "aria-hidden": "true" }, [
491
+ h("path", { d: SHIELD }),
492
+ h("path", { d: glyph }),
493
+ ]);
494
+ },
495
+ };
496
+
497
+ export default {
498
+ name: "LaresDesktopComposer",
499
+ components: { LaresDesktopContextMeter, LaresDesktopGoal, LaresDesktopQuestions, LaresDesktopQueue, LaresDesktopTodos, LaresPcDialog, LaresPcPopover, LaresPcTooltip, ChevronIcon, CheckIcon, PlusIcon, PaperclipIcon, ArrowIcon, StopIcon, CloseIcon, PermissionIcon },
500
+ props: {
501
+ state: { type: Object, required: true },
502
+ t: { type: Function, required: true },
503
+ hero: { type: Boolean, default: false },
504
+ modelKey: { type: Function, required: true },
505
+ effortName: { type: Function, required: true },
506
+ },
507
+ emits: [
508
+ "update-draft", "send", "stop", "steer-queue", "files", "choose-model", "choose-effort", "answer-approval", "answer-questions",
509
+ "choose-permission", "disable-plan", "goal-action", "open-commands", "pick-command", "queue-action", "remove-image", "pick-workspace",
510
+ ],
511
+ data() {
512
+ return {
513
+ modelOpen: false,
514
+ workspaceOpen: false,
515
+ effortOpen: false,
516
+ permissionOpen: false,
517
+ permissionConfirm: false,
518
+ permissionAcknowledged: false,
519
+ commandsOpen: false,
520
+ commandsButtonOpen: false,
521
+ commandTyped: false,
522
+ commandIndex: 0,
523
+ dropping: false,
524
+ dropDepth: 0,
525
+ referenceOpen: false,
526
+ referenceToken: null,
527
+ referenceIndex: 0,
528
+ referenceTimer: 0,
529
+ previewImageId: "",
530
+ };
531
+ },
532
+ computed: {
533
+ defaultWorkspaceTitle() {
534
+ return DEFAULT_WORKSPACE_TITLE;
535
+ },
536
+ workspaceLabel() {
537
+ if (!this.state.workspacesReady) return this.t("agent.loading");
538
+ const current = this.state.workspaces.find(
539
+ (row) => row.workspaceId === this.state.currentWorkspaceId,
540
+ );
541
+ return current?.title || this.state.workspaces[0]?.title || DEFAULT_WORKSPACE_TITLE;
542
+ },
543
+ /** Held by id, so removing or sending the draft closes the dialog. */
544
+ previewImage() {
545
+ return this.state.draftImages.find((row) => row.id === this.previewImageId) ?? null;
546
+ },
547
+ /** Row chrome follows dsh's `@` menu: "<kind> · <name>" plus a section break. */
548
+ referenceRows() {
549
+ let previous = "";
550
+ return this.state.references.map((row) => {
551
+ const group = row.kind === "session" ? "sessions" : "files";
552
+ const section = group === previous ? "" : this.t(`references.section.${group}`);
553
+ previous = group;
554
+ return { ...row, section, name: `${this.t(`references.${row.kind}`)} · ${row.label}` };
555
+ });
556
+ },
557
+ attachDisabled() {
558
+ return !this.state.sessionId || this.state.historyLoading || this.state.starting;
559
+ },
560
+ visibleCommands() {
561
+ const rows = this.state.commands || [];
562
+ if (!this.commandTyped) return rows;
563
+ const needle = this.state.draft.match(/^\/([^\s]*)$/)?.[1]?.toLowerCase() ?? "";
564
+ return rows.filter((row) => row.name.toLowerCase().includes(needle));
565
+ },
566
+ composerOverlayPointerActive() {
567
+ return this.referenceOpen || (this.commandsOpen && this.commandTyped);
568
+ },
569
+ placeholder() {
570
+ if (this.state.modelBlocked) return this.t("chat.placeholderNoModel");
571
+ if (
572
+ this.state.running
573
+ && !this.state.draft.trim()
574
+ && !this.state.draftImages.length
575
+ && this.state.queue.some((row) => row.placement === "queued")
576
+ ) {
577
+ const shortcut = /Mac|iPhone|iPad/.test(globalThis.navigator?.platform || "")
578
+ ? "⌘↵"
579
+ : "Ctrl+Enter";
580
+ return this.t("chat.placeholderSteerQueue", { shortcut });
581
+ }
582
+ if (this.state.planActive) return this.t("chat.placeholderPlan");
583
+ if (this.hero) return this.t("chat.placeholderNewSession");
584
+ return this.t("chat.placeholderDesktop");
585
+ },
586
+ },
587
+ watch: {
588
+ "state.draft"() {
589
+ this.autosize();
590
+ },
591
+ composerOverlayPointerActive(active) {
592
+ if (active) window.addEventListener("mousedown", this.onComposerOverlayPointer, true);
593
+ else window.removeEventListener("mousedown", this.onComposerOverlayPointer, true);
594
+ },
595
+ },
596
+ mounted() {
597
+ this.autosize();
598
+ },
599
+ beforeUnmount() {
600
+ window.clearTimeout(this.referenceTimer);
601
+ window.removeEventListener("mousedown", this.onComposerOverlayPointer, true);
602
+ },
603
+ methods: {
604
+ pickWorkspace(workspaceId) {
605
+ this.workspaceOpen = false;
606
+ if (!workspaceId || workspaceId === this.state.currentWorkspaceId) return;
607
+ this.$emit("pick-workspace", workspaceId);
608
+ },
609
+ chooseModel(model) {
610
+ this.modelOpen = false;
611
+ this.$emit("choose-model", model);
612
+ },
613
+ chooseEffort(id) {
614
+ this.effortOpen = false;
615
+ this.$emit("choose-effort", id);
616
+ },
617
+ choosePermission(value) {
618
+ this.permissionOpen = false;
619
+ if (value === this.state.permissionCurrent) return;
620
+ if (value === "danger-full-access") {
621
+ this.permissionAcknowledged = false;
622
+ this.permissionConfirm = true;
623
+ return;
624
+ }
625
+ this.$emit("choose-permission", value);
626
+ },
627
+ closePermissionConfirm() {
628
+ this.permissionConfirm = false;
629
+ this.permissionAcknowledged = false;
630
+ },
631
+ confirmPermission() {
632
+ if (!this.permissionAcknowledged || this.state.permissionBusy) return;
633
+ this.closePermissionConfirm();
634
+ this.$emit("choose-permission", "danger-full-access");
635
+ },
636
+ /** The registry is read when the menu opens, not on every keystroke. */
637
+ openCommands(toggle) {
638
+ const opening = !this.commandsButtonOpen;
639
+ this.commandTyped = false;
640
+ this.commandsOpen = false;
641
+ this.commandIndex = 0;
642
+ toggle();
643
+ if (opening) this.$emit("open-commands");
644
+ },
645
+ pickCommand(command) {
646
+ this.commandsOpen = false;
647
+ this.commandsButtonOpen = false;
648
+ this.commandTyped = false;
649
+ this.$emit("pick-command", command);
650
+ },
651
+ onInput(event) {
652
+ const value = event.target.value;
653
+ this.$emit("update-draft", value);
654
+ this.trackReference(value, event.target.selectionStart ?? value.length);
655
+ const typed = /^\/[^\s]*$/.test(value);
656
+ if (typed) {
657
+ if (!this.commandsOpen) this.$emit("open-commands");
658
+ this.commandsButtonOpen = false;
659
+ this.commandTyped = true;
660
+ this.commandIndex = 0;
661
+ this.commandsOpen = true;
662
+ } else if (this.commandTyped) {
663
+ this.commandTyped = false;
664
+ this.commandsOpen = false;
665
+ }
666
+ },
667
+ closeReferences() {
668
+ window.clearTimeout(this.referenceTimer);
669
+ this.referenceOpen = false;
670
+ this.referenceToken = null;
671
+ },
672
+ /**
673
+ * A press outside dismisses the panel; one inside the textarea only moves
674
+ * the caret, which decides on its own whether a mention is still under it.
675
+ */
676
+ onComposerOverlayPointer(event) {
677
+ if (this.$refs.referenceMenu?.contains(event.target)) return;
678
+ if (this.$refs.commandsMenu?.contains(event.target)) return;
679
+ const input = this.$refs.input;
680
+ if (event.target !== input) {
681
+ this.closeReferences();
682
+ if (this.commandTyped) {
683
+ this.commandsOpen = false;
684
+ this.commandTyped = false;
685
+ }
686
+ return;
687
+ }
688
+ window.setTimeout(() => {
689
+ if (this.referenceOpen) this.trackReference(this.state.draft, input.selectionStart ?? 0);
690
+ }, 0);
691
+ },
692
+ trackReference(value, caret) {
693
+ const token = activeReferenceToken(value, caret);
694
+ window.clearTimeout(this.referenceTimer);
695
+ if (!token) {
696
+ this.closeReferences();
697
+ return;
698
+ }
699
+ this.commandsOpen = false;
700
+ this.commandsButtonOpen = false;
701
+ this.commandTyped = false;
702
+ this.referenceOpen = true;
703
+ this.referenceToken = token;
704
+ this.referenceIndex = 0;
705
+ this.referenceTimer = window.setTimeout(() => {
706
+ this.$emit("open-references", { query: token.query, quoted: token.quoted });
707
+ }, 100);
708
+ },
709
+ pickReference(row) {
710
+ if (!this.referenceToken) return;
711
+ const inserted = insertReference(this.state.draft, this.referenceToken, row);
712
+ this.$emit("update-draft", inserted.draft);
713
+ this.$nextTick(() => {
714
+ const input = this.$refs.input;
715
+ input?.focus();
716
+ input?.setSelectionRange(inserted.caret, inserted.caret);
717
+ if (row.continuation) this.trackReference(inserted.draft, inserted.caret);
718
+ else this.closeReferences();
719
+ });
720
+ },
721
+ focus() {
722
+ this.$refs.input?.focus();
723
+ },
724
+ imageStatus(image) {
725
+ return draftImageStatus(image);
726
+ },
727
+ onFiles(event) {
728
+ const files = Array.from(event.currentTarget.files ?? []);
729
+ event.currentTarget.value = "";
730
+ if (files.length) this.$emit("files", files);
731
+ },
732
+ onPaste(event) {
733
+ const files = Array.from(event.clipboardData?.files ?? []);
734
+ if (!files.length) return;
735
+ event.preventDefault();
736
+ this.$emit("files", files);
737
+ },
738
+ onDrop(event) {
739
+ const files = Array.from(event.dataTransfer?.files ?? []);
740
+ if (files.length) this.$emit("files", files);
741
+ },
742
+ onKeydown(event) {
743
+ if (this.referenceOpen) {
744
+ if (event.key === "Escape") {
745
+ event.preventDefault();
746
+ this.closeReferences();
747
+ return;
748
+ }
749
+ if ((event.key === "ArrowDown" || event.key === "ArrowUp") && this.state.references.length) {
750
+ event.preventDefault();
751
+ const step = event.key === "ArrowDown" ? 1 : -1;
752
+ this.referenceIndex = (
753
+ this.referenceIndex + step + this.state.references.length
754
+ ) % this.state.references.length;
755
+ return;
756
+ }
757
+ if (event.key === "Enter" && !event.shiftKey && !event.isComposing && this.state.references.length) {
758
+ event.preventDefault();
759
+ this.pickReference(this.state.references[this.referenceIndex] ?? this.state.references[0]);
760
+ return;
761
+ }
762
+ }
763
+ if (this.commandsOpen || this.commandsButtonOpen) {
764
+ if (event.key === "Escape") {
765
+ event.preventDefault();
766
+ this.commandsOpen = false;
767
+ this.commandsButtonOpen = false;
768
+ this.commandTyped = false;
769
+ return;
770
+ }
771
+ if ((event.key === "ArrowDown" || event.key === "ArrowUp") && this.visibleCommands.length) {
772
+ event.preventDefault();
773
+ const step = event.key === "ArrowDown" ? 1 : -1;
774
+ this.commandIndex = (this.commandIndex + step + this.visibleCommands.length) % this.visibleCommands.length;
775
+ return;
776
+ }
777
+ if (event.key === "Enter" && !event.shiftKey && !event.isComposing && this.visibleCommands.length) {
778
+ event.preventDefault();
779
+ this.pickCommand(this.visibleCommands[this.commandIndex] ?? this.visibleCommands[0]);
780
+ return;
781
+ }
782
+ }
783
+ if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
784
+ event.preventDefault();
785
+ const accelerated = event.metaKey || event.ctrlKey;
786
+ if (accelerated && this.state.running && !this.state.canSend) {
787
+ if (this.state.queue.some((row) => row.placement === "queued")) this.$emit("steer-queue");
788
+ return;
789
+ }
790
+ if (this.state.canSend) {
791
+ this.$emit("send", resolveSubmitMode(
792
+ this.state.running,
793
+ accelerated,
794
+ true,
795
+ this.state.busyEnter,
796
+ ));
797
+ }
798
+ },
799
+ onWheel(event) {
800
+ const input = event.currentTarget;
801
+ const atTop = input.scrollTop <= 0 && event.deltaY < 0;
802
+ const atBottom = input.scrollTop + input.clientHeight >= input.scrollHeight - 1 && event.deltaY > 0;
803
+ if (!atTop && !atBottom) return;
804
+ const log = document.querySelector(".lares-log");
805
+ if (!log) return;
806
+ log.scrollTop += event.deltaY;
807
+ event.preventDefault();
808
+ },
809
+ primary() {
810
+ this.$emit(this.state.running ? "stop" : "send");
811
+ },
812
+ autosize() {
813
+ this.$nextTick(() => {
814
+ const input = this.$refs.input;
815
+ if (!input) return;
816
+ input.style.height = "auto";
817
+ input.style.height = `${Math.min(input.scrollHeight, 200)}px`;
818
+ });
819
+ },
820
+ },
821
+ };
822
+ </script>
823
+
824
+ <style scoped>
825
+ .desktop-composer {
826
+ position: relative;
827
+ z-index: 2;
828
+ width: calc(var(--lares-column, min(760px, 100%)) - 2 * var(--lares-gutter, 24px));
829
+ margin: 0 auto;
830
+ padding: 8px 0 20px;
831
+ }
832
+ .desktop-composer__model-row { display:flex; justify-content:flex-end; gap:2px; min-height:28px; margin-bottom:6px; }
833
+ .desktop-composer[data-hero="true"] .desktop-composer__model-row { justify-content:space-between; }
834
+ .desktop-composer__model,
835
+ .desktop-composer__circle,
836
+ .desktop-composer__send {
837
+ border:0; color:var(--q-ink-2); cursor:pointer;
838
+ }
839
+ .desktop-composer__model { display:flex; align-items:center; gap:4px; border-radius:999px; padding:4px 10px; background:transparent; font-size:14px; }
840
+ /* The model name is the long label of the pair, so it is the one that gives way;
841
+ without a floor of its own the workspace chip absorbs every pixel of shrink. */
842
+ .desktop-composer__workspace-slot { flex:0 0 auto; max-width:45%; }
843
+ .desktop-composer__model-slot { min-width:0; }
844
+ .desktop-composer__workspace,
845
+ .desktop-composer__model-slot .desktop-composer__model { min-width:0; max-width:100%; }
846
+ .desktop-composer__workspace span,
847
+ .desktop-composer__model-slot .desktop-composer__model span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
848
+ .desktop-composer__model svg,
849
+ .desktop-composer__permission svg,
850
+ .desktop-composer__circle svg,
851
+ .desktop-composer__send svg,
852
+ .desktop-composer__option svg { width:16px; height:16px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }
853
+ .desktop-composer__card { position:relative; border:1px solid var(--q-input-stroke); border-radius:14px; padding:10px 12px 9px; background:var(--q-background-2, var(--q-background-1)); box-shadow:0 1px 3px rgb(0 0 0 / 8%); }
854
+ .desktop-composer__card[data-dropping="true"] { border-color:var(--q-blue-default); background:color-mix(in srgb, var(--q-blue-default) 8%, var(--q-background-2, var(--q-background-1))); }
855
+ /* Metrics mirror dsh's `@` menu (MenuView.module.css); only the palette is the
856
+ LarePass shell's, since dsh's --dsw-* tokens do not exist here. */
857
+ .desktop-composer__reference-menu { position:absolute; z-index:20; bottom:calc(100% + 4px); left:0; display:flex; min-width:min(260px, 100%); max-width:min(537px, 100%); max-height:320px; flex-direction:column; overflow:hidden; border:1px solid var(--q-separator); border-radius:12px; padding:4px; background:var(--q-background-1); box-shadow:0 12px 30px rgb(0 0 0 / 18%); }
858
+ .desktop-composer__reference-viewport { display:flex; min-height:0; flex-direction:column; overflow-y:auto; }
859
+ .desktop-composer__reference-status { display:flex; min-height:40px; align-items:center; margin:0; padding:8px 10px; color:var(--q-ink-3); font-size:14px; line-height:22px; }
860
+ .desktop-composer__reference-section { min-height:26px; flex:none; margin:0; padding:6px 10px 2px; color:var(--q-ink-3); font-size:12px; font-weight:500; line-height:18px; }
861
+ .desktop-composer__reference-section:not(:first-child) { margin-top:4px; }
862
+ .desktop-composer__reference-item { display:flex; width:100%; min-height:40px; align-items:center; gap:8px; border:0; border-radius:10px; padding:8px 10px; background:transparent; color:var(--q-ink-1); font:inherit; font-size:14px; line-height:22px; text-align:left; cursor:pointer; }
863
+ .desktop-composer__reference-item:hover,
864
+ .desktop-composer__reference-item[data-selected="true"] { background:var(--q-background-hover); }
865
+ .desktop-composer__reference-name { max-width:40%; flex:none; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
866
+ .desktop-composer__reference-path { min-width:0; flex:1; overflow:hidden; color:var(--q-ink-3); text-overflow:ellipsis; white-space:nowrap; }
867
+ .desktop-composer__approval { overflow:hidden; border:1px solid var(--q-separator); border-radius:14px; background:var(--q-background-2); box-shadow:0 1px 3px rgb(0 0 0 / 8%); }
868
+ .desktop-composer__approval-strip { display:flex; height:30px; align-items:center; gap:7px; padding:0 12px; background:var(--q-background-3); color:var(--q-ink-3); font-size:12px; }
869
+ .desktop-composer__approval-strip span { width:6px; height:6px; border-radius:50%; background:var(--q-orange-default); }
870
+ .desktop-composer__approval-body { max-height:96px; overflow:auto; padding:14px 16px; color:var(--q-ink-1); font-size:14px; line-height:20px; }
871
+ .desktop-composer__approval-actions { display:flex; justify-content:flex-end; gap:8px; padding:8px 12px 12px; }
872
+ .desktop-composer__approval-actions button { height:32px; border:1px solid var(--q-separator); border-radius:7px; padding:0 13px; background:transparent; color:var(--q-ink-2); font:inherit; cursor:pointer; }
873
+ .desktop-composer__approval-actions button:last-child { border-color:transparent; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
874
+ .desktop-composer__approval-actions button:disabled { opacity:.45; cursor:default; }
875
+ .desktop-composer__attachments { display:flex; gap:8px; overflow-x:auto; padding:0 0 8px 2px; }
876
+ .desktop-composer__attachments figure { position:relative; width:64px; height:64px; flex:0 0 64px; overflow:hidden; margin:0; border:1px solid var(--q-separator); border-radius:9px; background:var(--q-background-3); }
877
+ .desktop-composer__attachments img { width:100%; height:100%; object-fit:cover; transition:opacity .22s ease; }
878
+ .desktop-composer__attachments figure[data-status="failed"] { border-color:var(--q-orange-default); }
879
+ .desktop-composer__attachment-overlay { position:absolute; inset:0; display:grid; place-items:center; background:rgb(0 0 0 / 42%); color:white; pointer-events:none; }
880
+ .desktop-composer__attachment-overlay[data-kind="error"] { font-size:16px; font-weight:700; }
881
+ .desktop-composer__attachment-spin { width:18px; height:18px; animation:desktop-attachment-spin .8s linear infinite; }
882
+ .desktop-composer__attachment-spin circle { fill:none; stroke:currentColor; stroke-dasharray:24 12; }
883
+ @keyframes desktop-attachment-spin { to { transform:rotate(360deg); } }
884
+ @media (prefers-reduced-motion: reduce) { .desktop-composer__attachment-spin { animation:none; } }
885
+ .desktop-composer__attachment-open { display:block; width:100%; height:100%; border:0; padding:0; background:transparent; cursor:zoom-in; }
886
+ .desktop-composer__attachment-remove { position:absolute; top:3px; right:3px; display:grid; width:20px; height:20px; place-items:center; border:0; border-radius:50%; padding:0; background:rgb(0 0 0 / 58%); color:white; cursor:pointer; }
887
+ .desktop-composer__attachment-remove svg { width:12px; height:12px; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; }
888
+ .desktop-composer__preview { display:block; max-width:100%; max-height:calc(100vh - 220px); margin:0 auto; border-radius:8px; }
889
+ .desktop-composer__input { box-sizing:border-box; width:100%; min-height:37px; max-height:200px; resize:none; overflow-y:auto; border:0; padding:1px 2px 8px; outline:0; background:transparent; color:var(--q-ink-1); font:inherit; font-size:16px; line-height:1.5; }
890
+ .desktop-composer__footer { display:flex; align-items:center; gap:8px; min-height:30px; }
891
+ /* The flex items here are the tooltip/popover wrappers, not the buttons. They
892
+ carry min-width:0, so without this the fixed 28px controls get squeezed
893
+ narrower than their own 16px glyph and the icons spill onto their neighbours.
894
+ Only the permission label gives way, by ellipsis. */
895
+ .desktop-composer__footer > * { flex-shrink:0; }
896
+ .desktop-composer__permission-slot { min-width:0; flex-shrink:1; }
897
+ .desktop-composer__permission { min-width:0; }
898
+ .desktop-composer__permission svg { flex-shrink:0; }
899
+ .desktop-composer__permission span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
900
+ .desktop-composer__picker { display:none; }
901
+ .desktop-composer__circle { display:grid; width:28px; height:28px; place-items:center; border-radius:50%; background:var(--q-background-3); }
902
+ .desktop-composer__permission { display:flex; align-items:center; gap:5px; height:28px; border:0; border-radius:999px; padding:0 8px; background:var(--q-background-3); color:var(--q-ink-2); font:inherit; font-size:14px; cursor:pointer; }
903
+ .desktop-composer__plan { display:flex; height:28px; align-items:center; gap:4px; border:0; border-radius:999px; padding:0 8px; background:var(--q-background-3); color:var(--q-orange-default); font:inherit; font-size:13px; cursor:pointer; }
904
+ .desktop-composer__plan svg { width:14px; height:14px; fill:none; stroke:currentColor; stroke-width:1.8; stroke-linecap:round; }
905
+ .desktop-composer__permission .desktop-composer__caret { transition:transform .15s ease; }
906
+ .desktop-composer__permission[data-open="true"] .desktop-composer__caret { transform:rotate(180deg); }
907
+ .desktop-composer__model:hover,
908
+ .desktop-composer__circle:hover,
909
+ .desktop-composer__permission:hover,
910
+ .desktop-composer__plan:hover { background:var(--q-background-hover); }
911
+ /* The meter rides with the send button; it renders nothing without an occupancy,
912
+ in which case the send button keeps the auto margin itself. */
913
+ .desktop-composer__meter { margin-left:auto; }
914
+ .desktop-composer__send-slot { margin-left:auto; }
915
+ .desktop-composer__meter + .desktop-composer__send-slot { margin-left:0; }
916
+ .desktop-composer__send { display:grid; width:28px; height:28px; place-items:center; border-radius:50%; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
917
+ .desktop-composer button:disabled { opacity:.45; cursor:default; }
918
+ .desktop-composer__menu { margin:-2px; }
919
+ .desktop-composer__menu-label,
920
+ .desktop-composer__menu-status { margin:0; padding:7px 8px 4px; color:var(--q-ink-3); font-size:13px; }
921
+ .desktop-composer__option { display:flex; align-items:center; justify-content:space-between; width:100%; border:0; border-radius:6px; padding:7px 8px; background:transparent; color:var(--q-ink-2); text-align:left; cursor:pointer; }
922
+ /* Menu rows are wrapped in a tooltip; the wrapper must fill the panel. Footer
923
+ tooltips are flex items and must keep their own width. */
924
+ .desktop-composer__menu :deep(.lares-pc-tooltip) { width:100%; }
925
+ .desktop-composer__option:hover { background:var(--q-background-hover); }
926
+ .desktop-composer__option[data-selected="true"] { color:var(--q-blue-default); }
927
+ .desktop-composer__option-main { display:flex; min-width:0; align-items:center; gap:8px; }
928
+ .desktop-composer__option-main span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
929
+ .desktop-composer__command { display:flex; align-items:baseline; gap:10px; width:100%; border:0; border-radius:6px; padding:7px 8px; background:transparent; color:var(--q-ink-2); font:inherit; text-align:left; cursor:pointer; }
930
+ .desktop-composer__command:hover { background:var(--q-background-hover); }
931
+ .desktop-composer__command[data-selected="true"] { background:var(--q-background-hover); }
932
+ .desktop-composer__command-name { flex-shrink:0; color:var(--q-ink-1); font-size:15px; }
933
+ .desktop-composer__command-hint { min-width:0; overflow:hidden; color:var(--q-ink-3); font-size:14px; text-overflow:ellipsis; white-space:nowrap; }
934
+ .desktop-composer__error { margin:5px 4px 0; color:var(--q-orange-default); font-size:14px; }
935
+ .desktop-composer__risk-check { display:flex; align-items:flex-start; gap:10px; color:var(--q-ink-2); font-size:14px; line-height:20px; cursor:pointer; }
936
+ .desktop-composer__risk-check input { width:16px; height:16px; margin:2px 0 0; accent-color:var(--q-blue-default); }
937
+ .desktop-composer__dialog-cancel,
938
+ .desktop-composer__dialog-confirm { min-width:76px; height:32px; border-radius:7px; padding:0 12px; font:inherit; cursor:pointer; }
939
+ .desktop-composer__dialog-cancel { border:1px solid var(--q-separator); background:transparent; color:var(--q-ink-2); }
940
+ .desktop-composer__dialog-confirm { border:0; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
941
+ .desktop-composer__dialog-confirm:disabled { opacity:.45; cursor:default; }
942
+ </style>