@agent-link/server 0.1.186 → 0.1.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +13 -15
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/web/dist/assets/index-C9bIrYkZ.js +320 -0
- package/web/dist/assets/index-C9bIrYkZ.js.map +1 -0
- package/web/dist/assets/index-Y1FN_mFe.css +1 -0
- package/web/{index.html → dist/index.html} +2 -19
- package/web/app.js +0 -2881
- package/web/css/ask-question.css +0 -333
- package/web/css/base.css +0 -270
- package/web/css/btw.css +0 -148
- package/web/css/chat.css +0 -176
- package/web/css/file-browser.css +0 -499
- package/web/css/input.css +0 -671
- package/web/css/loop.css +0 -674
- package/web/css/markdown.css +0 -169
- package/web/css/responsive.css +0 -314
- package/web/css/sidebar.css +0 -593
- package/web/css/team.css +0 -1277
- package/web/css/tools.css +0 -327
- package/web/encryption.js +0 -56
- package/web/modules/appHelpers.js +0 -100
- package/web/modules/askQuestion.js +0 -63
- package/web/modules/backgroundRouting.js +0 -269
- package/web/modules/connection.js +0 -731
- package/web/modules/fileAttachments.js +0 -125
- package/web/modules/fileBrowser.js +0 -379
- package/web/modules/filePreview.js +0 -213
- package/web/modules/i18n.js +0 -101
- package/web/modules/loop.js +0 -338
- package/web/modules/loopTemplates.js +0 -110
- package/web/modules/markdown.js +0 -83
- package/web/modules/messageHelpers.js +0 -206
- package/web/modules/sidebar.js +0 -402
- package/web/modules/streaming.js +0 -116
- package/web/modules/team.js +0 -396
- package/web/modules/teamTemplates.js +0 -360
- package/web/vendor/highlight.min.js +0 -1213
- package/web/vendor/marked.min.js +0 -6
- package/web/vendor/nacl-fast.min.js +0 -1
- package/web/vendor/nacl-util.min.js +0 -1
- package/web/vendor/pako.min.js +0 -2
- package/web/vendor/vue.global.prod.js +0 -13
- /package/web/{favicon.svg → dist/favicon.svg} +0 -0
- /package/web/{images → dist/images}/chat-iPad.webp +0 -0
- /package/web/{images → dist/images}/chat-iPhone.webp +0 -0
- /package/web/{images → dist/images}/loop-iPad.webp +0 -0
- /package/web/{images → dist/images}/team-iPad.webp +0 -0
- /package/web/{landing.html → dist/landing.html} +0 -0
- /package/web/{landing.zh.html → dist/landing.zh.html} +0 -0
- /package/web/{locales → dist/locales}/en.json +0 -0
- /package/web/{locales → dist/locales}/zh.json +0 -0
- /package/web/{vendor → dist/vendor}/github-dark.min.css +0 -0
- /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
|
-
navigator.clipboard.writeText(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 }">▶</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')">✓</button>
|
|
1505
|
-
<button class="session-rename-cancel" @click.stop="cancelRename" :title="t('sidebar.cancel')">×</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')">✓</button>
|
|
1575
|
-
<button class="session-rename-cancel" @click.stop="cancelTeamRename" :title="t('sidebar.cancel')">×</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')">✓</button>
|
|
1638
|
-
<button class="session-rename-cancel" @click.stop="cancelLoopRename" :title="t('sidebar.cancel')">×</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 }">▶</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')">✕</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')">×</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')">×</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')">×</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">×</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');
|