@olares/lares-mobile 0.15.5 → 0.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -2
  2. package/package.json +2 -2
  3. package/src/App.vue +917 -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 +871 -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 +203 -0
  22. package/src/desktop/DesktopSidebar.vue +645 -0
  23. package/src/desktop/DesktopTodos.vue +52 -0
  24. package/src/desktop/DirectoryBrowser.vue +170 -0
  25. package/src/desktop/DirectoryNewFolder.vue +124 -0
  26. package/src/desktop/ui/PcDialog.vue +85 -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,645 @@
1
+ <template>
2
+ <aside class="desktop-sidebar">
3
+ <button
4
+ type="button"
5
+ class="desktop-sidebar__new"
6
+ :disabled="state.starting"
7
+ @click="$emit('create', defaultWorkspaceId)"
8
+ >
9
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4.4a7.6 7.6 0 0 1 5.4 13L19.2 20l-3-.7A7.6 7.6 0 1 1 12 4.4Z"/><path d="M12 9v6M9 12h6"/></svg>
10
+ <span>{{ t("desktop.newSession") }}</span>
11
+ </button>
12
+
13
+ <header class="desktop-sidebar__section-head">
14
+ <span v-if="!searchOpen">{{ flat ? t("desktop.sessions") : t("desktop.workspaces") }}</span>
15
+ <div v-else class="desktop-sidebar__search">
16
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></svg>
17
+ <input ref="search" v-model="query" :placeholder="t('desktop.searchSessions')" @keydown.esc="closeSearch" />
18
+ <button type="button" @click="closeSearch" :aria-label="t('desktop.clearSearch')">×</button>
19
+ </div>
20
+ <div v-if="!searchOpen" class="desktop-sidebar__actions">
21
+ <LaresPcTooltip :label="t('desktop.searchSessions')">
22
+ <button type="button" :aria-label="t('desktop.searchSessions')" @click="openSearch">
23
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></svg>
24
+ </button>
25
+ </LaresPcTooltip>
26
+ <LaresPcPopover v-model="optionsOpen" :width="170">
27
+ <template #trigger="{ toggle }">
28
+ <LaresPcTooltip :label="t('desktop.viewOptions')">
29
+ <button type="button" :aria-label="t('desktop.viewOptions')" @click="toggle">
30
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9h9M17 9h3M4 15h3M11 15h9"/><circle cx="15" cy="9" r="2"/><circle cx="9" cy="15" r="2"/></svg>
31
+ </button>
32
+ </LaresPcTooltip>
33
+ </template>
34
+ <div class="desktop-sidebar__options">
35
+ <p>{{ t("desktop.groupBy") }}</p>
36
+ <button type="button" :data-selected="!flat" @click="flat = false; optionsOpen = false">{{ t("desktop.byWorkspace") }}</button>
37
+ <button type="button" :data-selected="flat" @click="flat = true; optionsOpen = false">{{ t("desktop.oneList") }}</button>
38
+ <p>{{ t("desktop.orderBy") }}</p>
39
+ <button type="button" :data-selected="orderBy === 'updated'" @click="orderBy = 'updated'; optionsOpen = false">{{ t("desktop.recent") }}</button>
40
+ <button type="button" :data-selected="orderBy === 'manual'" @click="orderBy = 'manual'; optionsOpen = false">{{ t("desktop.manual") }}</button>
41
+ </div>
42
+ </LaresPcPopover>
43
+ <LaresPcTooltip :label="t('desktop.addWorkspace')">
44
+ <button type="button" :aria-label="t('desktop.addWorkspace')" @click="$emit('browse-workspaces')">
45
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5.5H7A2.5 2.5 0 0 0 4.5 8v9A2.5 2.5 0 0 0 7 19.5h8a2.5 2.5 0 0 0 2.5-2.5v-3.5"/><path d="M19 4.5v4M17 6.5h4"/></svg>
46
+ </button>
47
+ </LaresPcTooltip>
48
+ </div>
49
+ </header>
50
+
51
+ <div class="desktop-sidebar__list">
52
+ <div v-if="!state.sessionsReady || !state.workspacesReady" class="desktop-sidebar__loading">
53
+ <span v-for="i in 7" :key="i" />
54
+ </div>
55
+ <p v-else-if="state.workspaceError" class="desktop-sidebar__error">
56
+ {{ state.workspaceError }}
57
+ <button type="button" @click.stop="$emit('refresh-workspaces')">{{ t("agent.refresh") }}</button>
58
+ </p>
59
+ <p v-else-if="groups.length === 0" class="desktop-sidebar__empty">{{ t("history.empty") }}</p>
60
+ <section
61
+ v-for="group in groups"
62
+ :key="group.key"
63
+ class="desktop-sidebar__group"
64
+ >
65
+ <div
66
+ v-if="!flat"
67
+ class="desktop-sidebar__group-row"
68
+ :data-open="actionMenu === `workspace:${group.workspaceId}`"
69
+ :data-drop="dropEdge('workspace', group.workspaceId)"
70
+ :draggable="Boolean(group.workspaceId) && orderBy === 'manual'"
71
+ @dragstart="startWorkspaceDrag(group, $event)"
72
+ @dragend="endDrag"
73
+ @dragover.prevent="overWorkspace(group, $event)"
74
+ @dragleave="leaveRow('workspace', group.workspaceId, $event)"
75
+ @drop.prevent="dropWorkspace()"
76
+ >
77
+ <button type="button" class="desktop-sidebar__group-toggle" @click="toggleGroup(group.key)">
78
+ <span
79
+ class="desktop-sidebar__group-icon"
80
+ :data-expanded="isExpanded(group.key)"
81
+ :data-active="holdsCurrent(group)"
82
+ >
83
+ <svg class="desktop-sidebar__folder" viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 7.5h6l1.6 2H20.5v9H3.5z"/></svg>
84
+ <svg class="desktop-sidebar__caret" viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 6 15.5 12l-7 6z"/></svg>
85
+ </span>
86
+ <span>{{ group.title || t("desktop.ungrouped") }}</span>
87
+ </button>
88
+ <div v-if="group.workspaceId" class="desktop-sidebar__row-actions">
89
+ <LaresPcPopover
90
+ :model-value="actionMenu === `workspace:${group.workspaceId}`"
91
+ :width="176"
92
+ @update:model-value="setActionMenu(`workspace:${group.workspaceId}`, $event)"
93
+ >
94
+ <template #trigger="{ toggle }">
95
+ <LaresPcTooltip :label="t('desktop.workspaceActions')">
96
+ <button
97
+ type="button"
98
+ class="desktop-sidebar__row-action"
99
+ :aria-label="t('desktop.workspaceActions')"
100
+ @click="toggle"
101
+ >
102
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.35"/><circle cx="12" cy="12" r="1.35"/><circle cx="18" cy="12" r="1.35"/></svg>
103
+ </button>
104
+ </LaresPcTooltip>
105
+ </template>
106
+ <div class="desktop-sidebar__action-menu">
107
+ <button type="button" @click="openDialog('rename-workspace', group)">
108
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19h3.2L18.6 8.6a2.05 2.05 0 0 0-2.9-2.9L5 15.8z"/></svg>
109
+ <span>{{ t("desktop.rename") }}</span>
110
+ </button>
111
+ <button type="button" data-danger="true" @click="openDialog('delete-workspace', group)">
112
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5.5 7.5h13M10 7.5V6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v1.5M7 7.5l.8 11a1.5 1.5 0 0 0 1.5 1.4h5.4a1.5 1.5 0 0 0 1.5-1.4l.8-11"/></svg>
113
+ <span>{{ t("desktop.deleteWorkspace") }}</span>
114
+ </button>
115
+ </div>
116
+ </LaresPcPopover>
117
+ <LaresPcTooltip :label="t('desktop.newSession')">
118
+ <button
119
+ type="button"
120
+ class="desktop-sidebar__row-action"
121
+ :aria-label="t('desktop.newSession')"
122
+ @click="$emit('create', group.workspaceId)"
123
+ >
124
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
125
+ </button>
126
+ </LaresPcTooltip>
127
+ </div>
128
+ </div>
129
+ <div v-show="flat || isExpanded(group.key)" class="desktop-sidebar__sessions">
130
+ <div
131
+ v-for="session in visibleSessions(group)"
132
+ :key="session.sessionId"
133
+ class="desktop-sidebar__session-row"
134
+ :data-current="session.sessionId === state.sessionId"
135
+ :data-open="actionMenu === `session:${session.sessionId}`"
136
+ :data-unseen="Boolean(state.unseen?.[session.sessionId])"
137
+ :data-drop="dropEdge('session', session.sessionId)"
138
+ draggable="true"
139
+ @dragstart="startSessionDrag(group, session, $event)"
140
+ @dragend="endDrag"
141
+ @dragover.prevent.stop="overSession(group, session, $event)"
142
+ @dragleave.stop="leaveRow('session', session.sessionId, $event)"
143
+ @drop.prevent.stop="dropSession(group)"
144
+ >
145
+ <button
146
+ type="button"
147
+ class="desktop-sidebar__session"
148
+ @click="$emit('pick-session', session.sessionId)"
149
+ @contextmenu.prevent="openDialog('rename-session', session)"
150
+ >
151
+ <span class="desktop-sidebar__unseen" aria-hidden="true" />
152
+ <span>{{ session.title || t("history.untitled") }}</span>
153
+ <time>{{ relativeTime(session.updatedAt) }}</time>
154
+ </button>
155
+ <LaresPcPopover
156
+ :model-value="actionMenu === `session:${session.sessionId}`"
157
+ :width="150"
158
+ @update:model-value="setActionMenu(`session:${session.sessionId}`, $event)"
159
+ >
160
+ <template #trigger="{ toggle }">
161
+ <LaresPcTooltip :label="t('desktop.sessionActions')">
162
+ <button
163
+ type="button"
164
+ class="desktop-sidebar__session-action"
165
+ :aria-label="t('desktop.sessionActions')"
166
+ @click="toggle"
167
+ >
168
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="12" r="1.35"/><circle cx="12" cy="12" r="1.35"/><circle cx="18" cy="12" r="1.35"/></svg>
169
+ </button>
170
+ </LaresPcTooltip>
171
+ </template>
172
+ <div class="desktop-sidebar__action-menu">
173
+ <button type="button" @click="openDialog('rename-session', session)">
174
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 19h3.2L18.6 8.6a2.05 2.05 0 0 0-2.9-2.9L5 15.8z"/></svg>
175
+ <span>{{ t("desktop.rename") }}</span>
176
+ </button>
177
+ <button type="button" @click="forkSession(session)">
178
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.5v5.5a3.5 3.5 0 0 0 3.5 3.5h6"/><path d="m14.5 11.5 3 3-3 3"/></svg>
179
+ <span>{{ t("desktop.forkSession") }}</span>
180
+ </button>
181
+ <button type="button" @click="openDialog('archive-session', session)">
182
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.8 9.5v8a1.5 1.5 0 0 0 1.5 1.5h11.4a1.5 1.5 0 0 0 1.5-1.5v-8"/><path d="M3.8 5.5h16.4v4H3.8z"/><path d="M10 13h4"/></svg>
183
+ <span>{{ t("desktop.archiveSession") }}</span>
184
+ </button>
185
+ </div>
186
+ </LaresPcPopover>
187
+ </div>
188
+ <button
189
+ v-if="!flat && overflowCount(group) > 0"
190
+ type="button"
191
+ class="desktop-sidebar__session-overflow"
192
+ :aria-expanded="isSessionsExpanded(group.key)"
193
+ @click="toggleSessions(group.key)"
194
+ >
195
+ {{ isSessionsExpanded(group.key)
196
+ ? t("desktop.showLessSessions")
197
+ : t("desktop.showMoreSessions", { n: overflowCount(group) }) }}
198
+ </button>
199
+ </div>
200
+ </section>
201
+ </div>
202
+ <LaresPcDialog
203
+ :open="Boolean(dialog)"
204
+ :title="dialogTitle"
205
+ :description="dialogDescription"
206
+ :close-label="t('desktop.close')"
207
+ :width="420"
208
+ @close="closeDialog"
209
+ >
210
+ <input
211
+ v-if="isRenameDialog"
212
+ ref="dialogInput"
213
+ v-model="dialogValue"
214
+ class="desktop-sidebar__dialog-input"
215
+ @keydown.enter.prevent="submitDialog"
216
+ />
217
+ <template #footer>
218
+ <button type="button" class="desktop-sidebar__dialog-cancel" @click="closeDialog">{{ t("desktop.cancel") }}</button>
219
+ <button
220
+ type="button"
221
+ class="desktop-sidebar__dialog-confirm"
222
+ :data-danger="dialog?.kind === 'delete-workspace' || dialog?.kind === 'archive-session'"
223
+ :disabled="isRenameDialog && !dialogValue.trim()"
224
+ @click="submitDialog"
225
+ >
226
+ {{ dialogConfirmLabel }}
227
+ </button>
228
+ </template>
229
+ </LaresPcDialog>
230
+ </aside>
231
+ </template>
232
+
233
+ <script>
234
+ import {
235
+ collapsedSessions,
236
+ FLAT_SESSION_ORDER_KEY,
237
+ groupWorkspaceSessions,
238
+ hiddenSessionCount,
239
+ } from "@olares/lares-core/larepass/workspace";
240
+ import LaresPcDialog from "./ui/PcDialog.vue";
241
+ import LaresPcPopover from "./ui/PcPopover.vue";
242
+ import LaresPcTooltip from "./ui/PcTooltip.vue";
243
+ import {
244
+ dropAnchor,
245
+ insertSessionBefore,
246
+ loadWorkspaceView,
247
+ saveWorkspaceView,
248
+ syncWorkspaceViewOrders,
249
+ } from "./workspace-view.js";
250
+
251
+ export default {
252
+ name: "LaresDesktopSidebar",
253
+ components: { LaresPcDialog, LaresPcPopover, LaresPcTooltip },
254
+ props: {
255
+ state: { type: Object, required: true },
256
+ t: { type: Function, required: true },
257
+ },
258
+ emits: [
259
+ "create", "pick-session", "browse-workspaces", "refresh-workspaces",
260
+ "rename-workspace", "delete-workspace", "move-workspace", "move-session",
261
+ "rename-session", "fork-session", "archive-session",
262
+ ],
263
+ data() {
264
+ const view = loadWorkspaceView();
265
+ return {
266
+ query: "",
267
+ searchOpen: false,
268
+ optionsOpen: false,
269
+ flat: view.groupBy === "flat",
270
+ orderBy: view.orderBy,
271
+ expanded: view.groupExpansion,
272
+ sessionOrderByAccount: view.sessionOrderByAccount,
273
+ sessionUpdatedAtByAccount: view.sessionUpdatedAtByAccount,
274
+ sessionsExpanded: {},
275
+ workspaceDrag: null,
276
+ sessionDrag: null,
277
+ dropHint: null,
278
+ actionMenu: "",
279
+ dialog: null,
280
+ dialogValue: "",
281
+ };
282
+ },
283
+ computed: {
284
+ defaultWorkspaceId() {
285
+ return this.state.currentWorkspaceId || this.state.workspaces[0]?.workspaceId || "";
286
+ },
287
+ groups() {
288
+ return groupWorkspaceSessions(
289
+ this.state.sessions,
290
+ this.state.workspaces,
291
+ this.state.archivedSessionIds,
292
+ {
293
+ query: this.query,
294
+ orderBy: this.orderBy,
295
+ flat: this.flat,
296
+ currentSessionId: this.state.sessionId,
297
+ sessionOrderByAccount: this.sessionOrderByAccount,
298
+ },
299
+ );
300
+ },
301
+ orderRevision() {
302
+ return JSON.stringify({
303
+ current: this.state.sessionId,
304
+ sessions: this.state.sessions.map((row) => [row.sessionId, row.updatedAt, row.blank]),
305
+ workspaces: this.state.workspaces.map((row) => [row.workspaceId, row.sessionIds]),
306
+ });
307
+ },
308
+ isRenameDialog() {
309
+ return this.dialog?.kind === "rename-workspace" || this.dialog?.kind === "rename-session";
310
+ },
311
+ dialogTitle() {
312
+ if (this.dialog?.kind === "rename-workspace") return this.t("desktop.renameWorkspace");
313
+ if (this.dialog?.kind === "delete-workspace") return this.t("desktop.deleteWorkspace");
314
+ if (this.dialog?.kind === "rename-session") return this.t("desktop.renameSession");
315
+ if (this.dialog?.kind === "archive-session") return this.t("desktop.archiveSession");
316
+ return "";
317
+ },
318
+ dialogDescription() {
319
+ if (this.dialog?.kind === "delete-workspace") {
320
+ return this.t("desktop.deleteWorkspaceConfirm", { name: this.dialog.target.title });
321
+ }
322
+ if (this.dialog?.kind === "archive-session") return this.t("desktop.archiveSessionConfirm");
323
+ return "";
324
+ },
325
+ dialogConfirmLabel() {
326
+ if (this.dialog?.kind === "delete-workspace") return this.t("desktop.deleteWorkspace");
327
+ if (this.dialog?.kind === "archive-session") return this.t("desktop.archiveSession");
328
+ return this.t("desktop.save");
329
+ },
330
+ },
331
+ watch: {
332
+ orderRevision: {
333
+ immediate: true,
334
+ handler() {
335
+ this.syncOrders();
336
+ },
337
+ },
338
+ flat() {
339
+ this.persistView();
340
+ },
341
+ orderBy(next, previous) {
342
+ this.syncOrders(next === "updated" && previous !== "updated");
343
+ },
344
+ "state.workspaces": {
345
+ immediate: true,
346
+ handler(workspaces) {
347
+ const next = { ...this.expanded };
348
+ for (const workspace of workspaces ?? []) {
349
+ if (!(workspace.workspaceId in next)) next[workspace.workspaceId] = true;
350
+ }
351
+ this.expanded = next;
352
+ this.persistView();
353
+ },
354
+ },
355
+ },
356
+ methods: {
357
+ openSearch() {
358
+ this.searchOpen = true;
359
+ this.$nextTick(() => this.$refs.search?.focus());
360
+ },
361
+ closeSearch() {
362
+ this.query = "";
363
+ this.searchOpen = false;
364
+ },
365
+ isExpanded(key) {
366
+ return this.expanded[key] !== false;
367
+ },
368
+ toggleGroup(key) {
369
+ this.expanded = { ...this.expanded, [key]: !this.isExpanded(key) };
370
+ this.persistView();
371
+ },
372
+ isSessionsExpanded(key) {
373
+ return this.sessionsExpanded[key] === true;
374
+ },
375
+ toggleSessions(key) {
376
+ this.sessionsExpanded = { ...this.sessionsExpanded, [key]: !this.isSessionsExpanded(key) };
377
+ },
378
+ /** Colours the folder, so a folded group still says where the open session lives. */
379
+ holdsCurrent(group) {
380
+ return group.sessions.some((row) => row.sessionId === this.state.sessionId);
381
+ },
382
+ /** Folded groups cut to the same five rows the Lares app shows. */
383
+ visibleSessions(group) {
384
+ if (this.flat) return group.sessions;
385
+ return collapsedSessions(group.sessions, this.isSessionsExpanded(group.key));
386
+ },
387
+ overflowCount(group) {
388
+ return hiddenSessionCount(group.sessions);
389
+ },
390
+ viewState() {
391
+ return {
392
+ groupBy: this.flat ? "flat" : "workspace",
393
+ orderBy: this.orderBy,
394
+ groupExpansion: this.expanded,
395
+ sessionOrderByAccount: this.sessionOrderByAccount,
396
+ sessionUpdatedAtByAccount: this.sessionUpdatedAtByAccount,
397
+ };
398
+ },
399
+ persistView() {
400
+ saveWorkspaceView(this.viewState());
401
+ },
402
+ syncOrders(sortByRecency = false) {
403
+ const next = syncWorkspaceViewOrders(this.viewState(), {
404
+ sessions: this.state.sessions,
405
+ workspaces: this.state.workspaces,
406
+ currentSessionId: this.state.sessionId,
407
+ sortByRecency,
408
+ });
409
+ this.sessionOrderByAccount = next.sessionOrderByAccount;
410
+ this.sessionUpdatedAtByAccount = next.sessionUpdatedAtByAccount;
411
+ this.persistView();
412
+ },
413
+ setActionMenu(key, open) {
414
+ this.actionMenu = open ? key : "";
415
+ },
416
+ forkSession(session) {
417
+ this.actionMenu = "";
418
+ this.$emit("fork-session", session.sessionId);
419
+ },
420
+ openDialog(kind, target) {
421
+ this.actionMenu = "";
422
+ this.dialog = { kind, target };
423
+ this.dialogValue = kind === "rename-workspace" ? target.title : kind === "rename-session" ? target.title || "" : "";
424
+ if (this.isRenameDialog) this.$nextTick(() => {
425
+ this.$refs.dialogInput?.focus();
426
+ this.$refs.dialogInput?.select();
427
+ });
428
+ },
429
+ closeDialog() {
430
+ this.dialog = null;
431
+ this.dialogValue = "";
432
+ },
433
+ submitDialog() {
434
+ if (!this.dialog) return;
435
+ const { kind, target } = this.dialog;
436
+ if (kind === "rename-workspace" && this.dialogValue.trim()) {
437
+ this.$emit("rename-workspace", target.workspaceId, this.dialogValue.trim());
438
+ } else if (kind === "delete-workspace") {
439
+ this.$emit("delete-workspace", target.workspaceId);
440
+ } else if (kind === "rename-session" && this.dialogValue.trim()) {
441
+ this.$emit("rename-session", target.sessionId, this.dialogValue.trim());
442
+ } else if (kind === "archive-session") {
443
+ this.$emit("archive-session", target.sessionId);
444
+ } else {
445
+ return;
446
+ }
447
+ this.closeDialog();
448
+ },
449
+ relativeTime(value) {
450
+ if (!Number(value)) return "";
451
+ const diff = Math.max(0, Date.now() - (Number(value) || 0));
452
+ const days = Math.floor(diff / 86400000);
453
+ if (days > 0) return this.t("desktop.daysAgo", { n: days });
454
+ const hours = Math.floor(diff / 3600000);
455
+ if (hours > 0) return this.t("desktop.hoursAgo", { n: hours });
456
+ const minutes = Math.max(1, Math.floor(diff / 60000));
457
+ return this.t("desktop.minutesAgo", { n: minutes });
458
+ },
459
+ /** Which half of the hovered row the pointer is in, i.e. which edge the row lands on. */
460
+ edgeOf(event) {
461
+ const box = event.currentTarget.getBoundingClientRect();
462
+ return event.clientY - box.top < box.height / 2 ? "before" : "after";
463
+ },
464
+ /** The rule the marker draws and the drop follows: land on this side of `key`. */
465
+ dropEdge(kind, key) {
466
+ if (!key || this.dropHint?.kind !== kind || this.dropHint.key !== key) return undefined;
467
+ return this.dropHint.edge;
468
+ },
469
+ /** Dragging off a row drops its marker, but moving onto its own children does not. */
470
+ leaveRow(kind, key, event) {
471
+ if (event.currentTarget.contains(event.relatedTarget)) return;
472
+ if (this.dropHint?.kind === kind && this.dropHint.key === key) this.dropHint = null;
473
+ },
474
+ endDrag() {
475
+ this.workspaceDrag = null;
476
+ this.sessionDrag = null;
477
+ this.dropHint = null;
478
+ },
479
+ startWorkspaceDrag(group, event) {
480
+ if (!group.workspaceId || this.orderBy !== "manual") return event.preventDefault();
481
+ this.workspaceDrag = group.workspaceId;
482
+ event.dataTransfer.effectAllowed = "move";
483
+ // Firefox refuses to start a drag whose transfer carries nothing.
484
+ event.dataTransfer.setData("text/plain", group.workspaceId);
485
+ },
486
+ overWorkspace(group, event) {
487
+ if (!this.workspaceDrag || !group.workspaceId) {
488
+ this.dropHint = null;
489
+ return;
490
+ }
491
+ event.dataTransfer.dropEffect = "move";
492
+ this.dropHint = { kind: "workspace", key: group.workspaceId, edge: this.edgeOf(event) };
493
+ },
494
+ dropWorkspace() {
495
+ const dragged = this.workspaceDrag;
496
+ const anchor = this.dropHint?.kind === "workspace"
497
+ ? dropAnchor(this.groups.map((group) => group.workspaceId).filter(Boolean), this.dropHint)
498
+ : null;
499
+ this.endDrag();
500
+ if (!dragged || anchor === null || anchor === dragged) return;
501
+ this.$emit("move-workspace", dragged, anchor);
502
+ },
503
+ startSessionDrag(group, session, event) {
504
+ this.sessionDrag = {
505
+ accountKey: this.flat ? FLAT_SESSION_ORDER_KEY : group.key,
506
+ workspaceId: group.workspaceId,
507
+ sessionId: session.sessionId,
508
+ };
509
+ event.dataTransfer.effectAllowed = "move";
510
+ event.dataTransfer.setData("text/plain", session.sessionId);
511
+ },
512
+ overSession(group, session, event) {
513
+ const accountKey = this.flat ? FLAT_SESSION_ORDER_KEY : group.key;
514
+ if (accountKey !== this.sessionDrag?.accountKey) return;
515
+ event.dataTransfer.dropEffect = "move";
516
+ this.dropHint = { kind: "session", key: session.sessionId, edge: this.edgeOf(event) };
517
+ },
518
+ dropSession(group) {
519
+ const drag = this.sessionDrag;
520
+ const accountKey = this.flat ? FLAT_SESSION_ORDER_KEY : group.key;
521
+ // The full list, not the collapsed one: dropping past the last visible row
522
+ // lands before the first folded session, which is where the marker sits.
523
+ const rows = group.sessions.map((row) => row.sessionId);
524
+ const anchor = this.dropHint?.kind === "session" ? dropAnchor(rows, this.dropHint) : null;
525
+ this.endDrag();
526
+ if (!drag || anchor === null || accountKey !== drag.accountKey) return;
527
+ const sessionId = drag.sessionId;
528
+ if (anchor === sessionId) return;
529
+ const current = this.sessionOrderByAccount[accountKey] ?? group.sessions.map((row) => row.sessionId);
530
+ this.sessionOrderByAccount = {
531
+ ...this.sessionOrderByAccount,
532
+ [accountKey]: insertSessionBefore(current, sessionId, anchor),
533
+ };
534
+ this.persistView();
535
+ if (this.orderBy === "manual" && group.workspaceId) {
536
+ this.$emit("move-session", group.workspaceId, sessionId, anchor);
537
+ }
538
+ },
539
+ },
540
+ };
541
+ </script>
542
+
543
+ <style scoped>
544
+ .desktop-sidebar { position:relative; display:flex; width:210px; min-width:180px; max-width:240px; flex:0 0 21%; min-height:0; flex-direction:column; border-right:1px solid var(--q-separator); background:var(--q-background-2, var(--q-background-1)); color:var(--q-ink-1); }
545
+ .desktop-sidebar svg { width:16px; height:16px; flex-shrink:0; fill:none; stroke:currentColor; stroke-width:1.65; stroke-linecap:round; stroke-linejoin:round; }
546
+ .desktop-sidebar button { font:inherit; }
547
+ .desktop-sidebar__new { display:flex; height:36px; flex-shrink:0; align-items:center; justify-content:center; gap:8px; margin:12px 10px 15px; border:0; border-radius:999px; background:var(--q-background-3); color:var(--q-ink-1); font-size:15px; cursor:pointer; }
548
+ .desktop-sidebar__new svg { width:18px; height:18px; }
549
+ .desktop-sidebar__new:hover { background:var(--q-background-hover); }
550
+ .desktop-sidebar__section-head { position:relative; display:flex; height:34px; flex-shrink:0; align-items:center; gap:6px; padding:0 10px; color:var(--q-ink-3); font-size:15px; }
551
+ .desktop-sidebar__section-head > span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
552
+ .desktop-sidebar__actions { display:flex; flex-shrink:0; align-items:center; gap:1px; }
553
+ .desktop-sidebar__actions svg { width:19px; height:19px; }
554
+ .desktop-sidebar__actions button,
555
+ .desktop-sidebar__search button { display:grid; width:28px; height:28px; place-items:center; border:0; border-radius:6px; background:transparent; color:var(--q-ink-3); cursor:pointer; }
556
+ .desktop-sidebar__actions button:hover { background:var(--q-background-hover); color:var(--q-ink-1); }
557
+ .desktop-sidebar__search { display:flex; width:100%; align-items:center; gap:5px; border-radius:6px; padding-left:6px; background:var(--q-background-3); }
558
+ .desktop-sidebar__search input { min-width:0; flex:1; border:0; outline:0; padding:5px 0; background:transparent; color:var(--q-ink-1); font-size:14px; }
559
+ .desktop-sidebar__options { margin:-2px; }
560
+ .desktop-sidebar__options p { margin:0; padding:6px 7px 3px; color:var(--q-ink-3); font-size:12px; }
561
+ .desktop-sidebar__options button { display:block; width:100%; border:0; border-radius:5px; padding:6px 7px; background:transparent; color:var(--q-ink-2); font-size:13px; text-align:left; }
562
+ .desktop-sidebar__options button:hover { background:var(--q-background-hover); }
563
+ .desktop-sidebar__options button[data-selected="true"] { color:var(--q-blue-default); }
564
+ .desktop-sidebar__list { flex:1; min-height:0; overflow:auto; padding:2px 7px 14px; }
565
+ .desktop-sidebar__group { margin-bottom:5px; }
566
+ .desktop-sidebar__group-row { display:flex; min-height:28px; align-items:center; border-radius:6px; }
567
+ .desktop-sidebar__group-row:hover,
568
+ .desktop-sidebar__group-row[data-open="true"] { background:var(--q-background-hover); }
569
+ .desktop-sidebar__group-toggle { display:flex; min-width:0; width:100%; align-items:center; gap:6px; overflow:hidden; border:0; border-radius:6px; padding:5px 4px; background:transparent; color:var(--q-ink-2); text-align:left; cursor:pointer; }
570
+ .desktop-sidebar__group-toggle span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
571
+ /* At rest the folder names the group; pointing at the row turns the same slot
572
+ into the control that opens it, so the caret must not shift the title. */
573
+ .desktop-sidebar__group-toggle > .desktop-sidebar__group-icon { display:grid; width:16px; height:16px; flex:0 0 16px; place-items:center; }
574
+ .desktop-sidebar__group-icon svg { grid-area:1/1; }
575
+ .desktop-sidebar__folder { color:var(--q-ink-3); }
576
+ .desktop-sidebar__group-icon[data-active="true"] .desktop-sidebar__folder { color:var(--q-blue-default); }
577
+ .desktop-sidebar__caret { color:var(--q-ink-3); fill:currentColor; stroke:none; opacity:0; transition:transform 120ms ease; }
578
+ .desktop-sidebar__group-icon[data-expanded="true"] .desktop-sidebar__caret { transform:rotate(90deg); }
579
+ .desktop-sidebar__group-row:hover .desktop-sidebar__folder,
580
+ .desktop-sidebar__group-row[data-open="true"] .desktop-sidebar__folder,
581
+ .desktop-sidebar__group-toggle:focus-visible .desktop-sidebar__folder { opacity:0; }
582
+ .desktop-sidebar__group-row:hover .desktop-sidebar__caret,
583
+ .desktop-sidebar__group-row[data-open="true"] .desktop-sidebar__caret,
584
+ .desktop-sidebar__group-toggle:focus-visible .desktop-sidebar__caret { opacity:1; }
585
+ .desktop-sidebar__row-actions { display:flex; flex-shrink:0; align-items:center; }
586
+ .desktop-sidebar__row-action { display:grid; width:22px; height:22px; place-items:center; border:0; border-radius:5px; background:transparent; color:var(--q-ink-3); cursor:pointer; opacity:0; pointer-events:none; }
587
+ .desktop-sidebar__group-row:hover .desktop-sidebar__row-action,
588
+ .desktop-sidebar__group-row[data-open="true"] .desktop-sidebar__row-action { opacity:1; pointer-events:auto; }
589
+ .desktop-sidebar__row-action:hover { color:var(--q-ink-1); }
590
+ .desktop-sidebar__row-action svg circle { fill:currentColor; stroke:none; }
591
+ .desktop-sidebar__sessions { display:flex; flex-direction:column; gap:1px; }
592
+ /* Reorder marker: the bar sits on the edge the dragged row lands on. */
593
+ .desktop-sidebar__group-row[data-drop],
594
+ .desktop-sidebar__session-row[data-drop] { position:relative; }
595
+ .desktop-sidebar__group-row[data-drop]::after,
596
+ .desktop-sidebar__session-row[data-drop]::after { content:""; position:absolute; right:2px; left:2px; height:2px; border-radius:2px; background:var(--q-blue-default); pointer-events:none; }
597
+ .desktop-sidebar__group-row[data-drop="before"]::after,
598
+ .desktop-sidebar__session-row[data-drop="before"]::after { top:-1px; }
599
+ .desktop-sidebar__group-row[data-drop="after"]::after,
600
+ .desktop-sidebar__session-row[data-drop="after"]::after { bottom:-1px; }
601
+ .desktop-sidebar__session-row { display:flex; width:100%; align-items:center; border-radius:7px; padding-right:4px; }
602
+ .desktop-sidebar__session-row:hover,
603
+ .desktop-sidebar__session-row[data-open="true"] { background:var(--q-background-hover); }
604
+ .desktop-sidebar__session-row[data-current="true"] { background:var(--q-background-3); }
605
+ .desktop-sidebar__session { display:flex; min-width:0; flex:1; height:30px; align-items:center; gap:6px; border:0; border-radius:7px; padding:0 4px 0 10px; background:transparent; color:var(--q-ink-2); font-size:13px; text-align:left; cursor:pointer; }
606
+ .desktop-sidebar__session > span { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
607
+ /* Always laid out, so a session's title sits in the same place with or without
608
+ the dot; the host sends no read state, the shell decides (see larepass/unread). */
609
+ .desktop-sidebar__session > .desktop-sidebar__unseen { width:6px; height:6px; flex:0 0 6px; border-radius:50%; background:transparent; }
610
+ .desktop-sidebar__session-row[data-unseen="true"] .desktop-sidebar__unseen { background:var(--q-positive, var(--q-green-default, #21c55d)); }
611
+ .desktop-sidebar__session time { flex-shrink:0; color:var(--q-ink-3); font-size:9px; }
612
+ .desktop-sidebar__session-row[data-current="true"] .desktop-sidebar__session { color:var(--q-ink-1); }
613
+ .desktop-sidebar__session-row:hover time,
614
+ .desktop-sidebar__session-row[data-open="true"] time { display:none; }
615
+ .desktop-sidebar__session-action { display:none; width:22px; height:22px; flex-shrink:0; place-items:center; border:0; border-radius:5px; background:transparent; color:var(--q-ink-3); cursor:pointer; }
616
+ .desktop-sidebar__session-row:hover .desktop-sidebar__session-action,
617
+ .desktop-sidebar__session-row[data-open="true"] .desktop-sidebar__session-action { display:grid; }
618
+ .desktop-sidebar__session-action:hover { color:var(--q-ink-1); }
619
+ .desktop-sidebar__session-action svg circle { fill:currentColor; stroke:none; }
620
+ /* Lares app parity: a plain text row, never a filled control. */
621
+ .desktop-sidebar__session-overflow { width:100%; height:26px; border:0; border-radius:7px; padding:0 8px 0 24px; background:transparent; color:var(--q-ink-3); font-size:13px; text-align:left; cursor:pointer; }
622
+ .desktop-sidebar__session-overflow:hover { color:var(--q-ink-2); }
623
+ .desktop-sidebar__action-menu { margin:-2px; }
624
+ .desktop-sidebar__action-menu button { display:flex; width:100%; align-items:center; gap:8px; border:0; border-radius:5px; padding:7px 8px; background:transparent; color:var(--q-ink-2); font-size:13px; text-align:left; cursor:pointer; }
625
+ /* Teleported out of `.desktop-sidebar`, so the sidebar's own icon rule misses these. */
626
+ .desktop-sidebar__action-menu svg { width:15px; height:15px; flex-shrink:0; fill:none; stroke:currentColor; stroke-width:1.65; stroke-linecap:round; stroke-linejoin:round; }
627
+ .desktop-sidebar__action-menu span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
628
+ .desktop-sidebar__action-menu button:hover { background:var(--q-background-hover); }
629
+ .desktop-sidebar__action-menu button[data-danger="true"] { color:var(--q-red-default, #ef4444); }
630
+ .desktop-sidebar__dialog-input { box-sizing:border-box; width:100%; height:36px; border:1px solid var(--q-input-stroke, var(--q-separator)); border-radius:8px; padding:0 10px; outline:none; background:var(--q-background-2, transparent); color:var(--q-ink-1); font:inherit; font-size:15px; }
631
+ .desktop-sidebar__dialog-input:focus { border-color:var(--q-blue-default); }
632
+ .desktop-sidebar__dialog-cancel,
633
+ .desktop-sidebar__dialog-confirm { min-width:76px; height:32px; border-radius:7px; padding:0 12px; font:inherit; cursor:pointer; }
634
+ .desktop-sidebar__dialog-cancel { border:1px solid var(--q-separator); background:transparent; color:var(--q-ink-2); }
635
+ .desktop-sidebar__dialog-confirm { border:0; background:var(--q-blue-default); color:var(--q-ink-on-brand); }
636
+ .desktop-sidebar__dialog-confirm[data-danger="true"] { background:var(--q-orange-default); }
637
+ .desktop-sidebar__dialog-confirm:disabled { opacity:.5; cursor:default; }
638
+ .desktop-sidebar__loading { display:flex; flex-direction:column; gap:8px; padding:7px; }
639
+ .desktop-sidebar__loading span { height:24px; border-radius:6px; background:var(--q-background-3); animation:sidebar-pulse 1.2s ease-in-out infinite; }
640
+ .desktop-sidebar__empty,
641
+ .desktop-sidebar__error { margin:14px 8px; color:var(--q-ink-3); font-size:13px; }
642
+ .desktop-sidebar__error button { display:block; margin-top:7px; border:0; background:transparent; color:var(--q-blue-default); }
643
+ @keyframes sidebar-pulse { 50% { opacity:.45; } }
644
+ @media (prefers-reduced-motion: reduce) { .desktop-sidebar__loading span { animation:none; } }
645
+ </style>