@agent-link/server 0.1.187 → 0.1.189

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 (76) hide show
  1. package/dist/auth-manager.d.ts +36 -0
  2. package/dist/auth-manager.js +96 -0
  3. package/dist/auth-manager.js.map +1 -0
  4. package/dist/http.d.ts +4 -0
  5. package/dist/http.js +85 -0
  6. package/dist/http.js.map +1 -0
  7. package/dist/index.js +5 -84
  8. package/dist/index.js.map +1 -1
  9. package/dist/message-relay.d.ts +17 -0
  10. package/dist/message-relay.js +23 -0
  11. package/dist/message-relay.js.map +1 -0
  12. package/dist/session-manager.d.ts +44 -0
  13. package/dist/session-manager.js +83 -0
  14. package/dist/session-manager.js.map +1 -0
  15. package/dist/ws-agent.js +19 -27
  16. package/dist/ws-agent.js.map +1 -1
  17. package/dist/ws-client.js +31 -37
  18. package/dist/ws-client.js.map +1 -1
  19. package/package.json +3 -3
  20. package/web/dist/assets/index-DIO7Hox0.js +320 -0
  21. package/web/dist/assets/index-DIO7Hox0.js.map +1 -0
  22. package/web/dist/assets/index-Y1FN_mFe.css +1 -0
  23. package/web/{index.html → dist/index.html} +2 -19
  24. package/dist/auth.d.ts +0 -13
  25. package/dist/auth.js +0 -65
  26. package/dist/auth.js.map +0 -1
  27. package/dist/context.d.ts +0 -52
  28. package/dist/context.js +0 -60
  29. package/dist/context.js.map +0 -1
  30. package/web/app.js +0 -2881
  31. package/web/css/ask-question.css +0 -333
  32. package/web/css/base.css +0 -270
  33. package/web/css/btw.css +0 -148
  34. package/web/css/chat.css +0 -176
  35. package/web/css/file-browser.css +0 -499
  36. package/web/css/input.css +0 -671
  37. package/web/css/loop.css +0 -674
  38. package/web/css/markdown.css +0 -169
  39. package/web/css/responsive.css +0 -314
  40. package/web/css/sidebar.css +0 -593
  41. package/web/css/team.css +0 -1277
  42. package/web/css/tools.css +0 -327
  43. package/web/encryption.js +0 -56
  44. package/web/modules/appHelpers.js +0 -100
  45. package/web/modules/askQuestion.js +0 -63
  46. package/web/modules/backgroundRouting.js +0 -269
  47. package/web/modules/connection.js +0 -731
  48. package/web/modules/fileAttachments.js +0 -125
  49. package/web/modules/fileBrowser.js +0 -398
  50. package/web/modules/filePreview.js +0 -213
  51. package/web/modules/i18n.js +0 -101
  52. package/web/modules/loop.js +0 -338
  53. package/web/modules/loopTemplates.js +0 -110
  54. package/web/modules/markdown.js +0 -83
  55. package/web/modules/messageHelpers.js +0 -206
  56. package/web/modules/sidebar.js +0 -402
  57. package/web/modules/streaming.js +0 -116
  58. package/web/modules/team.js +0 -396
  59. package/web/modules/teamTemplates.js +0 -360
  60. package/web/vendor/highlight.min.js +0 -1213
  61. package/web/vendor/marked.min.js +0 -6
  62. package/web/vendor/nacl-fast.min.js +0 -1
  63. package/web/vendor/nacl-util.min.js +0 -1
  64. package/web/vendor/pako.min.js +0 -2
  65. package/web/vendor/vue.global.prod.js +0 -13
  66. /package/web/{favicon.svg → dist/favicon.svg} +0 -0
  67. /package/web/{images → dist/images}/chat-iPad.webp +0 -0
  68. /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
  69. /package/web/{images → dist/images}/loop-iPad.webp +0 -0
  70. /package/web/{images → dist/images}/team-iPad.webp +0 -0
  71. /package/web/{landing.html → dist/landing.html} +0 -0
  72. /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
  73. /package/web/{locales → dist/locales}/en.json +0 -0
  74. /package/web/{locales → dist/locales}/zh.json +0 -0
  75. /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
  76. /package/web/{vendor → dist/vendor}/github.min.css +0 -0
package/web/app.js DELETED
@@ -1,2881 +0,0 @@
1
- // ── AgentLink Web UI — Main coordinator ──────────────────────────────────────
2
- const { createApp, ref, nextTick, onMounted, onUnmounted, computed, watch } = Vue;
3
-
4
- // Module imports
5
- import { renderMarkdown, getToolIcon } from './modules/markdown.js';
6
- import {
7
- isContextSummary, formatRelativeTime, formatTimestamp,
8
- getRenderedContent, copyMessage, isPrevAssistant, toggleContextSummary,
9
- toggleTool, getToolSummary, isEditTool, getFormattedToolInput, getEditDiffHtml,
10
- } from './modules/messageHelpers.js';
11
- import { formatFileSize, createFileAttachments } from './modules/fileAttachments.js';
12
- import {
13
- selectQuestionOption, submitQuestionAnswer,
14
- hasQuestionAnswer, getQuestionResponseSummary,
15
- } from './modules/askQuestion.js';
16
- import { createStreaming } from './modules/streaming.js';
17
- import { createSidebar } from './modules/sidebar.js';
18
- import { createConnection } from './modules/connection.js';
19
- import { createFileBrowser } from './modules/fileBrowser.js';
20
- import { createFilePreview } from './modules/filePreview.js';
21
- import { createTeam } from './modules/team.js';
22
- import { TEMPLATES, TEMPLATE_KEYS, buildFullLeadPrompt } from './modules/teamTemplates.js';
23
- import { createLoop } from './modules/loop.js';
24
- import { LOOP_TEMPLATES, LOOP_TEMPLATE_KEYS, buildCronExpression, formatSchedule } from './modules/loopTemplates.js';
25
- import { createScrollManager, createHighlightScheduler, formatUsage } from './modules/appHelpers.js';
26
- import { createI18n } from './modules/i18n.js';
27
-
28
- // ── Slash commands ──────────────────────────────────────────────────────────
29
- const SLASH_COMMANDS = [
30
- { command: '/btw', descKey: 'slash.btw', isPrefix: true },
31
- { command: '/cost', descKey: 'slash.cost' },
32
- { command: '/context', descKey: 'slash.context' },
33
- { command: '/compact', descKey: 'slash.compact' },
34
- ];
35
-
36
- // ── App ─────────────────────────────────────────────────────────────────────
37
- const App = {
38
- setup() {
39
- // ── Reactive state ──
40
- const status = ref('Connecting...');
41
- const agentName = ref('');
42
- const hostname = ref('');
43
- const workDir = ref('');
44
- const sessionId = ref('');
45
- const error = ref('');
46
- const serverVersion = ref('');
47
- const agentVersion = ref('');
48
- const messages = ref([]);
49
- const visibleLimit = ref(50);
50
- const hasMoreMessages = computed(() => messages.value.length > visibleLimit.value);
51
- const visibleMessages = computed(() => {
52
- if (messages.value.length <= visibleLimit.value) return messages.value;
53
- return messages.value.slice(messages.value.length - visibleLimit.value);
54
- });
55
- function loadMoreMessages() {
56
- const el = document.querySelector('.message-list');
57
- const prevHeight = el ? el.scrollHeight : 0;
58
- visibleLimit.value += 50;
59
- nextTick(() => {
60
- if (el) el.scrollTop += el.scrollHeight - prevHeight;
61
- });
62
- }
63
- const inputText = ref('');
64
- const isProcessing = ref(false);
65
- const isCompacting = ref(false);
66
- const latency = ref(null);
67
- const queuedMessages = ref([]);
68
- const usageStats = ref(null);
69
- const inputRef = ref(null);
70
- const slashMenuIndex = ref(0);
71
- const slashMenuOpen = ref(false);
72
-
73
- // Side question (/btw) state
74
- const btwState = ref(null);
75
- const btwPending = ref(false);
76
-
77
- // Sidebar state
78
- const sidebarOpen = ref(window.innerWidth > 768);
79
- const historySessions = ref([]);
80
- const currentClaudeSessionId = ref(null);
81
- const needsResume = ref(false);
82
- const loadingSessions = ref(false);
83
- const loadingHistory = ref(false);
84
-
85
- // Folder picker state
86
- const folderPickerOpen = ref(false);
87
- const folderPickerPath = ref('');
88
- const folderPickerEntries = ref([]);
89
- const folderPickerLoading = ref(false);
90
- const folderPickerSelected = ref('');
91
-
92
- // Delete confirmation dialog state
93
- const deleteConfirmOpen = ref(false);
94
- const deleteConfirmTitle = ref('');
95
-
96
- // Rename session state
97
- const renamingSessionId = ref(null);
98
- const renameText = ref('');
99
-
100
- // Team rename/delete state
101
- const renamingTeamId = ref(null);
102
- const renameTeamText = ref('');
103
- const deleteTeamConfirmOpen = ref(false);
104
- const deleteTeamConfirmTitle = ref('');
105
- const pendingDeleteTeamId = ref(null);
106
-
107
- // Working directory history
108
- const workdirHistory = ref([]);
109
-
110
- // Working directory switching loading state
111
- const workdirSwitching = ref(false);
112
-
113
- // Authentication state
114
- const authRequired = ref(false);
115
- const authPassword = ref('');
116
- const authError = ref('');
117
- const authAttempts = ref(null);
118
- const authLocked = ref(false);
119
-
120
- // File attachment state
121
- const attachments = ref([]);
122
- const fileInputRef = ref(null);
123
- const dragOver = ref(false);
124
-
125
- // Multi-session parallel state
126
- const conversationCache = ref({}); // conversationId → saved state snapshot
127
- const currentConversationId = ref(crypto.randomUUID()); // currently visible conversation
128
- const processingConversations = ref({}); // conversationId → boolean
129
-
130
- // Plan mode state
131
- const planMode = ref(false);
132
- const pendingPlanMode = ref(null); // 'enter' | 'exit' | null — set while toggle is in flight
133
-
134
- // File browser state
135
- const filePanelOpen = ref(false);
136
- const filePanelWidth = ref(parseInt(localStorage.getItem('agentlink-file-panel-width'), 10) || 280);
137
- const fileTreeRoot = ref(null);
138
- const fileTreeLoading = ref(false);
139
- const fileContextMenu = ref(null);
140
- const sidebarView = ref('sessions'); // 'sessions' | 'files' | 'preview' | 'memory' (mobile only)
141
- const isMobile = ref(window.innerWidth <= 768);
142
- const workdirMenuOpen = ref(false);
143
- const teamsCollapsed = ref(false);
144
- const chatsCollapsed = ref(false);
145
- const loopsCollapsed = ref(false);
146
-
147
- // Memory management state
148
- const memoryPanelOpen = ref(false);
149
- const memoryFiles = ref([]);
150
- const memoryDir = ref(null);
151
- const memoryLoading = ref(false);
152
- const memoryEditing = ref(false);
153
- const memoryEditContent = ref('');
154
- const memorySaving = ref(false);
155
- const _sidebarCollapseKey = () => hostname.value ? `agentlink-sidebar-collapsed-${hostname.value}` : null;
156
- const loadingTeams = ref(false);
157
- const loadingLoops = ref(false);
158
-
159
- // Team creation state
160
- const teamInstruction = ref('');
161
- const selectedTemplate = ref('custom');
162
- const editedLeadPrompt = ref(TEMPLATES.custom.leadPrompt);
163
- const leadPromptExpanded = ref(false);
164
- const teamExamples = [
165
- {
166
- icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>',
167
- title: 'Full-stack App',
168
- template: 'full-stack',
169
- text: 'Build a single-page calculator app: one agent creates the HTML/CSS UI, one implements the JavaScript logic, and one writes tests.',
170
- },
171
- {
172
- icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>',
173
- title: 'Research',
174
- template: 'research',
175
- text: 'Research this project\'s architecture: one agent analyzes the backend structure, one maps the frontend components, and one reviews the build and deployment pipeline. Produce a unified architecture report.',
176
- },
177
- {
178
- icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>',
179
- title: '代码审查',
180
- template: 'code-review',
181
- text: '审查当前项目的代码质量、安全漏洞和测试覆盖率,按严重程度生成分级报告,并给出修复建议。',
182
- },
183
- {
184
- icon: '<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>',
185
- title: '技术文档',
186
- template: 'content',
187
- text: '为当前项目编写一份完整的技术文档:先调研项目结构和核心模块,然后撰写包含架构概览、API 参考和使用指南的文档,最后校审确保准确性和可读性。',
188
- },
189
- ];
190
- const kanbanExpanded = ref(false);
191
- const instructionExpanded = ref(false);
192
-
193
- // Loop creation/editing form state
194
- const loopName = ref('');
195
- const loopPrompt = ref('');
196
- const loopScheduleType = ref('daily');
197
- const loopScheduleHour = ref(9);
198
- const loopScheduleMinute = ref(0);
199
- const loopScheduleDayOfWeek = ref(1);
200
- const loopCronExpr = ref('0 9 * * *');
201
- const loopSelectedTemplate = ref(null);
202
- const loopDeleteConfirmOpen = ref(false);
203
- const loopDeleteConfirmId = ref(null);
204
- const loopDeleteConfirmName = ref('');
205
- const renamingLoopId = ref(null);
206
- const renameLoopText = ref('');
207
-
208
- // File preview state
209
- const previewPanelOpen = ref(false);
210
- const previewPanelWidth = ref(parseInt(localStorage.getItem('agentlink-preview-panel-width'), 10) || 400);
211
- const previewFile = ref(null);
212
- const previewLoading = ref(false);
213
- const previewMarkdownRendered = ref(false);
214
- const isMemoryPreview = computed(() => {
215
- if (!previewFile.value?.filePath || !memoryDir.value) return false;
216
- const fp = previewFile.value.filePath.replace(/\\/g, '/');
217
- const md = memoryDir.value.replace(/\\/g, '/');
218
- return fp.startsWith(md);
219
- });
220
-
221
- // ── switchConversation: save current → load target ──
222
- // Defined here and used by sidebar.newConversation, sidebar.resumeSession, workdir_changed
223
- // Needs access to streaming / connection which are created later, so we use late-binding refs.
224
- let _getToolMsgMap = () => new Map();
225
- let _restoreToolMsgMap = () => {};
226
- let _clearToolMsgMap = () => {};
227
-
228
- function switchConversation(newConvId) {
229
- const oldConvId = currentConversationId.value;
230
-
231
- // Save current state (if there is one)
232
- if (oldConvId) {
233
- const streamState = streaming.saveState();
234
- conversationCache.value[oldConvId] = {
235
- messages: messages.value,
236
- isProcessing: isProcessing.value,
237
- isCompacting: isCompacting.value,
238
- loadingHistory: loadingHistory.value,
239
- claudeSessionId: currentClaudeSessionId.value,
240
- visibleLimit: visibleLimit.value,
241
- needsResume: needsResume.value,
242
- streamingState: streamState,
243
- toolMsgMap: _getToolMsgMap(),
244
- messageIdCounter: streaming.getMessageIdCounter(),
245
- queuedMessages: queuedMessages.value,
246
- usageStats: usageStats.value,
247
- planMode: planMode.value,
248
- };
249
- }
250
-
251
- // Load target state
252
- const cached = conversationCache.value[newConvId];
253
- if (cached) {
254
- messages.value = cached.messages;
255
- isProcessing.value = cached.isProcessing;
256
- isCompacting.value = cached.isCompacting;
257
- loadingHistory.value = cached.loadingHistory || false;
258
- currentClaudeSessionId.value = cached.claudeSessionId;
259
- visibleLimit.value = cached.visibleLimit;
260
- needsResume.value = cached.needsResume;
261
- streaming.restoreState(cached.streamingState || { pendingText: '', streamingMessageId: null, messageIdCounter: cached.messageIdCounter || 0 });
262
- // Background routing may have incremented messageIdCounter beyond what
263
- // streamingState recorded at save time — use the authoritative value.
264
- streaming.setMessageIdCounter(cached.messageIdCounter || 0);
265
- _restoreToolMsgMap(cached.toolMsgMap || new Map());
266
- queuedMessages.value = cached.queuedMessages || [];
267
- usageStats.value = cached.usageStats || null;
268
- planMode.value = cached.planMode || false;
269
- } else {
270
- // New blank conversation
271
- messages.value = [];
272
- isProcessing.value = false;
273
- isCompacting.value = false;
274
- loadingHistory.value = false;
275
- currentClaudeSessionId.value = null;
276
- visibleLimit.value = 50;
277
- needsResume.value = false;
278
- streaming.setMessageIdCounter(0);
279
- streaming.setStreamingMessageId(null);
280
- streaming.reset();
281
- _clearToolMsgMap();
282
- queuedMessages.value = [];
283
- usageStats.value = null;
284
- planMode.value = false;
285
- }
286
-
287
- currentConversationId.value = newConvId;
288
- scrollToBottom(true);
289
- }
290
-
291
- // Theme
292
- const theme = ref(localStorage.getItem('agentlink-theme') || 'light');
293
- function applyTheme() {
294
- document.documentElement.setAttribute('data-theme', theme.value);
295
- const link = document.getElementById('hljs-theme');
296
- if (link) link.href = theme.value === 'light'
297
- ? '/vendor/github.min.css'
298
- : '/vendor/github-dark.min.css';
299
- }
300
- function toggleTheme() {
301
- theme.value = theme.value === 'dark' ? 'light' : 'dark';
302
- localStorage.setItem('agentlink-theme', theme.value);
303
- applyTheme();
304
- }
305
- applyTheme();
306
-
307
- // ── i18n ──
308
- const { t, locale, setLocale, toggleLocale, localeLabel } = createI18n();
309
-
310
- // Map internal English status values to translated display strings
311
- const STATUS_KEYS = {
312
- 'No Session': 'status.noSession',
313
- 'Connecting...': 'status.connecting',
314
- 'Connected': 'status.connected',
315
- 'Waiting': 'status.waiting',
316
- 'Reconnecting...': 'status.reconnecting',
317
- 'Disconnected': 'status.disconnected',
318
- 'Authentication Required': 'status.authRequired',
319
- 'Locked': 'status.locked',
320
- };
321
- const displayStatus = computed(() => {
322
- const key = STATUS_KEYS[status.value];
323
- return key ? t(key) : status.value;
324
- });
325
-
326
- // ── Scroll management ──
327
- const { onScroll: onMessageListScroll, scrollToBottom, cleanup: cleanupScroll } = createScrollManager('.message-list');
328
-
329
- // ── Highlight.js scheduling ──
330
- const { scheduleHighlight, cleanup: cleanupHighlight } = createHighlightScheduler();
331
-
332
- // ── Create module instances ──
333
-
334
- const streaming = createStreaming({ messages, scrollToBottom });
335
-
336
- const fileAttach = createFileAttachments(attachments, fileInputRef, dragOver);
337
-
338
- // Sidebar needs wsSend, but connection creates wsSend.
339
- // Resolve circular dependency with a forwarding function.
340
- let _wsSend = () => {};
341
-
342
- const sidebar = createSidebar({
343
- wsSend: (msg) => _wsSend(msg),
344
- messages, isProcessing, sidebarOpen,
345
- historySessions, currentClaudeSessionId, needsResume,
346
- loadingSessions, loadingHistory, workDir, visibleLimit,
347
- folderPickerOpen, folderPickerPath, folderPickerEntries,
348
- folderPickerLoading, folderPickerSelected, streaming,
349
- deleteConfirmOpen, deleteConfirmTitle,
350
- renamingSessionId, renameText,
351
- hostname, workdirHistory, workdirSwitching,
352
- // Multi-session parallel
353
- currentConversationId, conversationCache, processingConversations,
354
- switchConversation,
355
- // i18n
356
- t,
357
- });
358
- const { connect, wsSend, closeWs, submitPassword, setDequeueNext, setFileBrowser, setFilePreview, setTeam, setLoop, getToolMsgMap, restoreToolMsgMap, clearToolMsgMap } = createConnection({
359
- status, agentName, hostname, workDir, sessionId, error,
360
- serverVersion, agentVersion, latency,
361
- messages, isProcessing, isCompacting, visibleLimit, queuedMessages, usageStats,
362
- historySessions, currentClaudeSessionId, needsResume, loadingSessions, loadingHistory,
363
- folderPickerLoading, folderPickerEntries, folderPickerPath,
364
- authRequired, authPassword, authError, authAttempts, authLocked,
365
- streaming, sidebar, scrollToBottom,
366
- workdirSwitching,
367
- // Multi-session parallel
368
- currentConversationId, processingConversations, conversationCache,
369
- switchConversation,
370
- // Memory management
371
- memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
372
- // Side question (/btw)
373
- btwState, btwPending,
374
- // Plan mode
375
- setPlanMode,
376
- // i18n
377
- t,
378
- });
379
-
380
- // Now wire up the forwarding function
381
- _wsSend = wsSend;
382
- setDequeueNext(dequeueNext);
383
- // Wire up late-binding toolMsgMap functions for switchConversation
384
- _getToolMsgMap = getToolMsgMap;
385
- _restoreToolMsgMap = restoreToolMsgMap;
386
- _clearToolMsgMap = clearToolMsgMap;
387
-
388
- // Team module
389
- const team = createTeam({
390
- wsSend, scrollToBottom, loadingTeams,
391
- });
392
- setTeam(team);
393
- // Loop module
394
- const loop = createLoop({
395
- wsSend, scrollToBottom, loadingLoops,
396
- });
397
- setLoop(loop);
398
- sidebar.setOnSwitchToChat(() => {
399
- team.viewMode.value = 'chat';
400
- team.historicalTeam.value = null;
401
- });
402
-
403
- // File browser module
404
- const fileBrowser = createFileBrowser({
405
- wsSend, workDir, inputText, inputRef, sendMessage,
406
- filePanelOpen, filePanelWidth, fileTreeRoot, fileTreeLoading, fileContextMenu,
407
- sidebarOpen, sidebarView,
408
- });
409
- setFileBrowser(fileBrowser);
410
-
411
- // File preview module
412
- const filePreview = createFilePreview({
413
- wsSend, previewPanelOpen, previewPanelWidth, previewFile, previewLoading,
414
- previewMarkdownRendered, sidebarView, sidebarOpen, isMobile, renderMarkdown,
415
- });
416
- setFilePreview(filePreview);
417
-
418
- // Track mobile state on resize (rAF-throttled)
419
- let _resizeRafId = 0;
420
- let _resizeHandler = () => {
421
- if (_resizeRafId) return;
422
- _resizeRafId = requestAnimationFrame(() => {
423
- _resizeRafId = 0;
424
- isMobile.value = window.innerWidth <= 768;
425
- });
426
- };
427
- window.addEventListener('resize', _resizeHandler);
428
-
429
- // Close workdir menu on outside click or Escape
430
- let _workdirMenuClickHandler = (e) => {
431
- if (!workdirMenuOpen.value) return;
432
- const row = document.querySelector('.sidebar-workdir-path-row');
433
- const menu = document.querySelector('.workdir-menu');
434
- if ((row && row.contains(e.target)) || (menu && menu.contains(e.target))) return;
435
- workdirMenuOpen.value = false;
436
- };
437
- let _workdirMenuKeyHandler = (e) => {
438
- if (e.key === 'Escape' && workdirMenuOpen.value) workdirMenuOpen.value = false;
439
- };
440
- document.addEventListener('click', _workdirMenuClickHandler);
441
- document.addEventListener('keydown', _workdirMenuKeyHandler);
442
-
443
- // ── Computed ──
444
- const hasInput = computed(() => !!(inputText.value.trim() || attachments.value.length > 0));
445
- const hasPendingQuestion = computed(() => messages.value.some(m => m.role === 'ask-question' && !m.answered));
446
- const canSend = computed(() =>
447
- status.value === 'Connected' && hasInput.value && !isCompacting.value && !hasPendingQuestion.value
448
- );
449
- const hasStreamingMessage = computed(() => messages.value.some(m => m.isStreaming));
450
-
451
- // ── Slash command menu ──
452
- const slashMenuVisible = computed(() => {
453
- if (slashMenuOpen.value) return true;
454
- const txt = inputText.value;
455
- return txt.startsWith('/') && !/\s/.test(txt.slice(1));
456
- });
457
- const filteredSlashCommands = computed(() => {
458
- if (slashMenuOpen.value && !inputText.value.startsWith('/')) return SLASH_COMMANDS;
459
- if (!inputText.value.startsWith('/')) return SLASH_COMMANDS;
460
- const txt = inputText.value.toLowerCase();
461
- return SLASH_COMMANDS.filter(c => c.command.startsWith(txt));
462
- });
463
- watch(filteredSlashCommands, () => { slashMenuIndex.value = 0; });
464
-
465
- // ── Auto-resize textarea ──
466
- let _autoResizeRaf = null;
467
- function autoResize() {
468
- if (_autoResizeRaf) return;
469
- _autoResizeRaf = requestAnimationFrame(() => {
470
- _autoResizeRaf = null;
471
- const ta = inputRef.value;
472
- if (ta) {
473
- ta.style.height = 'auto';
474
- ta.style.height = Math.min(ta.scrollHeight, 160) + 'px';
475
- }
476
- });
477
- }
478
-
479
- // ── Send message ──
480
- function sendMessage() {
481
- const text = inputText.value.trim();
482
-
483
- // Side question — /btw <question> (allowed even during compaction)
484
- if (text === '/btw' || text.startsWith('/btw ')) {
485
- if (status.value !== 'Connected') return;
486
- const question = text.startsWith('/btw ') ? text.slice(5).trim() : '';
487
- if (!question) return;
488
- btwState.value = { question, answer: '', done: false, error: null };
489
- btwPending.value = true;
490
- inputText.value = '';
491
- if (inputRef.value) inputRef.value.style.height = 'auto';
492
- wsSend({ type: 'btw_question', question, conversationId: currentConversationId.value, claudeSessionId: currentClaudeSessionId.value });
493
- return;
494
- }
495
-
496
- if (!canSend.value) return;
497
-
498
- const files = attachments.value.slice();
499
- inputText.value = '';
500
- if (inputRef.value) inputRef.value.style.height = 'auto';
501
-
502
- const msgAttachments = files.map(f => ({
503
- name: f.name, size: f.size, isImage: f.isImage, thumbUrl: f.thumbUrl,
504
- }));
505
-
506
- const payload = { type: 'chat', prompt: text || '(see attached files)' };
507
- if (currentConversationId.value) {
508
- payload.conversationId = currentConversationId.value;
509
- }
510
- if (needsResume.value && currentClaudeSessionId.value) {
511
- payload.resumeSessionId = currentClaudeSessionId.value;
512
- needsResume.value = false;
513
- }
514
- if (files.length > 0) {
515
- payload.files = files.map(f => ({
516
- name: f.name, mimeType: f.mimeType, data: f.data,
517
- }));
518
- }
519
-
520
- const userMsg = {
521
- id: streaming.nextId(), role: 'user',
522
- content: text || (files.length > 0 ? `[${files.length} file${files.length > 1 ? 's' : ''} attached]` : ''),
523
- attachments: msgAttachments.length > 0 ? msgAttachments : undefined,
524
- timestamp: new Date(),
525
- };
526
-
527
- if (isProcessing.value) {
528
- queuedMessages.value.push({ id: streaming.nextId(), content: userMsg.content, attachments: userMsg.attachments, payload });
529
- } else {
530
- userMsg.status = 'sent';
531
- messages.value.push(userMsg);
532
- isProcessing.value = true;
533
- if (currentConversationId.value) {
534
- processingConversations.value[currentConversationId.value] = true;
535
- }
536
- wsSend(payload);
537
- }
538
- scrollToBottom(true);
539
- attachments.value = [];
540
- }
541
-
542
- function cancelExecution() {
543
- if (!isProcessing.value) return;
544
- const cancelPayload = { type: 'cancel_execution' };
545
- if (currentConversationId.value) {
546
- cancelPayload.conversationId = currentConversationId.value;
547
- }
548
- wsSend(cancelPayload);
549
- }
550
-
551
- function dismissBtw() {
552
- btwState.value = null;
553
- btwPending.value = false;
554
- }
555
-
556
- function dequeueNext() {
557
- if (queuedMessages.value.length === 0) return;
558
- const queued = queuedMessages.value.shift();
559
- const userMsg = {
560
- id: queued.id, role: 'user', status: 'sent',
561
- content: queued.content, attachments: queued.attachments,
562
- timestamp: new Date(),
563
- };
564
- messages.value.push(userMsg);
565
- isProcessing.value = true;
566
- if (currentConversationId.value) {
567
- processingConversations.value[currentConversationId.value] = true;
568
- }
569
- wsSend(queued.payload);
570
- scrollToBottom(true);
571
- }
572
-
573
- function removeQueuedMessage(msgId) {
574
- const idx = queuedMessages.value.findIndex(m => m.id === msgId);
575
- if (idx !== -1) queuedMessages.value.splice(idx, 1);
576
- }
577
-
578
- // ── Plan mode ──
579
- function togglePlanMode() {
580
- if (isProcessing.value) return;
581
- const newMode = !planMode.value;
582
- pendingPlanMode.value = newMode ? 'enter' : 'exit';
583
- isProcessing.value = true;
584
- if (currentConversationId.value) {
585
- processingConversations.value[currentConversationId.value] = true;
586
- }
587
- const instruction = newMode ? 'Enter plan mode now.' : 'Exit plan mode now.';
588
- messages.value.push({
589
- id: streaming.nextId(), role: 'user', content: instruction,
590
- status: 'sent', timestamp: new Date(),
591
- });
592
- wsSend({ type: 'set_plan_mode', enabled: newMode, conversationId: currentConversationId.value, claudeSessionId: currentClaudeSessionId.value });
593
- nextTick(() => scrollToBottom());
594
- }
595
- function setPlanMode(enabled) {
596
- planMode.value = enabled;
597
- pendingPlanMode.value = null;
598
- }
599
-
600
- function selectSlashCommand(cmd) {
601
- slashMenuOpen.value = false;
602
- if (cmd.isPrefix) {
603
- inputText.value = cmd.command + ' ';
604
- nextTick(() => inputRef.value?.focus());
605
- } else {
606
- inputText.value = cmd.command;
607
- sendMessage();
608
- }
609
- }
610
-
611
- function openSlashMenu() {
612
- slashMenuOpen.value = !slashMenuOpen.value;
613
- slashMenuIndex.value = 0;
614
- }
615
-
616
- function _slashMenuClickOutside(e) {
617
- if (slashMenuOpen.value && !e.target.closest('.slash-btn') && !e.target.closest('.slash-menu')) {
618
- slashMenuOpen.value = false;
619
- }
620
- }
621
- document.addEventListener('click', _slashMenuClickOutside);
622
-
623
- function handleKeydown(e) {
624
- // Slash menu key handling (must come before btw overlay so Escape closes menu first)
625
- if (slashMenuVisible.value && filteredSlashCommands.value.length > 0 && !e.isComposing) {
626
- const len = filteredSlashCommands.value.length;
627
- if (e.key === 'ArrowDown') {
628
- e.preventDefault();
629
- slashMenuIndex.value = (slashMenuIndex.value + 1) % len;
630
- return;
631
- }
632
- if (e.key === 'ArrowUp') {
633
- e.preventDefault();
634
- slashMenuIndex.value = (slashMenuIndex.value - 1 + len) % len;
635
- return;
636
- }
637
- if (e.key === 'Enter') {
638
- e.preventDefault();
639
- selectSlashCommand(filteredSlashCommands.value[slashMenuIndex.value]);
640
- return;
641
- }
642
- if (e.key === 'Tab') {
643
- e.preventDefault();
644
- inputText.value = filteredSlashCommands.value[slashMenuIndex.value].command;
645
- return;
646
- }
647
- if (e.key === 'Escape') {
648
- e.preventDefault();
649
- slashMenuOpen.value = false;
650
- inputText.value = '';
651
- return;
652
- }
653
- }
654
- // Btw overlay dismiss (after slash menu so menu Escape takes priority)
655
- if (e.key === 'Escape' && btwState.value) {
656
- dismissBtw();
657
- e.preventDefault();
658
- return;
659
- }
660
-
661
- if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
662
- e.preventDefault();
663
- sendMessage();
664
- }
665
- }
666
-
667
- // ── Template adapter wrappers ──
668
- // These adapt the module function signatures to the template's call conventions.
669
- function _isPrevAssistant(idx) {
670
- return isPrevAssistant(visibleMessages.value, idx);
671
- }
672
-
673
- function _submitQuestionAnswer(msg) {
674
- submitQuestionAnswer(msg, wsSend);
675
- }
676
-
677
- // ── Watchers ──
678
- const messageCount = computed(() => messages.value.length);
679
- watch(messageCount, () => { nextTick(scheduleHighlight); });
680
-
681
- watch(hostname, (name) => {
682
- document.title = name ? `${name} — AgentLink` : 'AgentLink';
683
- // Restore sidebar collapsed states from localStorage
684
- const key = _sidebarCollapseKey();
685
- if (key) {
686
- try {
687
- const saved = JSON.parse(localStorage.getItem(key) || '{}');
688
- if (saved.chats !== undefined) chatsCollapsed.value = saved.chats;
689
- if (saved.teams !== undefined) teamsCollapsed.value = saved.teams;
690
- if (saved.loops !== undefined) loopsCollapsed.value = saved.loops;
691
- } catch (_) { /* ignore */ }
692
- }
693
- });
694
-
695
- // Persist sidebar collapsed states to localStorage
696
- const _saveSidebarCollapsed = () => {
697
- const key = _sidebarCollapseKey();
698
- if (key) {
699
- localStorage.setItem(key, JSON.stringify({
700
- chats: chatsCollapsed.value,
701
- teams: teamsCollapsed.value,
702
- loops: loopsCollapsed.value,
703
- }));
704
- }
705
- };
706
- watch(chatsCollapsed, _saveSidebarCollapsed);
707
- watch(teamsCollapsed, _saveSidebarCollapsed);
708
- watch(loopsCollapsed, _saveSidebarCollapsed);
709
-
710
- watch(team.teamsList, () => { loadingTeams.value = false; });
711
- watch(loop.loopsList, () => { loadingLoops.value = false; });
712
-
713
- // ── Lifecycle ──
714
- function _onVisibilityChange() {
715
- if (!document.hidden) {
716
- nextTick(() => scrollToBottom(true));
717
- }
718
- }
719
-
720
- onMounted(() => {
721
- connect(scheduleHighlight);
722
- document.addEventListener('visibilitychange', _onVisibilityChange);
723
- });
724
- onUnmounted(() => {
725
- closeWs(); streaming.cleanup(); cleanupScroll(); cleanupHighlight();
726
- window.removeEventListener('resize', _resizeHandler);
727
- document.removeEventListener('click', _workdirMenuClickHandler);
728
- document.removeEventListener('click', _slashMenuClickOutside);
729
- document.removeEventListener('keydown', _workdirMenuKeyHandler);
730
- document.removeEventListener('visibilitychange', _onVisibilityChange);
731
- });
732
-
733
- return {
734
- status, agentName, hostname, workDir, sessionId, error,
735
- serverVersion, agentVersion, latency,
736
- messages, visibleMessages, hasMoreMessages, loadMoreMessages,
737
- inputText, isProcessing, isCompacting, canSend, hasInput, hasStreamingMessage, inputRef, queuedMessages, usageStats,
738
- slashMenuVisible, filteredSlashCommands, slashMenuIndex, slashMenuOpen, selectSlashCommand, openSlashMenu,
739
- sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
740
- // Plan mode
741
- planMode, pendingPlanMode, togglePlanMode,
742
- // Side question (/btw)
743
- btwState, btwPending, dismissBtw, renderMarkdown,
744
- getRenderedContent, copyMessage, toggleTool,
745
- isPrevAssistant: _isPrevAssistant,
746
- toggleContextSummary, formatTimestamp, formatUsage: (u) => formatUsage(u, t),
747
- getToolIcon, getToolSummary: (msg) => getToolSummary(msg, t), isEditTool, getEditDiffHtml: (msg) => getEditDiffHtml(msg, t), getFormattedToolInput: (msg) => getFormattedToolInput(msg, t), autoResize,
748
- // AskUserQuestion
749
- selectQuestionOption,
750
- submitQuestionAnswer: _submitQuestionAnswer,
751
- hasQuestionAnswer, getQuestionResponseSummary,
752
- // Theme
753
- theme, toggleTheme,
754
- // i18n
755
- t, locale, toggleLocale, localeLabel, displayStatus,
756
- // Sidebar
757
- sidebarOpen, historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
758
- toggleSidebar: sidebar.toggleSidebar,
759
- resumeSession: sidebar.resumeSession,
760
- newConversation: sidebar.newConversation,
761
- requestSessionList: sidebar.requestSessionList,
762
- formatRelativeTime: (ts) => formatRelativeTime(ts, t),
763
- groupedSessions: sidebar.groupedSessions,
764
- isSessionProcessing: sidebar.isSessionProcessing,
765
- processingConversations,
766
- // Folder picker
767
- folderPickerOpen, folderPickerPath, folderPickerEntries,
768
- folderPickerLoading, folderPickerSelected,
769
- openFolderPicker: sidebar.openFolderPicker,
770
- folderPickerNavigateUp: sidebar.folderPickerNavigateUp,
771
- folderPickerSelectItem: sidebar.folderPickerSelectItem,
772
- folderPickerEnter: sidebar.folderPickerEnter,
773
- folderPickerGoToPath: sidebar.folderPickerGoToPath,
774
- confirmFolderPicker: sidebar.confirmFolderPicker,
775
- // Delete session
776
- deleteConfirmOpen, deleteConfirmTitle,
777
- deleteSession: sidebar.deleteSession,
778
- confirmDeleteSession: sidebar.confirmDeleteSession,
779
- cancelDeleteSession: sidebar.cancelDeleteSession,
780
- // Rename session
781
- renamingSessionId, renameText,
782
- startRename: sidebar.startRename,
783
- confirmRename: sidebar.confirmRename,
784
- cancelRename: sidebar.cancelRename,
785
- // Team rename/delete
786
- renamingTeamId, renameTeamText,
787
- deleteTeamConfirmOpen, deleteTeamConfirmTitle, pendingDeleteTeamId,
788
- startTeamRename(tm) {
789
- renamingTeamId.value = tm.teamId;
790
- renameTeamText.value = tm.title || '';
791
- },
792
- confirmTeamRename() {
793
- const tid = renamingTeamId.value;
794
- const title = renameTeamText.value.trim();
795
- if (!tid || !title) { renamingTeamId.value = null; renameTeamText.value = ''; return; }
796
- team.renameTeamById(tid, title);
797
- renamingTeamId.value = null;
798
- renameTeamText.value = '';
799
- },
800
- cancelTeamRename() {
801
- renamingTeamId.value = null;
802
- renameTeamText.value = '';
803
- },
804
- requestDeleteTeam(tm) {
805
- pendingDeleteTeamId.value = tm.teamId;
806
- deleteTeamConfirmTitle.value = tm.title || tm.teamId.slice(0, 8);
807
- deleteTeamConfirmOpen.value = true;
808
- },
809
- confirmDeleteTeam() {
810
- if (!pendingDeleteTeamId.value) return;
811
- team.deleteTeamById(pendingDeleteTeamId.value);
812
- deleteTeamConfirmOpen.value = false;
813
- pendingDeleteTeamId.value = null;
814
- },
815
- cancelDeleteTeam() {
816
- deleteTeamConfirmOpen.value = false;
817
- pendingDeleteTeamId.value = null;
818
- },
819
- // Working directory history
820
- filteredWorkdirHistory: sidebar.filteredWorkdirHistory,
821
- switchToWorkdir: sidebar.switchToWorkdir,
822
- removeFromWorkdirHistory: sidebar.removeFromWorkdirHistory,
823
- workdirSwitching,
824
- // Authentication
825
- authRequired, authPassword, authError, authAttempts, authLocked,
826
- submitPassword,
827
- // File attachments
828
- attachments, fileInputRef, dragOver,
829
- triggerFileInput: fileAttach.triggerFileInput,
830
- handleFileSelect: fileAttach.handleFileSelect,
831
- removeAttachment: fileAttach.removeAttachment,
832
- formatFileSize,
833
- handleDragOver: fileAttach.handleDragOver,
834
- handleDragLeave: fileAttach.handleDragLeave,
835
- handleDrop: fileAttach.handleDrop,
836
- handlePaste: fileAttach.handlePaste,
837
- // File browser
838
- filePanelOpen, filePanelWidth, fileTreeRoot, fileTreeLoading, fileContextMenu,
839
- sidebarView, isMobile, fileBrowser,
840
- flattenedTree: fileBrowser.flattenedTree,
841
- // File preview
842
- previewPanelOpen, previewPanelWidth, previewFile, previewLoading, previewMarkdownRendered, filePreview,
843
- workdirMenuOpen,
844
- teamsCollapsed, chatsCollapsed, loopsCollapsed, loadingTeams, loadingLoops,
845
- toggleWorkdirMenu() { workdirMenuOpen.value = !workdirMenuOpen.value; },
846
- workdirMenuBrowse() {
847
- workdirMenuOpen.value = false;
848
- if (isMobile.value) { sidebarView.value = 'files'; fileBrowser.openPanel(); }
849
- else { memoryPanelOpen.value = false; fileBrowser.togglePanel(); }
850
- },
851
- workdirMenuChangeDir() {
852
- workdirMenuOpen.value = false;
853
- sidebar.openFolderPicker();
854
- },
855
- workdirMenuCopyPath() {
856
- workdirMenuOpen.value = false;
857
- fileBrowser.copyToClipboard(workDir.value);
858
- },
859
- // Memory management
860
- memoryPanelOpen, memoryFiles, memoryDir, memoryLoading,
861
- memoryEditing, memoryEditContent, memorySaving, isMemoryPreview,
862
- workdirMenuMemory() {
863
- workdirMenuOpen.value = false;
864
- if (isMobile.value) {
865
- sidebarView.value = 'memory';
866
- } else {
867
- memoryPanelOpen.value = !memoryPanelOpen.value;
868
- if (memoryPanelOpen.value) filePanelOpen.value = false;
869
- }
870
- if (!memoryFiles.value.length) {
871
- memoryLoading.value = true;
872
- wsSend({ type: 'list_memory' });
873
- }
874
- },
875
- refreshMemory() {
876
- memoryLoading.value = true;
877
- wsSend({ type: 'list_memory' });
878
- },
879
- openMemoryFile(file) {
880
- memoryEditing.value = false;
881
- memoryEditContent.value = '';
882
- if (memoryDir.value) {
883
- const sep = memoryDir.value.includes('\\') ? '\\' : '/';
884
- filePreview.openPreview(memoryDir.value + sep + file.name);
885
- }
886
- if (isMobile.value) sidebarView.value = 'preview';
887
- },
888
- startMemoryEdit() {
889
- memoryEditing.value = true;
890
- memoryEditContent.value = previewFile.value?.content || '';
891
- },
892
- cancelMemoryEdit() {
893
- if (memoryEditContent.value !== (previewFile.value?.content || '')) {
894
- if (!confirm(t('memory.discardChanges'))) return;
895
- }
896
- memoryEditing.value = false;
897
- memoryEditContent.value = '';
898
- },
899
- saveMemoryEdit() {
900
- if (!previewFile.value) return;
901
- memorySaving.value = true;
902
- wsSend({
903
- type: 'update_memory',
904
- filename: previewFile.value.fileName,
905
- content: memoryEditContent.value,
906
- });
907
- },
908
- deleteMemoryFile(file) {
909
- if (!confirm(t('memory.deleteConfirm', { name: file.name }))) return;
910
- wsSend({ type: 'delete_memory', filename: file.name });
911
- },
912
- // Team mode
913
- team,
914
- teamState: team.teamState,
915
- viewMode: team.viewMode,
916
- activeAgentView: team.activeAgentView,
917
- historicalTeam: team.historicalTeam,
918
- teamsList: team.teamsList,
919
- isTeamActive: team.isTeamActive,
920
- isTeamRunning: team.isTeamRunning,
921
- displayTeam: team.displayTeam,
922
- pendingTasks: team.pendingTasks,
923
- activeTasks: team.activeTasks,
924
- doneTasks: team.doneTasks,
925
- failedTasks: team.failedTasks,
926
- launchTeam: team.launchTeam,
927
- dissolveTeam: team.dissolveTeam,
928
- viewAgent: team.viewAgent,
929
- viewDashboard: team.viewDashboard,
930
- viewHistoricalTeam: team.viewHistoricalTeam,
931
- requestTeamsList() {
932
- team.requestTeamsList();
933
- },
934
- deleteTeamById: team.deleteTeamById,
935
- renameTeamById: team.renameTeamById,
936
- getAgentColor: team.getAgentColor,
937
- findAgent: team.findAgent,
938
- getAgentMessages: team.getAgentMessages,
939
- backToChat: team.backToChat,
940
- newTeam: team.newTeam,
941
- teamInstruction,
942
- teamExamples,
943
- kanbanExpanded,
944
- instructionExpanded,
945
- selectedTemplate,
946
- editedLeadPrompt,
947
- leadPromptExpanded,
948
- TEMPLATES,
949
- TEMPLATE_KEYS,
950
- onTemplateChange(key) {
951
- selectedTemplate.value = key;
952
- editedLeadPrompt.value = TEMPLATES[key].leadPrompt;
953
- },
954
- resetLeadPrompt() {
955
- editedLeadPrompt.value = TEMPLATES[selectedTemplate.value].leadPrompt;
956
- },
957
- leadPromptPreview() {
958
- const text = editedLeadPrompt.value || '';
959
- return text.length > 80 ? text.slice(0, 80) + '...' : text;
960
- },
961
- launchTeamFromPanel() {
962
- const inst = teamInstruction.value.trim();
963
- if (!inst) return;
964
- const tplKey = selectedTemplate.value;
965
- const tpl = TEMPLATES[tplKey];
966
- const agents = tpl.agents;
967
- const leadPrompt = buildFullLeadPrompt(editedLeadPrompt.value, agents, inst);
968
- team.launchTeam(inst, leadPrompt, agents);
969
- teamInstruction.value = '';
970
- // Reset template state for next time
971
- selectedTemplate.value = 'custom';
972
- editedLeadPrompt.value = TEMPLATES.custom.leadPrompt;
973
- leadPromptExpanded.value = false;
974
- },
975
- formatTeamTime(ts) {
976
- if (!ts) return '';
977
- const d = new Date(ts);
978
- return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
979
- },
980
- getTaskAgent(task) {
981
- const assignee = task.assignee || task.assignedTo;
982
- if (!assignee) return null;
983
- return team.findAgent(assignee);
984
- },
985
- viewAgentWithHistory(agentId) {
986
- team.viewAgent(agentId);
987
- // For historical teams, request agent conversation history from server
988
- if (team.historicalTeam.value && team.historicalTeam.value.teamId) {
989
- team.requestAgentHistory(team.historicalTeam.value.teamId, agentId);
990
- }
991
- },
992
- feedAgentName(entry) {
993
- if (!entry.agentId) return null;
994
- const agent = team.findAgent(entry.agentId);
995
- if (!agent || !agent.name) return null;
996
- // Verify the content actually starts with this agent name
997
- if (entry.content && entry.content.startsWith(agent.name)) {
998
- return agent.name;
999
- }
1000
- return null;
1001
- },
1002
- feedContentRest(entry) {
1003
- const name = this.feedAgentName(entry);
1004
- if (name && entry.content && entry.content.startsWith(name)) {
1005
- return entry.content.slice(name.length);
1006
- }
1007
- return entry.content || '';
1008
- },
1009
- getLatestAgentActivity(agentId) {
1010
- // Find the latest feed entry for this agent
1011
- const t = team.displayTeam.value;
1012
- if (!t || !t.feed) return '';
1013
- for (let i = t.feed.length - 1; i >= 0; i--) {
1014
- const entry = t.feed[i];
1015
- if (entry.agentId === agentId && entry.type === 'tool_call') {
1016
- // Strip agent name prefix since it's already shown on the card
1017
- const agent = team.findAgent(agentId);
1018
- if (agent && agent.name && entry.content.startsWith(agent.name)) {
1019
- return entry.content.slice(agent.name.length).trimStart();
1020
- }
1021
- return entry.content;
1022
- }
1023
- }
1024
- return '';
1025
- },
1026
- // Loop mode
1027
- loop,
1028
- loopsList: loop.loopsList,
1029
- selectedLoop: loop.selectedLoop,
1030
- selectedExecution: loop.selectedExecution,
1031
- executionHistory: loop.executionHistory,
1032
- executionMessages: loop.executionMessages,
1033
- runningLoops: loop.runningLoops,
1034
- loadingExecutions: loop.loadingExecutions,
1035
- loadingExecution: loop.loadingExecution,
1036
- editingLoopId: loop.editingLoopId,
1037
- hasRunningLoop: loop.hasRunningLoop,
1038
- firstRunningLoop: loop.firstRunningLoop,
1039
- loopError: loop.loopError,
1040
- hasMoreExecutions: loop.hasMoreExecutions,
1041
- loadingMoreExecutions: loop.loadingMoreExecutions,
1042
- toggleLoop: loop.toggleLoop,
1043
- runNow: loop.runNow,
1044
- cancelLoopExecution: loop.cancelExecution,
1045
- viewLoopDetail: loop.viewLoopDetail,
1046
- viewExecution: loop.viewExecution,
1047
- backToLoopsList: loop.backToLoopsList,
1048
- backToLoopDetail: loop.backToLoopDetail,
1049
- LOOP_TEMPLATES, LOOP_TEMPLATE_KEYS,
1050
- buildCronExpression, formatSchedule,
1051
- // Loop form state
1052
- loopName, loopPrompt, loopScheduleType,
1053
- loopScheduleHour, loopScheduleMinute, loopScheduleDayOfWeek,
1054
- loopCronExpr, loopSelectedTemplate,
1055
- loopDeleteConfirmOpen, loopDeleteConfirmId, loopDeleteConfirmName,
1056
- renamingLoopId, renameLoopText,
1057
- startLoopRename(l) {
1058
- renamingLoopId.value = l.id;
1059
- renameLoopText.value = l.name || '';
1060
- },
1061
- confirmLoopRename() {
1062
- const lid = renamingLoopId.value;
1063
- const name = renameLoopText.value.trim();
1064
- if (!lid || !name) { renamingLoopId.value = null; renameLoopText.value = ''; return; }
1065
- loop.updateExistingLoop(lid, { name });
1066
- renamingLoopId.value = null;
1067
- renameLoopText.value = '';
1068
- },
1069
- cancelLoopRename() {
1070
- renamingLoopId.value = null;
1071
- renameLoopText.value = '';
1072
- },
1073
- requestLoopsList() {
1074
- loop.requestLoopsList();
1075
- },
1076
- newLoop() {
1077
- loop.backToLoopsList();
1078
- loop.editingLoopId.value = null;
1079
- loopSelectedTemplate.value = null;
1080
- loopName.value = '';
1081
- loopPrompt.value = '';
1082
- loopScheduleType.value = 'daily';
1083
- loopScheduleHour.value = 9;
1084
- loopScheduleMinute.value = 0;
1085
- loopScheduleDayOfWeek.value = 1;
1086
- loopCronExpr.value = '0 9 * * *';
1087
- team.viewMode.value = 'loop';
1088
- },
1089
- viewLoop(loopId) {
1090
- loop.viewLoopDetail(loopId);
1091
- team.viewMode.value = 'loop';
1092
- },
1093
- selectLoopTemplate(key) {
1094
- loopSelectedTemplate.value = key;
1095
- const tpl = LOOP_TEMPLATES[key];
1096
- if (!tpl) return;
1097
- loopName.value = tpl.name || '';
1098
- loopPrompt.value = tpl.prompt || '';
1099
- loopScheduleType.value = tpl.scheduleType || 'daily';
1100
- const cfg = tpl.scheduleConfig || {};
1101
- loopScheduleHour.value = cfg.hour ?? 9;
1102
- loopScheduleMinute.value = cfg.minute ?? 0;
1103
- loopScheduleDayOfWeek.value = cfg.dayOfWeek ?? 1;
1104
- loopCronExpr.value = buildCronExpression(tpl.scheduleType || 'daily', cfg);
1105
- },
1106
- resetLoopForm() {
1107
- loopSelectedTemplate.value = null;
1108
- loopName.value = '';
1109
- loopPrompt.value = '';
1110
- loopScheduleType.value = 'daily';
1111
- loopScheduleHour.value = 9;
1112
- loopScheduleMinute.value = 0;
1113
- loopScheduleDayOfWeek.value = 1;
1114
- loopCronExpr.value = '0 9 * * *';
1115
- loop.editingLoopId.value = null;
1116
- },
1117
- createLoopFromPanel() {
1118
- const name = loopName.value.trim();
1119
- const prompt = loopPrompt.value.trim();
1120
- if (!name || !prompt) return;
1121
- loop.clearLoopError();
1122
- const schedCfg = { hour: loopScheduleHour.value, minute: loopScheduleMinute.value };
1123
- if (loopScheduleType.value === 'weekly') schedCfg.dayOfWeek = loopScheduleDayOfWeek.value;
1124
- if (loopScheduleType.value === 'cron') schedCfg.cronExpression = loopCronExpr.value;
1125
- const schedule = loopScheduleType.value === 'manual' ? ''
1126
- : loopScheduleType.value === 'cron' ? loopCronExpr.value
1127
- : buildCronExpression(loopScheduleType.value, schedCfg);
1128
- loop.createNewLoop({ name, prompt, schedule, scheduleType: loopScheduleType.value, scheduleConfig: schedCfg });
1129
- // Reset form
1130
- loopSelectedTemplate.value = null;
1131
- loopName.value = '';
1132
- loopPrompt.value = '';
1133
- loopScheduleType.value = 'daily';
1134
- loopScheduleHour.value = 9;
1135
- loopScheduleMinute.value = 0;
1136
- loopScheduleDayOfWeek.value = 1;
1137
- loopCronExpr.value = '0 9 * * *';
1138
- },
1139
- startEditingLoop(l) {
1140
- loop.editingLoopId.value = l.id;
1141
- loopName.value = l.name || '';
1142
- loopPrompt.value = l.prompt || '';
1143
- loopScheduleType.value = l.scheduleType || 'daily';
1144
- const cfg = l.scheduleConfig || {};
1145
- loopScheduleHour.value = cfg.hour ?? 9;
1146
- loopScheduleMinute.value = cfg.minute ?? 0;
1147
- loopScheduleDayOfWeek.value = cfg.dayOfWeek ?? 1;
1148
- loopCronExpr.value = l.schedule || buildCronExpression(l.scheduleType || 'daily', cfg);
1149
- },
1150
- saveLoopEdits() {
1151
- const lid = loop.editingLoopId.value;
1152
- if (!lid) return;
1153
- const name = loopName.value.trim();
1154
- const prompt = loopPrompt.value.trim();
1155
- if (!name || !prompt) return;
1156
- loop.clearLoopError();
1157
- const schedCfg = { hour: loopScheduleHour.value, minute: loopScheduleMinute.value };
1158
- if (loopScheduleType.value === 'weekly') schedCfg.dayOfWeek = loopScheduleDayOfWeek.value;
1159
- if (loopScheduleType.value === 'cron') schedCfg.cronExpression = loopCronExpr.value;
1160
- const schedule = loopScheduleType.value === 'manual' ? ''
1161
- : loopScheduleType.value === 'cron' ? loopCronExpr.value
1162
- : buildCronExpression(loopScheduleType.value, schedCfg);
1163
- loop.updateExistingLoop(lid, { name, prompt, schedule, scheduleType: loopScheduleType.value, scheduleConfig: schedCfg });
1164
- loop.editingLoopId.value = null;
1165
- loopName.value = '';
1166
- loopPrompt.value = '';
1167
- },
1168
- cancelEditingLoop() {
1169
- loop.editingLoopId.value = null;
1170
- loopName.value = '';
1171
- loopPrompt.value = '';
1172
- loopScheduleType.value = 'daily';
1173
- loopScheduleHour.value = 9;
1174
- loopScheduleMinute.value = 0;
1175
- },
1176
- requestDeleteLoop(l) {
1177
- loopDeleteConfirmId.value = l.id;
1178
- loopDeleteConfirmName.value = l.name || l.id.slice(0, 8);
1179
- loopDeleteConfirmOpen.value = true;
1180
- },
1181
- confirmDeleteLoop() {
1182
- if (!loopDeleteConfirmId.value) return;
1183
- loop.deleteExistingLoop(loopDeleteConfirmId.value);
1184
- loopDeleteConfirmOpen.value = false;
1185
- loopDeleteConfirmId.value = null;
1186
- },
1187
- cancelDeleteLoop() {
1188
- loopDeleteConfirmOpen.value = false;
1189
- loopDeleteConfirmId.value = null;
1190
- },
1191
- loadMoreExecutions() {
1192
- loop.loadMoreExecutions();
1193
- },
1194
- clearLoopError() {
1195
- loop.clearLoopError();
1196
- },
1197
- loopScheduleDisplay(l) {
1198
- return formatSchedule(l.scheduleType, l.scheduleConfig || {}, l.schedule);
1199
- },
1200
- loopLastRunDisplay(l) {
1201
- if (!l.lastExecution) return '';
1202
- const exec = l.lastExecution;
1203
- const ago = formatRelativeTime(exec.startedAt, t);
1204
- const icon = exec.status === 'success' ? 'OK' : exec.status === 'error' ? 'ERR' : exec.status;
1205
- return ago + ' ' + icon;
1206
- },
1207
- formatExecTime(ts) {
1208
- if (!ts) return '';
1209
- const d = new Date(ts);
1210
- return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ', ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1211
- },
1212
- formatDuration(ms) {
1213
- if (!ms && ms !== 0) return '';
1214
- const totalSecs = Math.floor(ms / 1000);
1215
- if (totalSecs < 60) return totalSecs + 's';
1216
- const m = Math.floor(totalSecs / 60);
1217
- const s = totalSecs % 60;
1218
- if (m < 60) return m + 'm ' + String(s).padStart(2, '0') + 's';
1219
- const h = Math.floor(m / 60);
1220
- const rm = m % 60;
1221
- return h + 'h ' + String(rm).padStart(2, '0') + 'm';
1222
- },
1223
- isLoopRunning(loopId) {
1224
- return !!loop.runningLoops.value[loopId];
1225
- },
1226
- padTwo(n) {
1227
- return String(n).padStart(2, '0');
1228
- },
1229
- };
1230
- },
1231
- template: `
1232
- <div class="layout">
1233
- <header class="top-bar">
1234
- <div class="top-bar-left">
1235
- <button class="sidebar-toggle" @click="toggleSidebar" :title="t('header.toggleSidebar')">
1236
- <svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
1237
- </button>
1238
- <h1>AgentLink</h1>
1239
- </div>
1240
- <div class="top-bar-info">
1241
- <span :class="['badge', status.toLowerCase()]">{{ displayStatus }}</span>
1242
- <span v-if="latency !== null && status === 'Connected'" class="latency" :class="{ good: latency < 100, ok: latency >= 100 && latency < 500, bad: latency >= 500 }">{{ latency }}ms</span>
1243
- <span v-if="agentName" class="agent-label">{{ agentName }}</span>
1244
- <div class="team-mode-toggle">
1245
- <button :class="['team-mode-btn', { active: viewMode === 'chat' }]" @click="viewMode = 'chat'">{{ t('header.chat') }}</button>
1246
- <button :class="['team-mode-btn', { active: viewMode === 'team' }]" @click="viewMode = 'team'">{{ t('header.team') }}</button>
1247
- <button :class="['team-mode-btn', { active: viewMode === 'loop' }]" @click="viewMode = 'loop'">{{ t('header.loop') }}</button>
1248
- </div>
1249
- <select class="team-mode-select" :value="viewMode" @change="viewMode = $event.target.value">
1250
- <option value="chat">{{ t('header.chat') }}</option>
1251
- <option value="team">{{ t('header.team') }}</option>
1252
- <option value="loop">{{ t('header.loop') }}</option>
1253
- </select>
1254
- <button class="theme-toggle" @click="toggleTheme" :title="theme === 'dark' ? t('header.lightMode') : t('header.darkMode')">
1255
- <svg v-if="theme === 'dark'" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>
1256
- <svg v-else viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/></svg>
1257
- </button>
1258
- <button class="theme-toggle" @click="toggleLocale" :title="localeLabel">{{ localeLabel }}</button>
1259
- </div>
1260
- </header>
1261
-
1262
- <div v-if="status === 'No Session' || (status !== 'Connected' && status !== 'Connecting...' && status !== 'Reconnecting...' && messages.length === 0)" class="center-card">
1263
- <div class="status-card">
1264
- <p class="status">
1265
- <span class="label">{{ t('statusCard.status') }}</span>
1266
- <span :class="['badge', status.toLowerCase()]">{{ displayStatus }}</span>
1267
- </p>
1268
- <p v-if="agentName" class="info"><span class="label">{{ t('statusCard.agent') }}</span> {{ agentName }}</p>
1269
- <p v-if="workDir" class="info"><span class="label">{{ t('statusCard.directory') }}</span> {{ workDir }}</p>
1270
- <p v-if="sessionId" class="info muted"><span class="label">{{ t('statusCard.session') }}</span> {{ sessionId }}</p>
1271
- <p v-if="error" class="error-msg">{{ error }}</p>
1272
- </div>
1273
- </div>
1274
-
1275
- <div v-else class="main-body">
1276
- <!-- Sidebar backdrop (mobile) -->
1277
- <div v-if="sidebarOpen" class="sidebar-backdrop" @click="toggleSidebar(); sidebarView = 'sessions'"></div>
1278
- <!-- Sidebar -->
1279
- <aside v-if="sidebarOpen" class="sidebar">
1280
- <!-- Mobile: file browser view -->
1281
- <div v-if="isMobile && sidebarView === 'files'" class="file-panel-mobile">
1282
- <div class="file-panel-mobile-header">
1283
- <button class="file-panel-mobile-back" @click="sidebarView = 'sessions'">
1284
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
1285
- {{ t('sidebar.sessions') }}
1286
- </button>
1287
- <button class="file-panel-btn" @click="fileBrowser.refreshTree()" :title="t('sidebar.refresh')">
1288
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1289
- </button>
1290
- </div>
1291
- <div class="file-panel-breadcrumb" :title="workDir">{{ workDir }}</div>
1292
- <div v-if="fileTreeLoading" class="file-panel-loading">{{ t('filePanel.loading') }}</div>
1293
- <div v-else-if="!fileTreeRoot || !fileTreeRoot.children || fileTreeRoot.children.length === 0" class="file-panel-empty">
1294
- {{ t('filePanel.noFiles') }}
1295
- </div>
1296
- <div v-else class="file-tree">
1297
- <template v-for="item in flattenedTree" :key="item.node.path">
1298
- <div
1299
- class="file-tree-item"
1300
- :class="{ folder: item.node.type === 'directory' }"
1301
- :style="{ paddingLeft: (item.depth * 16 + 8) + 'px' }"
1302
- @click="item.node.type === 'directory' ? fileBrowser.toggleFolder(item.node) : filePreview.openPreview(item.node.path)"
1303
- @contextmenu.prevent="item.node.type !== 'directory' ? fileBrowser.onFileClick($event, item.node) : null"
1304
- >
1305
- <span v-if="item.node.type === 'directory'" class="file-tree-arrow" :class="{ expanded: item.node.expanded }">&#9654;</span>
1306
- <span v-else class="file-tree-file-icon">
1307
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
1308
- </span>
1309
- <span class="file-tree-name" :title="item.node.path">{{ item.node.name }}</span>
1310
- <span v-if="item.node.loading" class="file-tree-spinner"></span>
1311
- </div>
1312
- <div v-if="item.node.type === 'directory' && item.node.expanded && item.node.children && item.node.children.length === 0 && !item.node.loading" class="file-tree-empty" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ t('filePanel.empty') }}</div>
1313
- <div v-if="item.node.error" class="file-tree-error" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ item.node.error }}</div>
1314
- </template>
1315
- </div>
1316
- </div>
1317
-
1318
- <!-- Mobile: file preview view -->
1319
- <div v-else-if="isMobile && sidebarView === 'preview'" class="file-preview-mobile">
1320
- <div class="file-preview-mobile-header">
1321
- <button class="file-panel-mobile-back" @click="filePreview.closePreview(); memoryEditing = false">
1322
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
1323
- {{ t('sidebar.files') }}
1324
- </button>
1325
- <div class="preview-header-actions">
1326
- <button v-if="isMemoryPreview && previewFile && !memoryEditing"
1327
- class="preview-edit-btn" @click="startMemoryEdit()" :title="t('memory.edit')">
1328
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 0 0 0-1.42l-2.34-2.34a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.82z"/></svg>
1329
- {{ t('memory.edit') }}
1330
- </button>
1331
- <span v-if="memoryEditing" class="preview-edit-label">{{ t('memory.editing') }}</span>
1332
- <button v-if="memoryEditing" class="memory-header-cancel" @click="cancelMemoryEdit()">{{ t('loop.cancel') }}</button>
1333
- <button v-if="memoryEditing" class="memory-header-save" @click="saveMemoryEdit()" :disabled="memorySaving">
1334
- {{ memorySaving ? t('memory.saving') : t('memory.save') }}
1335
- </button>
1336
- <button v-if="previewFile?.content && !memoryEditing && filePreview.isMarkdownFile(previewFile.fileName)"
1337
- class="preview-md-toggle" :class="{ active: previewMarkdownRendered }"
1338
- @click="previewMarkdownRendered = !previewMarkdownRendered"
1339
- :title="previewMarkdownRendered ? t('preview.showSource') : t('preview.renderMarkdown')">
1340
- <svg viewBox="0 0 16 16" width="14" height="14"><path fill="currentColor" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"/></svg>
1341
- </button>
1342
- <span v-if="previewFile && !memoryEditing" class="file-preview-mobile-size">
1343
- {{ filePreview.formatFileSize(previewFile.totalSize) }}
1344
- </span>
1345
- <button v-if="previewFile && !memoryEditing" class="preview-refresh-btn" @click="filePreview.refreshPreview()" :title="t('sidebar.refresh')">
1346
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1347
- </button>
1348
- </div>
1349
- </div>
1350
- <div class="file-preview-mobile-filename" :title="previewFile?.filePath">
1351
- {{ previewFile?.fileName || t('preview.preview') }}
1352
- </div>
1353
- <div class="preview-panel-body">
1354
- <div v-if="memoryEditing" class="memory-edit-container">
1355
- <textarea class="memory-edit-textarea" v-model="memoryEditContent"></textarea>
1356
- </div>
1357
- <div v-else-if="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
1358
- <div v-else-if="previewFile?.error" class="preview-error">
1359
- {{ previewFile.error }}
1360
- </div>
1361
- <div v-else-if="previewFile?.encoding === 'base64' && previewFile?.content"
1362
- class="preview-image-container">
1363
- <img :src="'data:' + previewFile.mimeType + ';base64,' + previewFile.content"
1364
- :alt="previewFile.fileName" class="preview-image" />
1365
- </div>
1366
- <div v-else-if="previewFile?.content && previewMarkdownRendered && filePreview.isMarkdownFile(previewFile.fileName)"
1367
- class="preview-markdown-rendered markdown-body" v-html="filePreview.renderedMarkdownHtml(previewFile.content)">
1368
- </div>
1369
- <div v-else-if="previewFile?.content" class="preview-text-container">
1370
- <pre class="preview-code"><code v-html="filePreview.highlightCode(previewFile.content, previewFile.fileName)"></code></pre>
1371
- <div v-if="previewFile.truncated" class="preview-truncated-notice">
1372
- {{ t('preview.fileTruncated', { size: filePreview.formatFileSize(previewFile.totalSize) }) }}
1373
- </div>
1374
- </div>
1375
- <div v-else-if="previewFile && !previewFile.content && !previewFile.error" class="preview-binary-info">
1376
- <p>{{ t('preview.binaryFile') }} — {{ previewFile.mimeType }}</p>
1377
- <p>{{ filePreview.formatFileSize(previewFile.totalSize) }}</p>
1378
- </div>
1379
- </div>
1380
- </div>
1381
-
1382
- <!-- Mobile: memory view -->
1383
- <div v-else-if="isMobile && sidebarView === 'memory'" class="file-panel-mobile">
1384
- <div class="file-panel-mobile-header">
1385
- <button class="file-panel-mobile-back" @click="sidebarView = 'sessions'">
1386
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
1387
- {{ t('sidebar.sessions') }}
1388
- </button>
1389
- <button class="file-panel-btn" @click="refreshMemory()" :title="t('sidebar.refresh')">
1390
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1391
- </button>
1392
- </div>
1393
- <div v-if="memoryLoading" class="file-panel-loading">{{ t('memory.loading') }}</div>
1394
- <div v-else-if="memoryFiles.length === 0" class="memory-empty">
1395
- <p>{{ t('memory.noFiles') }}</p>
1396
- <p class="memory-empty-hint">{{ t('memory.noFilesHint') }}</p>
1397
- </div>
1398
- <div v-else class="file-tree">
1399
- <div v-for="file in memoryFiles" :key="file.name"
1400
- class="file-tree-item" @click="openMemoryFile(file)">
1401
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zM6 20V4h5v5c0 .55.45 1 1 1h5v10H6z"/></svg>
1402
- <span class="file-tree-name">{{ file.name }}</span>
1403
- </div>
1404
- </div>
1405
- </div>
1406
-
1407
- <!-- Normal sidebar content (sessions view) -->
1408
- <template v-else>
1409
- <div class="sidebar-section">
1410
- <div class="sidebar-workdir">
1411
- <div v-if="hostname" class="sidebar-hostname">
1412
- <svg class="sidebar-hostname-icon" viewBox="0 0 16 16" width="14" height="14"><path fill="currentColor" d="M3.5 2A1.5 1.5 0 0 0 2 3.5v5A1.5 1.5 0 0 0 3.5 10h9A1.5 1.5 0 0 0 14 8.5v-5A1.5 1.5 0 0 0 12.5 2h-9zM.5 3.5A3 3 0 0 1 3.5.5h9A3 3 0 0 1 15.5 3.5v5a3 3 0 0 1-3 3h-9a3 3 0 0 1-3-3v-5zM5 13.25a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75zM3.25 15a.75.75 0 0 0 0 1.5h9.5a.75.75 0 0 0 0-1.5h-9.5z"/></svg>
1413
- <span>{{ hostname }}</span>
1414
- </div>
1415
- <div class="sidebar-workdir-header">
1416
- <div class="sidebar-workdir-label">{{ t('sidebar.workingDirectory') }}</div>
1417
- </div>
1418
- <div class="sidebar-workdir-path-row" @click.stop="toggleWorkdirMenu()">
1419
- <div class="sidebar-workdir-path" :title="workDir">{{ workDir }}</div>
1420
- <svg class="sidebar-workdir-chevron" :class="{ open: workdirMenuOpen }" viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
1421
- </div>
1422
- <div v-if="workdirMenuOpen" class="workdir-menu">
1423
- <div class="workdir-menu-item" @click.stop="workdirMenuBrowse()">
1424
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13h8v2H8v-2z"/></svg>
1425
- <span>{{ t('sidebar.browseFiles') }}</span>
1426
- </div>
1427
- <div class="workdir-menu-item" @click.stop="workdirMenuChangeDir()">
1428
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
1429
- <span>{{ t('sidebar.changeDirectory') }}</span>
1430
- </div>
1431
- <div class="workdir-menu-item" @click.stop="workdirMenuCopyPath()">
1432
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
1433
- <span>{{ t('sidebar.copyPath') }}</span>
1434
- </div>
1435
- <div class="workdir-menu-item" @click.stop="workdirMenuMemory()">
1436
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zM6 20V4h5v5c0 .55.45 1 1 1h5v10H6z"/></svg>
1437
- <span>{{ t('sidebar.memory') }}</span>
1438
- </div>
1439
- </div>
1440
- <div v-if="filteredWorkdirHistory.length > 0" class="workdir-history">
1441
- <div class="workdir-history-label">{{ t('sidebar.recentDirectories') }}</div>
1442
- <div class="workdir-history-list">
1443
- <div
1444
- v-for="path in filteredWorkdirHistory" :key="path"
1445
- class="workdir-history-item"
1446
- @click="switchToWorkdir(path)"
1447
- :title="path"
1448
- >
1449
- <span class="workdir-history-path">{{ path }}</span>
1450
- <button class="workdir-history-delete" @click.stop="removeFromWorkdirHistory(path)" :title="t('sidebar.removeFromHistory')">
1451
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
1452
- </button>
1453
- </div>
1454
- </div>
1455
- </div>
1456
- </div>
1457
- </div>
1458
-
1459
- <!-- Chat History section -->
1460
- <div class="sidebar-section sidebar-sessions" :style="{ flex: chatsCollapsed ? '0 0 auto' : '1 1 0', minHeight: chatsCollapsed ? 'auto' : '0' }">
1461
- <div class="sidebar-section-header" @click="chatsCollapsed = !chatsCollapsed" style="cursor: pointer;">
1462
- <span>{{ t('sidebar.chatHistory') }}</span>
1463
- <span class="sidebar-section-header-actions">
1464
- <button class="sidebar-refresh-btn" @click.stop="requestSessionList" :title="t('sidebar.refresh')" :disabled="loadingSessions">
1465
- <svg :class="{ spinning: loadingSessions }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1466
- </button>
1467
- <button class="sidebar-collapse-btn" :title="chatsCollapsed ? t('sidebar.expand') : t('sidebar.collapse')">
1468
- <svg :class="{ collapsed: chatsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
1469
- </button>
1470
- </span>
1471
- </div>
1472
-
1473
- <div v-show="!chatsCollapsed" class="sidebar-section-collapsible">
1474
- <button class="new-conversation-btn" @click="newConversation">
1475
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
1476
- {{ t('sidebar.newConversation') }}
1477
- </button>
1478
-
1479
- <div v-if="loadingSessions && historySessions.length === 0" class="sidebar-loading">
1480
- {{ t('sidebar.loadingSessions') }}
1481
- </div>
1482
- <div v-else-if="historySessions.length === 0" class="sidebar-empty">
1483
- {{ t('sidebar.noSessions') }}
1484
- </div>
1485
- <div v-else class="session-list">
1486
- <div v-for="group in groupedSessions" :key="group.label" class="session-group">
1487
- <div class="session-group-label">{{ group.label }}</div>
1488
- <div
1489
- v-for="s in group.sessions" :key="s.sessionId"
1490
- :class="['session-item', { active: currentClaudeSessionId === s.sessionId, processing: isSessionProcessing(s.sessionId) }]"
1491
- @click="renamingSessionId !== s.sessionId && resumeSession(s)"
1492
- :title="s.preview"
1493
- :aria-label="(s.title || s.sessionId.slice(0, 8)) + (isSessionProcessing(s.sessionId) ? ' (processing)' : '')"
1494
- >
1495
- <div v-if="renamingSessionId === s.sessionId" class="session-rename-row">
1496
- <input
1497
- class="session-rename-input"
1498
- v-model="renameText"
1499
- @click.stop
1500
- @keydown.enter.stop="confirmRename"
1501
- @keydown.escape.stop="cancelRename"
1502
- @vue:mounted="$event.el.focus()"
1503
- />
1504
- <button class="session-rename-ok" @click.stop="confirmRename" :title="t('sidebar.confirm')">&#10003;</button>
1505
- <button class="session-rename-cancel" @click.stop="cancelRename" :title="t('sidebar.cancel')">&times;</button>
1506
- </div>
1507
- <div v-else class="session-title">
1508
- <svg v-if="s.title && s.title.startsWith('You are a team lead')" class="session-team-icon" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
1509
- {{ s.title }}
1510
- </div>
1511
- <div class="session-meta">
1512
- <span>{{ formatRelativeTime(s.lastModified) }}</span>
1513
- <span v-if="renamingSessionId !== s.sessionId" class="session-actions">
1514
- <button
1515
- class="session-rename-btn"
1516
- @click.stop="startRename(s)"
1517
- :title="t('sidebar.renameSession')"
1518
- >
1519
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
1520
- </button>
1521
- <button
1522
- v-if="currentClaudeSessionId !== s.sessionId"
1523
- class="session-delete-btn"
1524
- @click.stop="deleteSession(s)"
1525
- :title="t('sidebar.deleteSession')"
1526
- >
1527
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
1528
- </button>
1529
- </span>
1530
- </div>
1531
- </div>
1532
- </div>
1533
- </div>
1534
- </div>
1535
- </div>
1536
-
1537
- <!-- Teams section -->
1538
- <div class="sidebar-section sidebar-teams" :style="{ flex: teamsCollapsed ? '0 0 auto' : '1 1 0', minHeight: teamsCollapsed ? 'auto' : '0' }">
1539
- <div class="sidebar-section-header" @click="teamsCollapsed = !teamsCollapsed" style="cursor: pointer;">
1540
- <span>{{ t('sidebar.teamsHistory') }}</span>
1541
- <span class="sidebar-section-header-actions">
1542
- <button class="sidebar-refresh-btn" @click.stop="requestTeamsList" :title="t('sidebar.refresh')" :disabled="loadingTeams">
1543
- <svg :class="{ spinning: loadingTeams }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1544
- </button>
1545
- <button class="sidebar-collapse-btn" :title="teamsCollapsed ? t('sidebar.expand') : t('sidebar.collapse')">
1546
- <svg :class="{ collapsed: teamsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
1547
- </button>
1548
- </span>
1549
- </div>
1550
-
1551
- <div v-show="!teamsCollapsed" class="sidebar-section-collapsible">
1552
- <button class="new-conversation-btn" @click="newTeam" :disabled="isTeamActive">
1553
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
1554
- {{ t('sidebar.newTeam') }}
1555
- </button>
1556
-
1557
- <div class="team-history-list">
1558
- <div
1559
- v-for="tm in teamsList" :key="tm.teamId"
1560
- :class="['team-history-item', { active: displayTeam && displayTeam.teamId === tm.teamId }]"
1561
- @click="renamingTeamId !== tm.teamId && viewHistoricalTeam(tm.teamId)"
1562
- :title="tm.title"
1563
- >
1564
- <div class="team-history-info">
1565
- <div v-if="renamingTeamId === tm.teamId" class="session-rename-row">
1566
- <input
1567
- class="session-rename-input"
1568
- v-model="renameTeamText"
1569
- @click.stop
1570
- @keydown.enter.stop="confirmTeamRename"
1571
- @keydown.escape.stop="cancelTeamRename"
1572
- @vue:mounted="$event.el.focus()"
1573
- />
1574
- <button class="session-rename-ok" @click.stop="confirmTeamRename" :title="t('sidebar.confirm')">&#10003;</button>
1575
- <button class="session-rename-cancel" @click.stop="cancelTeamRename" :title="t('sidebar.cancel')">&times;</button>
1576
- </div>
1577
- <div v-else class="team-history-title">{{ tm.title || t('sidebar.untitledTeam') }}</div>
1578
- <div v-if="renamingTeamId !== tm.teamId" class="team-history-meta">
1579
- <span :class="['team-status-badge', 'team-status-badge-sm', 'team-status-' + tm.status]">{{ tm.status }}</span>
1580
- <span v-if="tm.taskCount" class="team-history-tasks">{{ tm.taskCount }} {{ t('sidebar.tasks') }}</span>
1581
- <span v-if="tm.totalCost" class="team-history-tasks">{{'$' + tm.totalCost.toFixed(2) }}</span>
1582
- <span class="session-actions">
1583
- <button class="session-rename-btn" @click.stop="startTeamRename(tm)" :title="t('sidebar.renameTeam')">
1584
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
1585
- </button>
1586
- <button class="session-delete-btn" @click.stop="requestDeleteTeam(tm)" :title="t('sidebar.deleteTeam')">
1587
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
1588
- </button>
1589
- </span>
1590
- </div>
1591
- </div>
1592
- </div>
1593
- </div>
1594
- </div>
1595
- </div>
1596
-
1597
- <!-- Loops section -->
1598
- <div class="sidebar-section sidebar-loops" :style="{ flex: loopsCollapsed ? '0 0 auto' : '1 1 0', minHeight: loopsCollapsed ? 'auto' : '0' }">
1599
- <div class="sidebar-section-header" @click="loopsCollapsed = !loopsCollapsed" style="cursor: pointer;">
1600
- <span>{{ t('sidebar.loops') }}</span>
1601
- <span class="sidebar-section-header-actions">
1602
- <button class="sidebar-refresh-btn" @click.stop="requestLoopsList" :title="t('sidebar.refresh')" :disabled="loadingLoops">
1603
- <svg :class="{ spinning: loadingLoops }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1604
- </button>
1605
- <button class="sidebar-collapse-btn" :title="loopsCollapsed ? t('sidebar.expand') : t('sidebar.collapse')">
1606
- <svg :class="{ collapsed: loopsCollapsed }" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
1607
- </button>
1608
- </span>
1609
- </div>
1610
-
1611
- <div v-show="!loopsCollapsed" class="sidebar-section-collapsible">
1612
- <button class="new-conversation-btn" @click="newLoop">
1613
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
1614
- {{ t('sidebar.newLoop') }}
1615
- </button>
1616
-
1617
- <div v-if="loopsList.length === 0 && !loadingLoops" class="sidebar-empty">
1618
- {{ t('sidebar.noLoops') }}
1619
- </div>
1620
- <div v-else class="loop-history-list">
1621
- <div
1622
- v-for="l in loopsList" :key="l.id"
1623
- :class="['team-history-item', { active: selectedLoop?.id === l.id }]"
1624
- @click="renamingLoopId !== l.id && viewLoop(l.id)"
1625
- :title="l.name"
1626
- >
1627
- <div class="team-history-info">
1628
- <div v-if="renamingLoopId === l.id" class="session-rename-row">
1629
- <input
1630
- class="session-rename-input"
1631
- v-model="renameLoopText"
1632
- @click.stop
1633
- @keydown.enter.stop="confirmLoopRename"
1634
- @keydown.escape.stop="cancelLoopRename"
1635
- @vue:mounted="$event.el.focus()"
1636
- />
1637
- <button class="session-rename-ok" @click.stop="confirmLoopRename" :title="t('sidebar.confirm')">&#10003;</button>
1638
- <button class="session-rename-cancel" @click.stop="cancelLoopRename" :title="t('sidebar.cancel')">&times;</button>
1639
- </div>
1640
- <div v-else class="team-history-title">{{ l.name || t('sidebar.untitledLoop') }}</div>
1641
- <div v-if="renamingLoopId !== l.id" class="team-history-meta">
1642
- <span :class="['team-status-badge', 'team-status-badge-sm', l.enabled ? 'team-status-running' : 'team-status-completed']">{{ l.enabled ? t('sidebar.active') : t('sidebar.paused') }}</span>
1643
- <span v-if="l.scheduleType" class="team-history-tasks">{{ formatSchedule(l.scheduleType, l.scheduleConfig || {}, l.schedule) }}</span>
1644
- <span class="session-actions">
1645
- <button class="session-rename-btn" @click.stop="startLoopRename(l)" :title="t('sidebar.renameLoop')">
1646
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
1647
- </button>
1648
- <button class="session-delete-btn" @click.stop="requestDeleteLoop(l)" :title="t('sidebar.deleteLoop')">
1649
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
1650
- </button>
1651
- </span>
1652
- </div>
1653
- </div>
1654
- </div>
1655
- </div>
1656
- </div>
1657
- </div>
1658
-
1659
- <div v-if="serverVersion || agentVersion" class="sidebar-version-footer">
1660
- <span v-if="serverVersion">{{ t('sidebar.server') }} {{ serverVersion }}</span>
1661
- <span v-if="serverVersion && agentVersion" class="sidebar-version-sep">/</span>
1662
- <span v-if="agentVersion">{{ t('sidebar.agent') }} {{ agentVersion }}</span>
1663
- </div>
1664
- </template>
1665
- </aside>
1666
-
1667
- <!-- File browser panel (desktop) -->
1668
- <Transition name="file-panel">
1669
- <div v-if="filePanelOpen && !isMobile" class="file-panel" :style="{ width: filePanelWidth + 'px' }">
1670
- <div class="file-panel-resize-handle" @mousedown="fileBrowser.onResizeStart($event)" @touchstart="fileBrowser.onResizeStart($event)"></div>
1671
- <div class="file-panel-header">
1672
- <span class="file-panel-title">{{ t('filePanel.files') }}</span>
1673
- <div class="file-panel-actions">
1674
- <button class="file-panel-btn" @click="fileBrowser.refreshTree()" :title="t('sidebar.refresh')">
1675
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1676
- </button>
1677
- <button class="file-panel-btn" @click="filePanelOpen = false" :title="t('sidebar.close')">
1678
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
1679
- </button>
1680
- </div>
1681
- </div>
1682
- <div class="file-panel-breadcrumb" :title="workDir">{{ workDir }}</div>
1683
- <div v-if="fileTreeLoading" class="file-panel-loading">{{ t('filePanel.loading') }}</div>
1684
- <div v-else-if="!fileTreeRoot || !fileTreeRoot.children || fileTreeRoot.children.length === 0" class="file-panel-empty">
1685
- {{ t('filePanel.noFiles') }}
1686
- </div>
1687
- <div v-else class="file-tree">
1688
- <template v-for="item in flattenedTree" :key="item.node.path">
1689
- <div
1690
- class="file-tree-item"
1691
- :class="{ folder: item.node.type === 'directory' }"
1692
- :style="{ paddingLeft: (item.depth * 16 + 8) + 'px' }"
1693
- @click="item.node.type === 'directory' ? fileBrowser.toggleFolder(item.node) : filePreview.openPreview(item.node.path)"
1694
- @contextmenu.prevent="item.node.type !== 'directory' ? fileBrowser.onFileClick($event, item.node) : null"
1695
- >
1696
- <span v-if="item.node.type === 'directory'" class="file-tree-arrow" :class="{ expanded: item.node.expanded }">&#9654;</span>
1697
- <span v-else class="file-tree-file-icon">
1698
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
1699
- </span>
1700
- <span class="file-tree-name" :title="item.node.path">{{ item.node.name }}</span>
1701
- <span v-if="item.node.loading" class="file-tree-spinner"></span>
1702
- </div>
1703
- <div v-if="item.node.type === 'directory' && item.node.expanded && item.node.children && item.node.children.length === 0 && !item.node.loading" class="file-tree-empty" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ t('filePanel.empty') }}</div>
1704
- <div v-if="item.node.error" class="file-tree-error" :style="{ paddingLeft: ((item.depth + 1) * 16 + 8) + 'px' }">{{ item.node.error }}</div>
1705
- </template>
1706
- </div>
1707
- </div>
1708
- </Transition>
1709
-
1710
- <!-- Memory panel (desktop) -->
1711
- <Transition name="file-panel">
1712
- <div v-if="memoryPanelOpen && !isMobile" class="file-panel memory-panel">
1713
- <div class="file-panel-header">
1714
- <span class="file-panel-title">{{ t('memory.title') }}</span>
1715
- <div class="file-panel-actions">
1716
- <button class="file-panel-btn" @click="refreshMemory()" :title="t('sidebar.refresh')">
1717
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
1718
- </button>
1719
- <button class="file-panel-btn" @click="memoryPanelOpen = false" :title="t('sidebar.close')">
1720
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
1721
- </button>
1722
- </div>
1723
- </div>
1724
- <div v-if="memoryLoading" class="file-panel-loading">{{ t('memory.loading') }}</div>
1725
- <div v-else-if="memoryFiles.length === 0" class="memory-empty">
1726
- <p>{{ t('memory.noFiles') }}</p>
1727
- <p class="memory-empty-hint">{{ t('memory.noFilesHint') }}</p>
1728
- </div>
1729
- <div v-else class="file-tree">
1730
- <div v-for="file in memoryFiles" :key="file.name" class="file-tree-item memory-file-item">
1731
- <div class="memory-file-row" @click="openMemoryFile(file)">
1732
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13zM6 20V4h5v5c0 .55.45 1 1 1h5v10H6z"/></svg>
1733
- <span class="file-tree-name">{{ file.name }}</span>
1734
- </div>
1735
- <button class="memory-delete-btn" @click.stop="deleteMemoryFile(file)" :title="t('memory.deleteFile')">
1736
- <svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
1737
- </button>
1738
- </div>
1739
- </div>
1740
- </div>
1741
- </Transition>
1742
-
1743
- <!-- Chat area -->
1744
- <div class="chat-area">
1745
-
1746
- <!-- ══ Team Dashboard ══ -->
1747
- <template v-if="viewMode === 'team'">
1748
-
1749
- <!-- Team creation panel (no active team) -->
1750
- <div v-if="!displayTeam" class="team-create-panel">
1751
- <div class="team-create-inner">
1752
- <div class="team-create-header">
1753
- <svg viewBox="0 0 24 24" width="28" height="28"><path fill="currentColor" opacity="0.5" d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
1754
- <h2>{{ t('team.launchAgentTeam') }}</h2>
1755
- </div>
1756
- <p class="team-create-desc">{{ t('team.selectTemplateDesc') }}</p>
1757
-
1758
- <!-- Template selector -->
1759
- <div class="team-tpl-section">
1760
- <label class="team-tpl-label">{{ t('team.template') }}</label>
1761
- <select class="team-tpl-select" :value="selectedTemplate" @change="onTemplateChange($event.target.value)">
1762
- <option v-for="key in TEMPLATE_KEYS" :key="key" :value="key">{{ TEMPLATES[key].label }}</option>
1763
- </select>
1764
- <span class="team-tpl-desc">{{ TEMPLATES[selectedTemplate].description }}</span>
1765
- </div>
1766
-
1767
- <!-- Collapsible lead prompt -->
1768
- <div class="team-lead-prompt-section">
1769
- <div class="team-lead-prompt-header" @click="leadPromptExpanded = !leadPromptExpanded">
1770
- <svg class="team-lead-prompt-arrow" :class="{ expanded: leadPromptExpanded }" viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z"/></svg>
1771
- <span class="team-lead-prompt-title">{{ t('team.leadPrompt') }}</span>
1772
- <span v-if="!leadPromptExpanded" class="team-lead-prompt-preview">{{ leadPromptPreview() }}</span>
1773
- </div>
1774
- <div v-if="leadPromptExpanded" class="team-lead-prompt-body">
1775
- <textarea
1776
- v-model="editedLeadPrompt"
1777
- class="team-lead-prompt-textarea"
1778
- rows="10"
1779
- ></textarea>
1780
- <div class="team-lead-prompt-actions">
1781
- <button class="team-lead-prompt-reset" @click="resetLeadPrompt()" :title="t('team.reset')">{{ t('team.reset') }}</button>
1782
- </div>
1783
- </div>
1784
- </div>
1785
-
1786
- <!-- Task description -->
1787
- <div class="team-tpl-section">
1788
- <label class="team-tpl-label">{{ t('team.taskDescription') }}</label>
1789
- <textarea
1790
- v-model="teamInstruction"
1791
- class="team-create-textarea"
1792
- :placeholder="t('team.taskPlaceholder')"
1793
- rows="4"
1794
- ></textarea>
1795
- </div>
1796
-
1797
- <div class="team-create-actions">
1798
- <button class="team-create-launch" :disabled="!teamInstruction.trim()" @click="launchTeamFromPanel()">
1799
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
1800
- {{ t('team.launchTeam') }}
1801
- </button>
1802
- <button class="team-create-cancel" @click="backToChat()">{{ t('team.backToChat') }}</button>
1803
- </div>
1804
-
1805
- <!-- Example instructions -->
1806
- <div class="team-examples-section">
1807
- <div class="team-examples-header">{{ t('team.examples') }}</div>
1808
- <div class="team-examples-list">
1809
- <div class="team-example-card" v-for="(ex, i) in teamExamples" :key="i">
1810
- <div class="team-example-icon" v-html="ex.icon"></div>
1811
- <div class="team-example-body">
1812
- <div class="team-example-title">{{ ex.title }}</div>
1813
- <div class="team-example-text">{{ ex.text }}</div>
1814
- </div>
1815
- <button class="team-example-try" @click="onTemplateChange(ex.template); teamInstruction = ex.text">{{ t('team.tryIt') }}</button>
1816
- </div>
1817
- </div>
1818
- </div>
1819
-
1820
- <!-- Historical teams -->
1821
- <div v-if="teamsList.length > 0" class="team-history-section">
1822
- <div class="team-history-section-header">{{ t('team.previousTeams') }}</div>
1823
- <div class="team-history-list">
1824
- <div
1825
- v-for="tm in teamsList" :key="tm.teamId"
1826
- class="team-history-item"
1827
- @click="viewHistoricalTeam(tm.teamId)"
1828
- :title="tm.title"
1829
- >
1830
- <div class="team-history-info">
1831
- <div class="team-history-title">{{ tm.title || t('sidebar.untitledTeam') }}</div>
1832
- <div class="team-history-meta">
1833
- <span :class="['team-status-badge', 'team-status-badge-sm', 'team-status-' + tm.status]">{{ tm.status }}</span>
1834
- <span v-if="tm.taskCount" class="team-history-tasks">{{ tm.taskCount }} {{ t('sidebar.tasks') }}</span>
1835
- <span v-if="tm.totalCost" class="team-history-tasks">{{'$' + tm.totalCost.toFixed(2) }}</span>
1836
- </div>
1837
- </div>
1838
- </div>
1839
- </div>
1840
- </div>
1841
- </div>
1842
- </div>
1843
-
1844
- <!-- Active/historical team dashboard -->
1845
- <div v-else class="team-dashboard">
1846
- <!-- Dashboard header -->
1847
- <div class="team-dash-header">
1848
- <div class="team-dash-header-top">
1849
- <span :class="['team-status-badge', 'team-status-' + displayTeam.status]">{{ displayTeam.status }}</span>
1850
- <div class="team-dash-header-right">
1851
- <button v-if="isTeamRunning" class="team-dissolve-btn" @click="dissolveTeam()">{{ t('team.dissolveTeam') }}</button>
1852
- <button v-if="!isTeamActive" class="team-new-btn" @click="newTeam()">{{ t('team.newTeam') }}</button>
1853
- <button v-if="!isTeamActive" class="team-back-btn" @click="backToChat()">{{ t('team.backToChat') }}</button>
1854
- </div>
1855
- </div>
1856
- <div class="team-dash-instruction" :class="{ expanded: instructionExpanded }">
1857
- <div class="team-dash-instruction-text">{{ displayTeam.config?.instruction || displayTeam.title || t('team.agentTeam') }}</div>
1858
- <button v-if="(displayTeam.config?.instruction || '').length > 120" class="team-dash-instruction-toggle" @click="instructionExpanded = !instructionExpanded">
1859
- {{ instructionExpanded ? t('team.showLess') : t('team.showMore') }}
1860
- </button>
1861
- </div>
1862
- </div>
1863
-
1864
- <!-- Lead status bar (clickable to view lead detail) -->
1865
- <div v-if="displayTeam.leadStatus && (displayTeam.status === 'planning' || displayTeam.status === 'running' || displayTeam.status === 'summarizing')" class="team-lead-bar team-lead-bar-clickable" @click="viewAgent('lead')">
1866
- <span class="team-lead-dot"></span>
1867
- <span class="team-lead-label">{{ t('team.lead') }}</span>
1868
- <span class="team-lead-text">{{ displayTeam.leadStatus }}</span>
1869
- </div>
1870
-
1871
- <!-- Dashboard body -->
1872
- <div class="team-dash-body">
1873
-
1874
- <!-- Main content: kanban + agents + feed (dashboard view) -->
1875
- <div v-if="!activeAgentView" class="team-dash-main">
1876
-
1877
- <!-- Kanban board (collapsible) -->
1878
- <div class="team-kanban-section">
1879
- <div class="team-kanban-section-header" @click="kanbanExpanded = !kanbanExpanded">
1880
- <span class="team-kanban-section-toggle">{{ kanbanExpanded ? '\u25BC' : '\u25B6' }}</span>
1881
- <span class="team-kanban-section-title">{{ t('team.tasks') }}</span>
1882
- <span class="team-kanban-section-summary">{{ doneTasks.length }}/{{ displayTeam.tasks.length }} {{ t('team.done') }}</span>
1883
- </div>
1884
- <div v-show="kanbanExpanded" class="team-kanban">
1885
- <div class="team-kanban-col">
1886
- <div class="team-kanban-col-header">
1887
- <span class="team-kanban-col-dot pending"></span>
1888
- {{ t('team.pending') }}
1889
- <span class="team-kanban-col-count">{{ pendingTasks.length }}</span>
1890
- </div>
1891
- <div class="team-kanban-col-body">
1892
- <div v-for="task in pendingTasks" :key="task.id" class="team-task-card">
1893
- <div class="team-task-title">{{ task.title }}</div>
1894
- <div v-if="task.description" class="team-task-desc team-task-desc-clamp" @click.stop="$event.target.classList.toggle('team-task-desc-expanded')">{{ task.description }}</div>
1895
- </div>
1896
- <div v-if="pendingTasks.length === 0" class="team-kanban-empty">{{ t('team.noTasks') }}</div>
1897
- </div>
1898
- </div>
1899
- <div class="team-kanban-col">
1900
- <div class="team-kanban-col-header">
1901
- <span class="team-kanban-col-dot active"></span>
1902
- {{ t('team.activeCol') }}
1903
- <span class="team-kanban-col-count">{{ activeTasks.length }}</span>
1904
- </div>
1905
- <div class="team-kanban-col-body">
1906
- <div v-for="task in activeTasks" :key="task.id" class="team-task-card active">
1907
- <div class="team-task-title">{{ task.title }}</div>
1908
- <div v-if="task.description" class="team-task-desc team-task-desc-clamp" @click.stop="$event.target.classList.toggle('team-task-desc-expanded')">{{ task.description }}</div>
1909
- <div v-if="getTaskAgent(task)" class="team-task-assignee">
1910
- <span class="team-agent-dot" :style="{ background: getAgentColor(task.assignee || task.assignedTo) }"></span>
1911
- {{ getTaskAgent(task).name || task.assignee || task.assignedTo }}
1912
- </div>
1913
- </div>
1914
- <div v-if="activeTasks.length === 0" class="team-kanban-empty">{{ t('team.noTasks') }}</div>
1915
- </div>
1916
- </div>
1917
- <div class="team-kanban-col">
1918
- <div class="team-kanban-col-header">
1919
- <span class="team-kanban-col-dot done"></span>
1920
- {{ t('team.doneCol') }}
1921
- <span class="team-kanban-col-count">{{ doneTasks.length }}</span>
1922
- </div>
1923
- <div class="team-kanban-col-body">
1924
- <div v-for="task in doneTasks" :key="task.id" class="team-task-card done">
1925
- <div class="team-task-title">{{ task.title }}</div>
1926
- <div v-if="task.description" class="team-task-desc team-task-desc-clamp" @click.stop="$event.target.classList.toggle('team-task-desc-expanded')">{{ task.description }}</div>
1927
- <div v-if="getTaskAgent(task)" class="team-task-assignee">
1928
- <span class="team-agent-dot" :style="{ background: getAgentColor(task.assignee || task.assignedTo) }"></span>
1929
- {{ getTaskAgent(task).name || task.assignee || task.assignedTo }}
1930
- </div>
1931
- </div>
1932
- <div v-if="doneTasks.length === 0" class="team-kanban-empty">{{ t('team.noTasks') }}</div>
1933
- </div>
1934
- </div>
1935
- <div v-if="failedTasks.length > 0" class="team-kanban-col">
1936
- <div class="team-kanban-col-header">
1937
- <span class="team-kanban-col-dot failed"></span>
1938
- {{ t('team.failed') }}
1939
- <span class="team-kanban-col-count">{{ failedTasks.length }}</span>
1940
- </div>
1941
- <div class="team-kanban-col-body">
1942
- <div v-for="task in failedTasks" :key="task.id" class="team-task-card failed">
1943
- <div class="team-task-title">{{ task.title }}</div>
1944
- <div v-if="task.description" class="team-task-desc team-task-desc-clamp" @click.stop="$event.target.classList.toggle('team-task-desc-expanded')">{{ task.description }}</div>
1945
- </div>
1946
- </div>
1947
- </div>
1948
- </div>
1949
- </div>
1950
-
1951
- <!-- Agent cards (horizontal) -->
1952
- <div class="team-agents-bar">
1953
- <div class="team-agents-bar-header">{{ t('team.agents') }}</div>
1954
- <div class="team-agents-bar-list">
1955
- <div
1956
- v-for="agent in (displayTeam.agents || [])" :key="agent.id"
1957
- class="team-agent-card"
1958
- @click="historicalTeam ? viewAgentWithHistory(agent.id) : viewAgent(agent.id)"
1959
- >
1960
- <div class="team-agent-card-top">
1961
- <span :class="['team-agent-dot', { working: agent.status === 'working' || agent.status === 'starting' }]" :style="{ background: getAgentColor(agent.id) }"></span>
1962
- <span class="team-agent-card-name">{{ agent.name || agent.id }}</span>
1963
- <span :class="['team-agent-card-status', 'team-agent-card-status-' + agent.status]">{{ agent.status }}</span>
1964
- </div>
1965
- <div v-if="getLatestAgentActivity(agent.id)" class="team-agent-card-activity">{{ getLatestAgentActivity(agent.id) }}</div>
1966
- </div>
1967
- <div v-if="!displayTeam.agents || displayTeam.agents.length === 0" class="team-agents-empty">
1968
- <span v-if="displayTeam.status === 'planning'">{{ t('team.planningTasks') }}</span>
1969
- <span v-else>{{ t('team.noAgents') }}</span>
1970
- </div>
1971
- </div>
1972
- </div>
1973
-
1974
- <!-- Activity feed -->
1975
- <div v-if="displayTeam.feed && displayTeam.feed.length > 0" class="team-feed">
1976
- <div class="team-feed-header">{{ t('team.activity') }}</div>
1977
- <div class="team-feed-list">
1978
- <div v-for="(entry, fi) in displayTeam.feed" :key="fi" class="team-feed-entry">
1979
- <span v-if="entry.agentId" class="team-agent-dot" :style="{ background: getAgentColor(entry.agentId) }"></span>
1980
- <span v-else class="team-agent-dot" style="background: #666;"></span>
1981
- <span class="team-feed-time">{{ formatTeamTime(entry.timestamp) }}</span>
1982
- <span class="team-feed-text"><span v-if="feedAgentName(entry)" class="team-feed-agent-name" :style="{ color: getAgentColor(entry.agentId) }">{{ feedAgentName(entry) }}</span>{{ feedContentRest(entry) }}</span>
1983
- </div>
1984
- </div>
1985
- </div>
1986
-
1987
- <!-- Completion stats -->
1988
- <div v-if="displayTeam.status === 'completed' || displayTeam.status === 'failed'" class="team-stats-bar">
1989
- <div class="team-stat">
1990
- <span class="team-stat-label">{{ t('team.tasksStat') }}</span>
1991
- <span class="team-stat-value">{{ doneTasks.length }}/{{ displayTeam.tasks.length }}</span>
1992
- </div>
1993
- <div v-if="displayTeam.durationMs" class="team-stat">
1994
- <span class="team-stat-label">{{ t('team.duration') }}</span>
1995
- <span class="team-stat-value">{{ formatDuration(displayTeam.durationMs) }}</span>
1996
- </div>
1997
- <div v-if="displayTeam.totalCost" class="team-stat">
1998
- <span class="team-stat-label">{{ t('team.cost') }}</span>
1999
- <span class="team-stat-value">{{ '$' + displayTeam.totalCost.toFixed(2) }}</span>
2000
- </div>
2001
- <div class="team-stat">
2002
- <span class="team-stat-label">{{ t('team.agentsStat') }}</span>
2003
- <span class="team-stat-value">{{ (displayTeam.agents || []).length }}</span>
2004
- </div>
2005
- </div>
2006
-
2007
- <!-- Completion summary -->
2008
- <div v-if="displayTeam.status === 'completed' && displayTeam.summary" class="team-summary">
2009
- <div class="team-summary-header">{{ t('team.summary') }}</div>
2010
- <div class="team-summary-body markdown-body" v-html="getRenderedContent({ role: 'assistant', content: displayTeam.summary })"></div>
2011
- </div>
2012
-
2013
- <!-- New team launcher after completion -->
2014
- <div v-if="!historicalTeam && (displayTeam.status === 'completed' || displayTeam.status === 'failed')" class="team-new-launcher">
2015
- <textarea
2016
- v-model="teamInstruction"
2017
- class="team-new-launcher-input"
2018
- :placeholder="t('team.launchAnotherPlaceholder')"
2019
- rows="2"
2020
- @keydown.enter.ctrl="launchTeamFromPanel()"
2021
- ></textarea>
2022
- <div class="team-new-launcher-actions">
2023
- <button class="team-create-launch" :disabled="!teamInstruction.trim()" @click="launchTeamFromPanel()">
2024
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
2025
- {{ t('team.newTeam') }}
2026
- </button>
2027
- <button class="team-create-cancel" @click="backToChat()">{{ t('team.backToChat') }}</button>
2028
- </div>
2029
- </div>
2030
- </div>
2031
-
2032
- <!-- Agent detail view -->
2033
- <div v-else class="team-agent-detail">
2034
- <div class="team-agent-detail-header" :style="{ borderBottom: '2px solid ' + getAgentColor(activeAgentView) }">
2035
- <button class="team-agent-back-btn" @click="viewDashboard()">
2036
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
2037
- {{ t('team.dashboard') }}
2038
- </button>
2039
- <span :class="['team-agent-dot', { working: findAgent(activeAgentView)?.status === 'working' || findAgent(activeAgentView)?.status === 'starting' }]" :style="{ background: getAgentColor(activeAgentView) }"></span>
2040
- <span class="team-agent-detail-name" :style="{ color: getAgentColor(activeAgentView) }">{{ findAgent(activeAgentView)?.name || activeAgentView }}</span>
2041
- <span class="team-agent-detail-status">{{ findAgent(activeAgentView)?.status }}</span>
2042
- </div>
2043
- <div class="team-agent-messages">
2044
- <div class="team-agent-messages-inner">
2045
- <div v-if="getAgentMessages(activeAgentView).length === 0" class="team-agent-empty-msg">
2046
- {{ t('team.noMessages') }}
2047
- </div>
2048
- <template v-for="(msg, mi) in getAgentMessages(activeAgentView)" :key="msg.id">
2049
- <!-- Agent user/prompt message -->
2050
- <div v-if="msg.role === 'user' && msg.content" class="team-agent-prompt">
2051
- <div class="team-agent-prompt-label">{{ t('team.taskPrompt') }}</div>
2052
- <div class="team-agent-prompt-body markdown-body" v-html="getRenderedContent(msg)"></div>
2053
- </div>
2054
- <!-- System notice (e.g. completion message) -->
2055
- <div v-else-if="msg.role === 'system'" class="team-agent-empty-msg">
2056
- {{ msg.content }}
2057
- </div>
2058
- <!-- Agent assistant text -->
2059
- <div v-else-if="msg.role === 'assistant'" :class="['message', 'message-assistant']">
2060
- <div class="team-agent-detail-name-tag" :style="{ color: getAgentColor(activeAgentView) }">{{ findAgent(activeAgentView)?.name || activeAgentView }}</div>
2061
- <div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]">
2062
- <div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
2063
- </div>
2064
- </div>
2065
- <!-- Plan mode switch indicator -->
2066
- <div v-else-if="msg.role === 'tool' && (msg.toolName === 'EnterPlanMode' || msg.toolName === 'ExitPlanMode')" class="plan-mode-divider">
2067
- <span class="plan-mode-divider-line"></span>
2068
- <span class="plan-mode-divider-text">{{ msg.toolName === 'EnterPlanMode' ? t('tool.enteredPlanMode') : t('tool.exitedPlanMode') }}</span>
2069
- <span class="plan-mode-divider-line"></span>
2070
- </div>
2071
- <!-- Agent tool use -->
2072
- <div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
2073
- <div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
2074
- <span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
2075
- <span class="tool-name">{{ msg.toolName }}</span>
2076
- <span class="tool-summary">{{ getToolSummary(msg) }}</span>
2077
- <span class="tool-status-icon" v-if="msg.hasResult">\u{2713}</span>
2078
- <span class="tool-status-icon running-dots" v-else>
2079
- <span></span><span></span><span></span>
2080
- </span>
2081
- <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2082
- </div>
2083
- <div v-if="msg.expanded" class="tool-expand">
2084
- <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2085
- <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2086
- <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
2087
- <pre v-if="msg.toolOutput" class="tool-block tool-output">{{ msg.toolOutput }}</pre>
2088
- </div>
2089
- </div>
2090
- </template>
2091
- </div>
2092
- </div>
2093
- </div>
2094
-
2095
- </div>
2096
- </div>
2097
- </template>
2098
-
2099
- <!-- ══ Loop Dashboard ══ -->
2100
- <template v-else-if="viewMode === 'loop'">
2101
-
2102
- <!-- ── Execution detail view ── -->
2103
- <div v-if="selectedLoop && selectedExecution" class="team-create-panel">
2104
- <div class="team-create-inner">
2105
- <div class="loop-detail-header">
2106
- <button class="team-agent-back-btn" @click="backToLoopDetail()">
2107
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
2108
- {{ selectedLoop.name }}
2109
- </button>
2110
- </div>
2111
-
2112
- <div v-if="loadingExecution" class="loop-loading">
2113
- <div class="history-loading-spinner"></div>
2114
- <span>{{ t('loop.loadingExecution') }}</span>
2115
- </div>
2116
-
2117
- <div v-else class="loop-exec-messages">
2118
- <div v-if="executionMessages.length === 0" class="team-agent-empty-msg">{{ t('loop.noExecMessages') }}</div>
2119
- <template v-for="(msg, mi) in executionMessages" :key="msg.id">
2120
- <div v-if="msg.role === 'user' && msg.content" class="team-agent-prompt">
2121
- <div class="team-agent-prompt-label">{{ t('loop.loopPrompt') }}</div>
2122
- <div class="team-agent-prompt-body markdown-body" v-html="getRenderedContent(msg)"></div>
2123
- </div>
2124
- <div v-else-if="msg.role === 'assistant'" :class="['message', 'message-assistant']">
2125
- <div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]">
2126
- <div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
2127
- </div>
2128
- </div>
2129
- <div v-else-if="msg.role === 'tool' && (msg.toolName === 'EnterPlanMode' || msg.toolName === 'ExitPlanMode')" class="plan-mode-divider">
2130
- <span class="plan-mode-divider-line"></span>
2131
- <span class="plan-mode-divider-text">{{ msg.toolName === 'EnterPlanMode' ? t('tool.enteredPlanMode') : t('tool.exitedPlanMode') }}</span>
2132
- <span class="plan-mode-divider-line"></span>
2133
- </div>
2134
- <div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
2135
- <div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
2136
- <span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
2137
- <span class="tool-name">{{ msg.toolName }}</span>
2138
- <span class="tool-summary">{{ getToolSummary(msg) }}</span>
2139
- <span class="tool-status-icon" v-if="msg.hasResult">\u{2713}</span>
2140
- <span class="tool-status-icon running-dots" v-else>
2141
- <span></span><span></span><span></span>
2142
- </span>
2143
- <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2144
- </div>
2145
- <div v-if="msg.expanded" class="tool-expand">
2146
- <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2147
- <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2148
- <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
2149
- <pre v-if="msg.toolOutput" class="tool-block tool-output">{{ msg.toolOutput }}</pre>
2150
- </div>
2151
- </div>
2152
- </template>
2153
- </div>
2154
- </div>
2155
- </div>
2156
-
2157
- <!-- ── Loop detail view (execution history) ── -->
2158
- <div v-else-if="selectedLoop" class="team-create-panel">
2159
- <div class="team-create-inner">
2160
- <div class="loop-detail-header">
2161
- <button class="team-agent-back-btn" @click="backToLoopsList()">
2162
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
2163
- {{ t('loop.backToLoops') }}
2164
- </button>
2165
- </div>
2166
- <div class="loop-detail-info">
2167
- <h2 class="loop-detail-name">{{ selectedLoop.name }}</h2>
2168
- <div class="loop-detail-meta">
2169
- <span class="loop-detail-schedule">{{ loopScheduleDisplay(selectedLoop) }}</span>
2170
- <span :class="['loop-status-badge', selectedLoop.enabled ? 'loop-status-enabled' : 'loop-status-disabled']">{{ selectedLoop.enabled ? t('loop.enabled') : t('loop.disabled') }}</span>
2171
- </div>
2172
- <div class="loop-detail-actions">
2173
- <button class="loop-action-btn" @click="startEditingLoop(selectedLoop); selectedLoop = null">{{ t('loop.edit') }}</button>
2174
- <button class="loop-action-btn loop-action-run" @click="runNow(selectedLoop.id)" :disabled="isLoopRunning(selectedLoop.id)">{{ t('loop.runNow') }}</button>
2175
- <button class="loop-action-btn" @click="toggleLoop(selectedLoop.id)">{{ selectedLoop.enabled ? t('loop.disable') : t('loop.enable') }}</button>
2176
- </div>
2177
- </div>
2178
-
2179
- <div class="loop-detail-prompt-section">
2180
- <div class="loop-detail-prompt-label">{{ t('loop.prompt') }}</div>
2181
- <div class="loop-detail-prompt-text">{{ selectedLoop.prompt }}</div>
2182
- </div>
2183
-
2184
- <div class="loop-exec-history-section">
2185
- <div class="loop-exec-history-header">{{ t('loop.executionHistory') }}</div>
2186
- <div v-if="loadingExecutions" class="loop-loading">
2187
- <div class="history-loading-spinner"></div>
2188
- <span>{{ t('loop.loadingExecutions') }}</span>
2189
- </div>
2190
- <div v-else-if="executionHistory.length === 0" class="loop-exec-empty">{{ t('loop.noExecutions') }}</div>
2191
- <div v-else class="loop-exec-list">
2192
- <div v-for="exec in executionHistory" :key="exec.id" class="loop-exec-item">
2193
- <div class="loop-exec-item-left">
2194
- <span :class="['loop-exec-status-icon', 'loop-exec-status-' + exec.status]">
2195
- <template v-if="exec.status === 'running'">\u{21BB}</template>
2196
- <template v-else-if="exec.status === 'success'">\u{2713}</template>
2197
- <template v-else-if="exec.status === 'error'">\u{2717}</template>
2198
- <template v-else-if="exec.status === 'cancelled'">\u{25CB}</template>
2199
- <template v-else>?</template>
2200
- </span>
2201
- <span class="loop-exec-time">{{ formatExecTime(exec.startedAt) }}</span>
2202
- <span v-if="exec.status === 'running'" class="loop-exec-running-label">{{ t('loop.running') }}</span>
2203
- <span v-else-if="exec.durationMs" class="loop-exec-duration">{{ formatDuration(exec.durationMs) }}</span>
2204
- <span v-if="exec.error" class="loop-exec-error-text" :title="exec.error">{{ exec.error.length > 40 ? exec.error.slice(0, 40) + '...' : exec.error }}</span>
2205
- <span v-if="exec.trigger === 'manual'" class="loop-exec-trigger-badge">{{ t('loop.manualBadge') }}</span>
2206
- </div>
2207
- <div class="loop-exec-item-right">
2208
- <button v-if="exec.status === 'running'" class="loop-action-btn" @click="viewExecution(selectedLoop.id, exec.id)">{{ t('loop.view') }}</button>
2209
- <button v-if="exec.status === 'running'" class="loop-action-btn loop-action-cancel" @click="cancelLoopExecution(selectedLoop.id)">{{ t('loop.cancelExec') }}</button>
2210
- <button v-if="exec.status !== 'running'" class="loop-action-btn" @click="viewExecution(selectedLoop.id, exec.id)">{{ t('loop.view') }}</button>
2211
- </div>
2212
- </div>
2213
- <!-- Load more executions -->
2214
- <div v-if="hasMoreExecutions && !loadingExecutions" class="loop-load-more">
2215
- <button class="loop-action-btn" :disabled="loadingMoreExecutions" @click="loadMoreExecutions()">
2216
- {{ loadingMoreExecutions ? t('filePanel.loading') : t('loop.loadMore') }}
2217
- </button>
2218
- </div>
2219
- </div>
2220
- </div>
2221
- </div>
2222
- </div>
2223
-
2224
- <!-- ── Loop creation panel (default) ── -->
2225
- <div v-else class="team-create-panel">
2226
- <div class="team-create-inner">
2227
- <div class="team-create-header">
2228
- <svg viewBox="0 0 24 24" width="28" height="28"><path fill="currentColor" opacity="0.5" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
2229
- <h2>{{ editingLoopId ? t('loop.editLoop') : t('loop.createLoop') }}</h2>
2230
- </div>
2231
- <p class="team-create-desc">{{ t('loop.createDesc') }}</p>
2232
-
2233
- <!-- Template cards -->
2234
- <div v-if="!editingLoopId" class="team-examples-section" style="margin-top: 0;">
2235
- <div class="team-examples-header">{{ t('loop.templates') }}</div>
2236
- <div class="team-examples-list">
2237
- <div v-for="key in LOOP_TEMPLATE_KEYS" :key="key"
2238
- :class="['team-example-card', { 'loop-template-selected': loopSelectedTemplate === key }]"
2239
- >
2240
- <div class="team-example-icon">
2241
- <svg v-if="key === 'competitive-intel'" viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95a15.65 15.65 0 0 0-1.38-3.56A8.03 8.03 0 0 1 18.92 8zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2s.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56A7.987 7.987 0 0 1 5.08 16zm2.95-8H5.08a7.987 7.987 0 0 1 4.33-3.56A15.65 15.65 0 0 0 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2s.07-1.35.16-2h4.68c.09.65.16 1.32.16 2s-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95a8.03 8.03 0 0 1-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2s-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/></svg>
2242
- <svg v-else-if="key === 'knowledge-base'" viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
2243
- <svg v-else viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
2244
- </div>
2245
- <div class="team-example-body">
2246
- <div class="team-example-title">{{ LOOP_TEMPLATES[key].label }}</div>
2247
- <div class="team-example-text">{{ LOOP_TEMPLATES[key].description }}</div>
2248
- </div>
2249
- <button class="team-example-try" @click="selectLoopTemplate(key)">{{ t('team.tryIt') }}</button>
2250
- </div>
2251
- </div>
2252
- </div>
2253
-
2254
- <!-- Name field -->
2255
- <div class="team-tpl-section">
2256
- <label class="team-tpl-label">{{ t('loop.name') }}</label>
2257
- <input
2258
- v-model="loopName"
2259
- type="text"
2260
- class="loop-name-input"
2261
- :placeholder="t('loop.namePlaceholder')"
2262
- />
2263
- </div>
2264
-
2265
- <!-- Prompt field -->
2266
- <div class="team-tpl-section">
2267
- <label class="team-tpl-label">{{ t('loop.prompt') }}</label>
2268
- <textarea
2269
- v-model="loopPrompt"
2270
- class="team-create-textarea"
2271
- :placeholder="t('loop.promptPlaceholder')"
2272
- rows="5"
2273
- ></textarea>
2274
- </div>
2275
-
2276
- <!-- Schedule selector -->
2277
- <div class="team-tpl-section">
2278
- <label class="team-tpl-label">{{ t('loop.schedule') }}</label>
2279
- <div class="loop-schedule-options">
2280
- <label class="loop-schedule-radio">
2281
- <input type="radio" v-model="loopScheduleType" value="manual" />
2282
- <span>{{ t('loop.manual') }}</span>
2283
- <span v-if="loopScheduleType === 'manual'" class="loop-schedule-detail" style="opacity:0.6">{{ t('loop.manualDetail') }}</span>
2284
- </label>
2285
- <label class="loop-schedule-radio">
2286
- <input type="radio" v-model="loopScheduleType" value="hourly" />
2287
- <span>{{ t('loop.everyHour') }}</span>
2288
- <span v-if="loopScheduleType === 'hourly'" class="loop-schedule-detail">at minute {{ padTwo(loopScheduleMinute) }}</span>
2289
- </label>
2290
- <label class="loop-schedule-radio">
2291
- <input type="radio" v-model="loopScheduleType" value="daily" />
2292
- <span>{{ t('loop.everyDay') }}</span>
2293
- <span v-if="loopScheduleType === 'daily'" class="loop-schedule-detail">
2294
- at
2295
- <input type="number" v-model.number="loopScheduleHour" min="0" max="23" class="loop-time-input" />
2296
- :
2297
- <input type="number" v-model.number="loopScheduleMinute" min="0" max="59" class="loop-time-input" />
2298
- </span>
2299
- </label>
2300
- <label class="loop-schedule-radio">
2301
- <input type="radio" v-model="loopScheduleType" value="cron" />
2302
- <span>{{ t('loop.advancedCron') }}</span>
2303
- <span v-if="loopScheduleType === 'cron'" class="loop-schedule-detail">
2304
- <input type="text" v-model="loopCronExpr" class="loop-cron-input" placeholder="0 9 * * *" />
2305
- </span>
2306
- </label>
2307
- </div>
2308
- </div>
2309
-
2310
- <!-- Action buttons -->
2311
- <div class="team-create-actions">
2312
- <button v-if="editingLoopId" class="team-create-launch" :disabled="!loopName.trim() || !loopPrompt.trim()" @click="saveLoopEdits()">
2313
- {{ t('loop.saveChanges') }}
2314
- </button>
2315
- <button v-else class="team-create-launch" :disabled="!loopName.trim() || !loopPrompt.trim()" @click="createLoopFromPanel()">
2316
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0 0 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 0 0 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
2317
- {{ t('loop.createLoopBtn') }}
2318
- </button>
2319
- <button v-if="editingLoopId" class="team-create-cancel" @click="cancelEditingLoop()">{{ t('loop.cancel') }}</button>
2320
- <button class="team-create-cancel" @click="backToChat()">{{ t('loop.backToChat') }}</button>
2321
- </div>
2322
-
2323
- <!-- Error message -->
2324
- <div v-if="loopError" class="loop-error-banner" @click="clearLoopError()">
2325
- <span class="loop-error-icon">\u{26A0}</span>
2326
- <span class="loop-error-text">{{ loopError }}</span>
2327
- <span class="loop-error-dismiss">\u{2715}</span>
2328
- </div>
2329
-
2330
- <!-- Active Loops list -->
2331
- <div v-if="loopsList.length > 0" class="loop-active-section">
2332
- <div class="loop-active-header">{{ t('loop.activeLoops') }}</div>
2333
- <div class="loop-active-list">
2334
- <div v-for="l in loopsList" :key="l.id" class="loop-active-item">
2335
- <div class="loop-active-item-info" @click="viewLoop(l.id)">
2336
- <div class="loop-active-item-top">
2337
- <span class="loop-active-item-name">{{ l.name }}</span>
2338
- <span :class="['loop-status-dot', l.enabled ? 'loop-status-dot-on' : 'loop-status-dot-off']"></span>
2339
- </div>
2340
- <div class="loop-active-item-meta">
2341
- <span class="loop-active-item-schedule">{{ loopScheduleDisplay(l) }}</span>
2342
- <span v-if="l.lastExecution" class="loop-active-item-last">
2343
- Last: {{ loopLastRunDisplay(l) }}
2344
- </span>
2345
- <span v-if="isLoopRunning(l.id)" class="loop-exec-running-label">{{ t('loop.running') }}</span>
2346
- </div>
2347
- </div>
2348
- <div class="loop-active-item-actions">
2349
- <button class="loop-action-btn loop-action-sm" @click="startEditingLoop(l)" :title="t('loop.edit')">{{ t('loop.edit') }}</button>
2350
- <button class="loop-action-btn loop-action-sm loop-action-run" @click="runNow(l.id)" :disabled="isLoopRunning(l.id)" :title="t('loop.runNow')">{{ t('loop.run') }}</button>
2351
- <button class="loop-action-btn loop-action-sm" @click="toggleLoop(l.id)" :title="l.enabled ? t('loop.disable') : t('loop.enable')">{{ l.enabled ? t('loop.pause') : t('loop.resume') }}</button>
2352
- <button v-if="!l.enabled" class="loop-action-btn loop-action-sm loop-action-delete" @click="requestDeleteLoop(l)" :title="t('loop.deleteLoop')">{{ t('loop.del') }}</button>
2353
- </div>
2354
- </div>
2355
- </div>
2356
- </div>
2357
- </div>
2358
- </div>
2359
-
2360
- <!-- Running Loop notification banner -->
2361
- <div v-if="hasRunningLoop && !selectedLoop" class="loop-running-banner">
2362
- <span class="loop-running-banner-dot"></span>
2363
- <span>{{ firstRunningLoop.name }} {{ t('loop.isRunning') }}</span>
2364
- <button class="loop-action-btn loop-action-sm" @click="viewLoop(firstRunningLoop.loopId)">{{ t('loop.view') }}</button>
2365
- </div>
2366
-
2367
- <!-- Loop delete confirm dialog -->
2368
- <div v-if="loopDeleteConfirmOpen" class="modal-overlay" @click.self="cancelDeleteLoop()">
2369
- <div class="modal-dialog">
2370
- <div class="modal-title">{{ t('loop.deleteLoop') }}</div>
2371
- <div class="modal-body" v-html="t('loop.deleteConfirm', { name: loopDeleteConfirmName })"></div>
2372
- <div class="modal-actions">
2373
- <button class="modal-confirm-btn" @click="confirmDeleteLoop()">{{ t('loop.delete') }}</button>
2374
- <button class="modal-cancel-btn" @click="cancelDeleteLoop()">{{ t('loop.cancel') }}</button>
2375
- </div>
2376
- </div>
2377
- </div>
2378
- </template>
2379
-
2380
- <!-- ══ Normal Chat ══ -->
2381
- <template v-else-if="viewMode === 'chat'">
2382
- <div class="message-list" @scroll="onMessageListScroll">
2383
- <div class="message-list-inner">
2384
- <div v-if="messages.length === 0 && status === 'Connected' && !loadingHistory" class="empty-state">
2385
- <div class="empty-state-icon">
2386
- <svg viewBox="0 0 24 24" width="48" height="48"><path fill="currentColor" opacity="0.4" d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
2387
- </div>
2388
- <p>{{ t('chat.connectedTo') }} <strong>{{ agentName }}</strong></p>
2389
- <p class="muted">{{ workDir }}</p>
2390
- <p class="muted" style="margin-top: 0.5rem;">{{ t('chat.sendToStart') }}</p>
2391
- </div>
2392
-
2393
- <div v-if="loadingHistory" class="history-loading">
2394
- <div class="history-loading-spinner"></div>
2395
- <span>{{ t('chat.loadingHistory') }}</span>
2396
- </div>
2397
-
2398
- <div v-if="hasMoreMessages" class="load-more-wrapper">
2399
- <button class="load-more-btn" @click="loadMoreMessages">{{ t('chat.loadEarlier') }}</button>
2400
- </div>
2401
-
2402
- <div v-for="(msg, msgIdx) in visibleMessages" :key="msg.id" :class="['message', 'message-' + msg.role]">
2403
-
2404
- <!-- User message -->
2405
- <template v-if="msg.role === 'user'">
2406
- <div class="message-role-label user-label">{{ t('chat.you') }}</div>
2407
- <div class="message-bubble user-bubble" :title="formatTimestamp(msg.timestamp)">
2408
- <div class="message-content">{{ msg.content }}</div>
2409
- <div v-if="msg.attachments && msg.attachments.length" class="message-attachments">
2410
- <div v-for="(att, ai) in msg.attachments" :key="ai" class="message-attachment-chip">
2411
- <img v-if="att.isImage && att.thumbUrl" :src="att.thumbUrl" class="message-attachment-thumb" />
2412
- <span v-else class="message-attachment-file-icon">
2413
- <svg viewBox="0 0 16 16" width="12" height="12"><path fill="currentColor" d="M1 2.5A2.5 2.5 0 0 1 3.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75H3.5a1 1 0 0 0-1 1h9.25a.75.75 0 0 1 0 1.5H3.5A2.5 2.5 0 0 1 1 14V2.5z"/></svg>
2414
- </span>
2415
- <span>{{ att.name }}</span>
2416
- </div>
2417
- </div>
2418
- </div>
2419
- </template>
2420
-
2421
- <!-- Assistant message (markdown) -->
2422
- <template v-else-if="msg.role === 'assistant'">
2423
- <div v-if="!isPrevAssistant(msgIdx)" class="message-role-label assistant-label">{{ t('chat.claude') }}</div>
2424
- <div :class="['message-bubble', 'assistant-bubble', { streaming: msg.isStreaming }]" :title="formatTimestamp(msg.timestamp)">
2425
- <div class="message-actions">
2426
- <button class="icon-btn" @click="copyMessage(msg)" :title="msg.copied ? t('chat.copied') : t('chat.copy')">
2427
- <svg v-if="!msg.copied" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
2428
- <svg v-else viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
2429
- </button>
2430
- </div>
2431
- <div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
2432
- </div>
2433
- </template>
2434
-
2435
- <!-- Agent tool call (team-styled) -->
2436
- <div v-else-if="msg.role === 'tool' && msg.toolName === 'Agent'" class="tool-line-wrapper team-agent-tool-wrapper">
2437
- <div :class="['tool-line', 'team-agent-tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
2438
- <span class="team-agent-tool-icon">
2439
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
2440
- </span>
2441
- <span class="team-agent-tool-name">Agent</span>
2442
- <span class="team-agent-tool-desc">{{ getToolSummary(msg) }}</span>
2443
- <span class="tool-status-icon" v-if="msg.hasResult">\u{2713}</span>
2444
- <span class="tool-status-icon running-dots" v-else>
2445
- <span></span><span></span><span></span>
2446
- </span>
2447
- <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2448
- </div>
2449
- <div v-if="msg.expanded" class="tool-expand team-agent-tool-expand">
2450
- <pre v-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
2451
- <div v-if="msg.toolOutput" class="team-agent-tool-result">
2452
- <div class="team-agent-tool-result-label">{{ t('team.agentResult') }}</div>
2453
- <div class="team-agent-tool-result-content markdown-body" v-html="getRenderedContent({ role: 'assistant', content: msg.toolOutput })"></div>
2454
- </div>
2455
- </div>
2456
- </div>
2457
-
2458
- <!-- Plan mode switch indicator -->
2459
- <div v-else-if="msg.role === 'tool' && (msg.toolName === 'EnterPlanMode' || msg.toolName === 'ExitPlanMode')" class="plan-mode-divider">
2460
- <span class="plan-mode-divider-line"></span>
2461
- <span class="plan-mode-divider-text">{{ msg.toolName === 'EnterPlanMode' ? t('tool.enteredPlanMode') : t('tool.exitedPlanMode') }}</span>
2462
- <span class="plan-mode-divider-line"></span>
2463
- </div>
2464
-
2465
- <!-- Tool use block (collapsible) -->
2466
- <div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
2467
- <div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
2468
- <span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
2469
- <span class="tool-name">{{ msg.toolName }}</span>
2470
- <span class="tool-summary">{{ getToolSummary(msg) }}</span>
2471
- <span class="tool-status-icon" v-if="msg.hasResult">\u{2713}</span>
2472
- <span class="tool-status-icon running-dots" v-else>
2473
- <span></span><span></span><span></span>
2474
- </span>
2475
- <span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
2476
- </div>
2477
- <div v-if="msg.expanded" class="tool-expand">
2478
- <div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
2479
- <div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
2480
- <pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
2481
- <pre v-if="msg.toolOutput" class="tool-block tool-output">{{ msg.toolOutput }}</pre>
2482
- </div>
2483
- </div>
2484
-
2485
- <!-- AskUserQuestion interactive card -->
2486
- <div v-else-if="msg.role === 'ask-question'" class="ask-question-wrapper">
2487
- <div v-if="!msg.answered" class="ask-question-card">
2488
- <div v-for="(q, qi) in msg.questions" :key="qi" class="ask-question-block">
2489
- <div v-if="q.header" class="ask-question-header">{{ q.header }}</div>
2490
- <div class="ask-question-text">{{ q.question }}</div>
2491
- <div class="ask-question-options">
2492
- <div
2493
- v-for="(opt, oi) in q.options" :key="oi"
2494
- :class="['ask-question-option', {
2495
- selected: q.multiSelect
2496
- ? (msg.selectedAnswers[qi] || []).includes(opt.label)
2497
- : msg.selectedAnswers[qi] === opt.label
2498
- }]"
2499
- @click="selectQuestionOption(msg, qi, opt.label)"
2500
- >
2501
- <div class="ask-option-label">{{ opt.label }}</div>
2502
- <div v-if="opt.description" class="ask-option-desc">{{ opt.description }}</div>
2503
- </div>
2504
- </div>
2505
- <div class="ask-question-custom">
2506
- <input
2507
- type="text"
2508
- v-model="msg.customTexts[qi]"
2509
- :placeholder="t('chat.customResponse')"
2510
- @input="msg.selectedAnswers[qi] = q.multiSelect ? [] : null"
2511
- @keydown.enter="hasQuestionAnswer(msg) && submitQuestionAnswer(msg)"
2512
- />
2513
- </div>
2514
- </div>
2515
- <div class="ask-question-actions">
2516
- <button class="ask-question-submit" :disabled="!hasQuestionAnswer(msg)" @click="submitQuestionAnswer(msg)">
2517
- {{ t('chat.submit') }}
2518
- </button>
2519
- </div>
2520
- </div>
2521
- <div v-else class="ask-question-answered">
2522
- <span class="ask-answered-icon">\u{2713}</span>
2523
- <span class="ask-answered-text">{{ getQuestionResponseSummary(msg) }}</span>
2524
- </div>
2525
- </div>
2526
-
2527
- <!-- Context summary (collapsed by default) -->
2528
- <div v-else-if="msg.role === 'context-summary'" class="context-summary-wrapper">
2529
- <div class="context-summary-bar" @click="toggleContextSummary(msg)">
2530
- <svg class="context-summary-icon" viewBox="0 0 16 16" width="14" height="14"><path fill="currentColor" d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>
2531
- <span class="context-summary-label">{{ t('chat.contextContinued') }}</span>
2532
- <span class="context-summary-toggle">{{ msg.contextExpanded ? t('chat.hide') : t('chat.show') }}</span>
2533
- </div>
2534
- <div v-if="msg.contextExpanded" class="context-summary-body">
2535
- <div class="markdown-body" v-html="getRenderedContent({ role: 'assistant', content: msg.content })"></div>
2536
- </div>
2537
- </div>
2538
-
2539
- <!-- System message -->
2540
- <div v-else-if="msg.role === 'system'" :class="['system-msg', { 'compact-msg': msg.isCompactStart, 'command-output-msg': msg.isCommandOutput, 'error-msg': msg.isError }]">
2541
- <template v-if="msg.isCompactStart && !msg.compactDone">
2542
- <span class="compact-inline-spinner"></span>
2543
- </template>
2544
- <template v-if="msg.isCompactStart && msg.compactDone">
2545
- <span class="compact-done-icon">✓</span>
2546
- </template>
2547
- <div v-if="msg.isCommandOutput" class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
2548
- <template v-else>{{ msg.content }}</template>
2549
- </div>
2550
- </div>
2551
-
2552
- <div v-if="isProcessing && !hasStreamingMessage" class="typing-indicator">
2553
- <span></span><span></span><span></span>
2554
- <span v-if="pendingPlanMode" class="typing-label">{{ pendingPlanMode === 'enter' ? t('tool.enteringPlanMode') : t('tool.exitingPlanMode') }}</span>
2555
- </div>
2556
- </div>
2557
- </div>
2558
- </template>
2559
-
2560
- <!-- ══ Side question overlay ══ -->
2561
- <Transition name="fade">
2562
- <div v-if="btwState" class="btw-overlay" @click.self="dismissBtw">
2563
- <div class="btw-panel">
2564
- <div class="btw-header">
2565
- <span class="btw-title">{{ t('btw.title') }}</span>
2566
- <button class="btw-close" @click="dismissBtw" :aria-label="t('btw.dismiss')">&#10005;</button>
2567
- </div>
2568
- <div class="btw-body">
2569
- <div class="btw-question">{{ btwState.question }}</div>
2570
- <div v-if="btwState.error" class="btw-error">{{ btwState.error }}</div>
2571
- <template v-else>
2572
- <div v-if="btwState.answer" class="btw-answer markdown-body" v-html="renderMarkdown(btwState.answer)"></div>
2573
- <div v-if="!btwState.done" class="btw-loading">
2574
- <span class="btw-loading-dots"><span></span><span></span><span></span></span>
2575
- <span v-if="!btwState.answer" class="btw-loading-text">{{ t('btw.thinking') }}</span>
2576
- </div>
2577
- </template>
2578
- </div>
2579
- <div v-if="btwState.done && !btwState.error" class="btw-hint">
2580
- {{ isMobile ? t('btw.tapDismiss') : t('btw.escDismiss') }}
2581
- </div>
2582
- </div>
2583
- </div>
2584
- </Transition>
2585
-
2586
- <!-- Input area (shown in both chat and team create mode) -->
2587
- <div class="input-area" v-if="viewMode === 'chat'">
2588
- <input
2589
- type="file"
2590
- ref="fileInputRef"
2591
- multiple
2592
- style="display: none"
2593
- @change="handleFileSelect"
2594
- accept="image/*,text/*,.pdf,.json,.md,.py,.js,.ts,.tsx,.jsx,.css,.html,.xml,.yaml,.yml,.toml,.sh,.sql,.csv"
2595
- />
2596
- <div v-if="queuedMessages.length > 0" class="queue-bar">
2597
- <div v-for="(qm, qi) in queuedMessages" :key="qm.id" class="queue-item">
2598
- <span class="queue-item-num">{{ qi + 1 }}.</span>
2599
- <span class="queue-item-text">{{ qm.content }}</span>
2600
- <span v-if="qm.attachments && qm.attachments.length" class="queue-item-attach" :title="qm.attachments.map(a => a.name).join(', ')">
2601
- <svg viewBox="0 0 24 24" width="11" height="11"><path fill="currentColor" d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
2602
- {{ qm.attachments.length }}
2603
- </span>
2604
- <button class="queue-item-remove" @click="removeQueuedMessage(qm.id)" :title="t('input.removeFromQueue')">&times;</button>
2605
- </div>
2606
- </div>
2607
- <div v-if="usageStats" class="usage-bar">{{ formatUsage(usageStats) }}</div>
2608
- <div v-if="slashMenuVisible && filteredSlashCommands.length > 0" class="slash-menu">
2609
- <div v-for="(cmd, i) in filteredSlashCommands" :key="cmd.command"
2610
- :class="['slash-menu-item', { active: i === slashMenuIndex }]"
2611
- @mouseenter="slashMenuIndex = i"
2612
- @click="selectSlashCommand(cmd)">
2613
- <span class="slash-menu-cmd">{{ cmd.command }}</span>
2614
- <span class="slash-menu-desc">{{ t(cmd.descKey) }}</span>
2615
- </div>
2616
- </div>
2617
- <div
2618
- :class="['input-card', { 'drag-over': dragOver, 'plan-mode': planMode }]"
2619
- @dragover="handleDragOver"
2620
- @dragleave="handleDragLeave"
2621
- @drop="handleDrop"
2622
- >
2623
- <textarea
2624
- ref="inputRef"
2625
- v-model="inputText"
2626
- @keydown="handleKeydown"
2627
- @input="autoResize"
2628
- @paste="handlePaste"
2629
- :disabled="status !== 'Connected'"
2630
- :placeholder="isCompacting ? t('input.compacting') : t('input.placeholder')"
2631
- rows="1"
2632
- ></textarea>
2633
- <div v-if="attachments.length > 0" class="attachment-bar">
2634
- <div v-for="(att, i) in attachments" :key="i" class="attachment-chip">
2635
- <img v-if="att.isImage && att.thumbUrl" :src="att.thumbUrl" class="attachment-thumb" />
2636
- <div v-else class="attachment-file-icon">
2637
- <svg viewBox="0 0 16 16" width="14" height="14"><path fill="currentColor" d="M1 2.5A2.5 2.5 0 0 1 3.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75H3.5a1 1 0 0 0-1 1h9.25a.75.75 0 0 1 0 1.5H3.5A2.5 2.5 0 0 1 1 14V2.5z"/></svg>
2638
- </div>
2639
- <div class="attachment-info">
2640
- <div class="attachment-name">{{ att.name }}</div>
2641
- <div class="attachment-size">{{ formatFileSize(att.size) }}</div>
2642
- </div>
2643
- <button class="attachment-remove" @click="removeAttachment(i)" :title="t('input.remove')">&times;</button>
2644
- </div>
2645
- </div>
2646
- <div class="input-bottom-row">
2647
- <div class="input-bottom-left">
2648
- <button class="attach-btn" @click="triggerFileInput" :disabled="status !== 'Connected' || isCompacting || attachments.length >= 5" :title="t('input.attachFiles')">
2649
- <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5a2.5 2.5 0 0 1 5 0v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5a2.5 2.5 0 0 0 5 0V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z"/></svg>
2650
- </button>
2651
- <button class="slash-btn" @click="openSlashMenu" :disabled="status !== 'Connected'" :title="t('input.slashCommands')">
2652
- <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 21 11 3h2L9 21H7Z"/></svg>
2653
- </button>
2654
- <button :class="['plan-mode-btn', { active: planMode }]" @click="togglePlanMode" :disabled="isProcessing" :title="planMode ? 'Switch to Normal Mode' : 'Switch to Plan Mode'">
2655
- <svg viewBox="0 0 24 24" width="12" height="12"><rect x="6" y="4" width="4" height="16" rx="1" fill="currentColor"/><rect x="14" y="4" width="4" height="16" rx="1" fill="currentColor"/></svg>
2656
- Plan
2657
- </button>
2658
- </div>
2659
- <button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" :title="t('input.stopGeneration')">
2660
- <svg viewBox="0 0 24 24" width="14" height="14"><rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor"/></svg>
2661
- </button>
2662
- <button v-else @click="sendMessage" :disabled="!canSend" class="send-btn" :title="t('input.send')">
2663
- <svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
2664
- </button>
2665
- </div>
2666
- </div>
2667
- </div>
2668
- </div>
2669
-
2670
- <!-- Preview Panel (desktop) -->
2671
- <Transition name="file-panel">
2672
- <div v-if="previewPanelOpen && !isMobile" class="preview-panel" :style="{ width: previewPanelWidth + 'px' }">
2673
- <div class="preview-panel-resize-handle"
2674
- @mousedown="filePreview.onResizeStart($event)"
2675
- @touchstart="filePreview.onResizeStart($event)"></div>
2676
- <div class="preview-panel-header">
2677
- <span class="preview-panel-filename" :title="previewFile?.filePath">
2678
- {{ previewFile?.fileName || t('preview.preview') }}
2679
- </span>
2680
- <button v-if="previewFile?.content && filePreview.isMarkdownFile(previewFile.fileName)"
2681
- class="preview-md-toggle" :class="{ active: previewMarkdownRendered }"
2682
- @click="previewMarkdownRendered = !previewMarkdownRendered"
2683
- :title="previewMarkdownRendered ? t('preview.showSource') : t('preview.renderMarkdown')">
2684
- <svg viewBox="0 0 16 16" width="14" height="14"><path fill="currentColor" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"/></svg>
2685
- </button>
2686
- <span v-if="previewFile" class="preview-panel-size">
2687
- {{ filePreview.formatFileSize(previewFile.totalSize) }}
2688
- </span>
2689
- <button v-if="previewFile && !memoryEditing" class="preview-refresh-btn" @click="filePreview.refreshPreview()" :title="t('sidebar.refresh')">
2690
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
2691
- </button>
2692
- <button v-if="isMemoryPreview && previewFile && !memoryEditing"
2693
- class="preview-edit-btn" @click="startMemoryEdit()" :title="t('memory.edit')">
2694
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1.003 1.003 0 0 0 0-1.42l-2.34-2.34a1.003 1.003 0 0 0-1.42 0l-1.83 1.83 3.75 3.75 1.84-1.82z"/></svg>
2695
- {{ t('memory.edit') }}
2696
- </button>
2697
- <span v-if="memoryEditing" class="preview-edit-label">{{ t('memory.editing') }}</span>
2698
- <button v-if="memoryEditing" class="memory-header-cancel" @click="cancelMemoryEdit()">{{ t('loop.cancel') }}</button>
2699
- <button v-if="memoryEditing" class="memory-header-save" @click="saveMemoryEdit()" :disabled="memorySaving">
2700
- {{ memorySaving ? t('memory.saving') : t('memory.save') }}
2701
- </button>
2702
- <button class="preview-panel-close" @click="filePreview.closePreview(); memoryEditing = false" :title="t('preview.closePreview')">&times;</button>
2703
- </div>
2704
- <div class="preview-panel-body">
2705
- <div v-if="memoryEditing" class="memory-edit-container">
2706
- <textarea class="memory-edit-textarea" v-model="memoryEditContent"></textarea>
2707
- </div>
2708
- <div v-else-if="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
2709
- <div v-else-if="previewFile?.error" class="preview-error">
2710
- {{ previewFile.error }}
2711
- </div>
2712
- <div v-else-if="previewFile?.encoding === 'base64' && previewFile?.content"
2713
- class="preview-image-container">
2714
- <img :src="'data:' + previewFile.mimeType + ';base64,' + previewFile.content"
2715
- :alt="previewFile.fileName" class="preview-image" />
2716
- </div>
2717
- <div v-else-if="previewFile?.content && previewMarkdownRendered && filePreview.isMarkdownFile(previewFile.fileName)"
2718
- class="preview-markdown-rendered markdown-body" v-html="filePreview.renderedMarkdownHtml(previewFile.content)">
2719
- </div>
2720
- <div v-else-if="previewFile?.content" class="preview-text-container">
2721
- <pre class="preview-code"><code v-html="filePreview.highlightCode(previewFile.content, previewFile.fileName)"></code></pre>
2722
- <div v-if="previewFile.truncated" class="preview-truncated-notice">
2723
- {{ t('preview.fileTruncated', { size: filePreview.formatFileSize(previewFile.totalSize) }) }}
2724
- </div>
2725
- </div>
2726
- <div v-else-if="previewFile && !previewFile.content && !previewFile.error" class="preview-binary-info">
2727
- <div class="preview-binary-icon">
2728
- <svg viewBox="0 0 24 24" width="48" height="48"><path fill="currentColor" opacity="0.4" d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z"/></svg>
2729
- </div>
2730
- <p>{{ t('preview.binaryFile') }}</p>
2731
- <p class="preview-binary-meta">{{ previewFile.mimeType }}</p>
2732
- <p class="preview-binary-meta">{{ filePreview.formatFileSize(previewFile.totalSize) }}</p>
2733
- </div>
2734
- </div>
2735
- </div>
2736
- </Transition>
2737
-
2738
- </div>
2739
-
2740
- <!-- Folder Picker Modal -->
2741
- <div class="folder-picker-overlay" v-if="folderPickerOpen" @click.self="folderPickerOpen = false">
2742
- <div class="folder-picker-dialog">
2743
- <div class="folder-picker-header">
2744
- <span>{{ t('folderPicker.title') }}</span>
2745
- <button class="folder-picker-close" @click="folderPickerOpen = false">&times;</button>
2746
- </div>
2747
- <div class="folder-picker-nav">
2748
- <button class="folder-picker-up" @click="folderPickerNavigateUp" :disabled="!folderPickerPath" :title="t('folderPicker.parentDir')">
2749
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
2750
- </button>
2751
- <input class="folder-picker-path-input" type="text" v-model="folderPickerPath" @keydown.enter="folderPickerGoToPath" :placeholder="t('folderPicker.pathPlaceholder')" spellcheck="false" />
2752
- </div>
2753
- <div class="folder-picker-list">
2754
- <div v-if="folderPickerLoading" class="folder-picker-loading">
2755
- <div class="history-loading-spinner"></div>
2756
- <span>{{ t('preview.loading') }}</span> </div>
2757
- <template v-else>
2758
- <div
2759
- v-for="entry in folderPickerEntries" :key="entry.name"
2760
- :class="['folder-picker-item', { 'folder-picker-selected': folderPickerSelected === entry.name }]"
2761
- @click="folderPickerSelectItem(entry)"
2762
- @dblclick="folderPickerEnter(entry)"
2763
- >
2764
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
2765
- <span>{{ entry.name }}</span>
2766
- </div>
2767
- <div v-if="folderPickerEntries.length === 0" class="folder-picker-empty">{{ t('folderPicker.noSubdirs') }}</div>
2768
- </template>
2769
- </div>
2770
- <div class="folder-picker-footer">
2771
- <button class="folder-picker-cancel" @click="folderPickerOpen = false">{{ t('folderPicker.cancel') }}</button>
2772
- <button class="folder-picker-confirm" @click="confirmFolderPicker" :disabled="!folderPickerPath">{{ t('folderPicker.open') }}</button>
2773
- </div>
2774
- </div>
2775
- </div>
2776
-
2777
- <!-- Delete Session Confirmation Dialog -->
2778
- <div class="folder-picker-overlay" v-if="deleteConfirmOpen" @click.self="cancelDeleteSession">
2779
- <div class="delete-confirm-dialog">
2780
- <div class="delete-confirm-header">{{ t('dialog.deleteSession') }}</div>
2781
- <div class="delete-confirm-body">
2782
- <p>{{ t('dialog.deleteSessionConfirm') }}</p>
2783
- <p class="delete-confirm-title">{{ deleteConfirmTitle }}</p>
2784
- <p class="delete-confirm-warning">{{ t('dialog.cannotUndo') }}</p>
2785
- </div>
2786
- <div class="delete-confirm-footer">
2787
- <button class="folder-picker-cancel" @click="cancelDeleteSession">{{ t('dialog.cancel') }}</button>
2788
- <button class="delete-confirm-btn" @click="confirmDeleteSession">{{ t('dialog.delete') }}</button>
2789
- </div>
2790
- </div>
2791
- </div>
2792
-
2793
- <!-- Delete Team Confirmation Dialog -->
2794
- <div class="folder-picker-overlay" v-if="deleteTeamConfirmOpen" @click.self="cancelDeleteTeam">
2795
- <div class="delete-confirm-dialog">
2796
- <div class="delete-confirm-header">{{ t('dialog.deleteTeam') }}</div>
2797
- <div class="delete-confirm-body">
2798
- <p>{{ t('dialog.deleteTeamConfirm') }}</p>
2799
- <p class="delete-confirm-title">{{ deleteTeamConfirmTitle }}</p>
2800
- <p class="delete-confirm-warning">{{ t('dialog.cannotUndo') }}</p>
2801
- </div>
2802
- <div class="delete-confirm-footer">
2803
- <button class="folder-picker-cancel" @click="cancelDeleteTeam">{{ t('dialog.cancel') }}</button>
2804
- <button class="delete-confirm-btn" @click="confirmDeleteTeam">{{ t('dialog.delete') }}</button>
2805
- </div>
2806
- </div>
2807
- </div>
2808
-
2809
- <!-- Password Authentication Dialog -->
2810
- <div class="folder-picker-overlay" v-if="authRequired && !authLocked">
2811
- <div class="auth-dialog">
2812
- <div class="auth-dialog-header">
2813
- <svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
2814
- <span>{{ t('auth.sessionProtected') }}</span>
2815
- </div>
2816
- <div class="auth-dialog-body">
2817
- <p>{{ t('auth.passwordRequired') }}</p>
2818
- <input
2819
- type="password"
2820
- class="auth-password-input"
2821
- v-model="authPassword"
2822
- @keydown.enter="submitPassword"
2823
- :placeholder="t('auth.passwordPlaceholder')"
2824
- autofocus
2825
- />
2826
- <p v-if="authError" class="auth-error">{{ authError }}</p>
2827
- <p v-if="authAttempts" class="auth-attempts">{{ authAttempts }}</p>
2828
- </div>
2829
- <div class="auth-dialog-footer">
2830
- <button class="auth-submit-btn" @click="submitPassword" :disabled="!authPassword.trim()">{{ t('auth.unlock') }}</button>
2831
- </div>
2832
- </div>
2833
- </div>
2834
-
2835
- <!-- Auth Locked Out -->
2836
- <div class="folder-picker-overlay" v-if="authLocked">
2837
- <div class="auth-dialog auth-dialog-locked">
2838
- <div class="auth-dialog-header">
2839
- <svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
2840
- <span>{{ t('auth.accessLocked') }}</span>
2841
- </div>
2842
- <div class="auth-dialog-body">
2843
- <p>{{ authError }}</p>
2844
- <p class="auth-locked-hint">{{ t('auth.tryAgainLater') }}</p>
2845
- </div>
2846
- </div>
2847
- </div>
2848
-
2849
- <!-- Workdir switching overlay -->
2850
- <Transition name="fade">
2851
- <div v-if="workdirSwitching" class="workdir-switching-overlay">
2852
- <div class="workdir-switching-spinner"></div>
2853
- <div class="workdir-switching-text">{{ t('workdir.switching') }}</div>
2854
- </div>
2855
- </Transition>
2856
-
2857
- <!-- File context menu -->
2858
- <div
2859
- v-if="fileContextMenu"
2860
- class="file-context-menu"
2861
- :style="{ left: fileContextMenu.x + 'px', top: fileContextMenu.y + 'px' }"
2862
- >
2863
- <div class="file-context-item" @click="fileBrowser.askClaudeRead()">
2864
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM5 15h14v2H5zm0-4h14v2H5zm0-4h14v2H5z"/></svg>
2865
- {{ t('contextMenu.askClaudeRead') }}
2866
- </div>
2867
- <div class="file-context-item" @click="fileBrowser.copyPath()">
2868
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
2869
- {{ fileContextMenu.copied ? t('contextMenu.copied') : t('contextMenu.copyPath') }}
2870
- </div>
2871
- <div class="file-context-item" @click="fileBrowser.insertPath()">
2872
- <svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
2873
- {{ t('contextMenu.insertPath') }}
2874
- </div>
2875
- </div>
2876
- </div>
2877
- `
2878
- };
2879
-
2880
- const app = createApp(App);
2881
- app.mount('#app');