@agent-link/server 0.1.184 → 0.1.186
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/package.json +1 -1
- package/web/app.js +75 -11
- package/web/css/ask-question.css +15 -3
- package/web/css/base.css +23 -0
- package/web/css/chat.css +2 -0
- package/web/css/file-browser.css +0 -4
- package/web/css/input.css +51 -5
- package/web/css/loop.css +3 -11
- package/web/css/sidebar.css +1 -11
- package/web/css/team.css +9 -9
- package/web/css/tools.css +23 -2
- package/web/locales/en.json +4 -0
- package/web/locales/zh.json +4 -0
- package/web/modules/appHelpers.js +2 -1
- package/web/modules/backgroundRouting.js +12 -2
- package/web/modules/connection.js +25 -1
- package/web/modules/messageHelpers.js +17 -4
- package/web/modules/streaming.js +16 -10
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -127,6 +127,10 @@ const App = {
|
|
|
127
127
|
const currentConversationId = ref(crypto.randomUUID()); // currently visible conversation
|
|
128
128
|
const processingConversations = ref({}); // conversationId → boolean
|
|
129
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
|
+
|
|
130
134
|
// File browser state
|
|
131
135
|
const filePanelOpen = ref(false);
|
|
132
136
|
const filePanelWidth = ref(parseInt(localStorage.getItem('agentlink-file-panel-width'), 10) || 280);
|
|
@@ -240,6 +244,7 @@ const App = {
|
|
|
240
244
|
messageIdCounter: streaming.getMessageIdCounter(),
|
|
241
245
|
queuedMessages: queuedMessages.value,
|
|
242
246
|
usageStats: usageStats.value,
|
|
247
|
+
planMode: planMode.value,
|
|
243
248
|
};
|
|
244
249
|
}
|
|
245
250
|
|
|
@@ -260,6 +265,7 @@ const App = {
|
|
|
260
265
|
_restoreToolMsgMap(cached.toolMsgMap || new Map());
|
|
261
266
|
queuedMessages.value = cached.queuedMessages || [];
|
|
262
267
|
usageStats.value = cached.usageStats || null;
|
|
268
|
+
planMode.value = cached.planMode || false;
|
|
263
269
|
} else {
|
|
264
270
|
// New blank conversation
|
|
265
271
|
messages.value = [];
|
|
@@ -275,6 +281,7 @@ const App = {
|
|
|
275
281
|
_clearToolMsgMap();
|
|
276
282
|
queuedMessages.value = [];
|
|
277
283
|
usageStats.value = null;
|
|
284
|
+
planMode.value = false;
|
|
278
285
|
}
|
|
279
286
|
|
|
280
287
|
currentConversationId.value = newConvId;
|
|
@@ -364,6 +371,8 @@ const App = {
|
|
|
364
371
|
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
365
372
|
// Side question (/btw)
|
|
366
373
|
btwState, btwPending,
|
|
374
|
+
// Plan mode
|
|
375
|
+
setPlanMode,
|
|
367
376
|
// i18n
|
|
368
377
|
t,
|
|
369
378
|
});
|
|
@@ -406,8 +415,15 @@ const App = {
|
|
|
406
415
|
});
|
|
407
416
|
setFilePreview(filePreview);
|
|
408
417
|
|
|
409
|
-
// Track mobile state on resize
|
|
410
|
-
let
|
|
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
|
+
};
|
|
411
427
|
window.addEventListener('resize', _resizeHandler);
|
|
412
428
|
|
|
413
429
|
// Close workdir menu on outside click or Escape
|
|
@@ -426,10 +442,11 @@ const App = {
|
|
|
426
442
|
|
|
427
443
|
// ── Computed ──
|
|
428
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));
|
|
429
446
|
const canSend = computed(() =>
|
|
430
|
-
status.value === 'Connected' && hasInput.value && !isCompacting.value
|
|
431
|
-
&& !messages.value.some(m => m.role === 'ask-question' && !m.answered)
|
|
447
|
+
status.value === 'Connected' && hasInput.value && !isCompacting.value && !hasPendingQuestion.value
|
|
432
448
|
);
|
|
449
|
+
const hasStreamingMessage = computed(() => messages.value.some(m => m.isStreaming));
|
|
433
450
|
|
|
434
451
|
// ── Slash command menu ──
|
|
435
452
|
const slashMenuVisible = computed(() => {
|
|
@@ -558,6 +575,28 @@ const App = {
|
|
|
558
575
|
if (idx !== -1) queuedMessages.value.splice(idx, 1);
|
|
559
576
|
}
|
|
560
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
|
+
|
|
561
600
|
function selectSlashCommand(cmd) {
|
|
562
601
|
slashMenuOpen.value = false;
|
|
563
602
|
if (cmd.isPrefix) {
|
|
@@ -695,9 +734,11 @@ const App = {
|
|
|
695
734
|
status, agentName, hostname, workDir, sessionId, error,
|
|
696
735
|
serverVersion, agentVersion, latency,
|
|
697
736
|
messages, visibleMessages, hasMoreMessages, loadMoreMessages,
|
|
698
|
-
inputText, isProcessing, isCompacting, canSend, hasInput, inputRef, queuedMessages, usageStats,
|
|
737
|
+
inputText, isProcessing, isCompacting, canSend, hasInput, hasStreamingMessage, inputRef, queuedMessages, usageStats,
|
|
699
738
|
slashMenuVisible, filteredSlashCommands, slashMenuIndex, slashMenuOpen, selectSlashCommand, openSlashMenu,
|
|
700
739
|
sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
|
|
740
|
+
// Plan mode
|
|
741
|
+
planMode, pendingPlanMode, togglePlanMode,
|
|
701
742
|
// Side question (/btw)
|
|
702
743
|
btwState, btwPending, dismissBtw, renderMarkdown,
|
|
703
744
|
getRenderedContent, copyMessage, toggleTool,
|
|
@@ -2021,6 +2062,12 @@ const App = {
|
|
|
2021
2062
|
<div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
2022
2063
|
</div>
|
|
2023
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>
|
|
2024
2071
|
<!-- Agent tool use -->
|
|
2025
2072
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2026
2073
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
@@ -2033,7 +2080,7 @@ const App = {
|
|
|
2033
2080
|
</span>
|
|
2034
2081
|
<span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
|
|
2035
2082
|
</div>
|
|
2036
|
-
<div v-
|
|
2083
|
+
<div v-if="msg.expanded" class="tool-expand">
|
|
2037
2084
|
<div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
|
|
2038
2085
|
<div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
|
|
2039
2086
|
<pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
|
|
@@ -2079,6 +2126,11 @@ const App = {
|
|
|
2079
2126
|
<div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
2080
2127
|
</div>
|
|
2081
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>
|
|
2082
2134
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2083
2135
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
2084
2136
|
<span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
|
|
@@ -2090,7 +2142,7 @@ const App = {
|
|
|
2090
2142
|
</span>
|
|
2091
2143
|
<span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
|
|
2092
2144
|
</div>
|
|
2093
|
-
<div v-
|
|
2145
|
+
<div v-if="msg.expanded" class="tool-expand">
|
|
2094
2146
|
<div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
|
|
2095
2147
|
<div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
|
|
2096
2148
|
<pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
|
|
@@ -2394,7 +2446,7 @@ const App = {
|
|
|
2394
2446
|
</span>
|
|
2395
2447
|
<span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
|
|
2396
2448
|
</div>
|
|
2397
|
-
<div v-
|
|
2449
|
+
<div v-if="msg.expanded" class="tool-expand team-agent-tool-expand">
|
|
2398
2450
|
<pre v-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
|
|
2399
2451
|
<div v-if="msg.toolOutput" class="team-agent-tool-result">
|
|
2400
2452
|
<div class="team-agent-tool-result-label">{{ t('team.agentResult') }}</div>
|
|
@@ -2403,6 +2455,13 @@ const App = {
|
|
|
2403
2455
|
</div>
|
|
2404
2456
|
</div>
|
|
2405
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
|
+
|
|
2406
2465
|
<!-- Tool use block (collapsible) -->
|
|
2407
2466
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2408
2467
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
@@ -2415,7 +2474,7 @@ const App = {
|
|
|
2415
2474
|
</span>
|
|
2416
2475
|
<span class="tool-toggle">{{ msg.expanded ? '\u{25B2}' : '\u{25BC}' }}</span>
|
|
2417
2476
|
</div>
|
|
2418
|
-
<div v-
|
|
2477
|
+
<div v-if="msg.expanded" class="tool-expand">
|
|
2419
2478
|
<div v-if="isEditTool(msg) && getEditDiffHtml(msg)" class="tool-diff" v-html="getEditDiffHtml(msg)"></div>
|
|
2420
2479
|
<div v-else-if="getFormattedToolInput(msg)" class="tool-input-formatted" v-html="getFormattedToolInput(msg)"></div>
|
|
2421
2480
|
<pre v-else-if="msg.toolInput" class="tool-block">{{ msg.toolInput }}</pre>
|
|
@@ -2490,8 +2549,9 @@ const App = {
|
|
|
2490
2549
|
</div>
|
|
2491
2550
|
</div>
|
|
2492
2551
|
|
|
2493
|
-
<div v-if="isProcessing && !
|
|
2552
|
+
<div v-if="isProcessing && !hasStreamingMessage" class="typing-indicator">
|
|
2494
2553
|
<span></span><span></span><span></span>
|
|
2554
|
+
<span v-if="pendingPlanMode" class="typing-label">{{ pendingPlanMode === 'enter' ? t('tool.enteringPlanMode') : t('tool.exitingPlanMode') }}</span>
|
|
2495
2555
|
</div>
|
|
2496
2556
|
</div>
|
|
2497
2557
|
</div>
|
|
@@ -2555,7 +2615,7 @@ const App = {
|
|
|
2555
2615
|
</div>
|
|
2556
2616
|
</div>
|
|
2557
2617
|
<div
|
|
2558
|
-
:class="['input-card', { 'drag-over': dragOver }]"
|
|
2618
|
+
:class="['input-card', { 'drag-over': dragOver, 'plan-mode': planMode }]"
|
|
2559
2619
|
@dragover="handleDragOver"
|
|
2560
2620
|
@dragleave="handleDragLeave"
|
|
2561
2621
|
@drop="handleDrop"
|
|
@@ -2591,6 +2651,10 @@ const App = {
|
|
|
2591
2651
|
<button class="slash-btn" @click="openSlashMenu" :disabled="status !== 'Connected'" :title="t('input.slashCommands')">
|
|
2592
2652
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 21 11 3h2L9 21H7Z"/></svg>
|
|
2593
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>
|
|
2594
2658
|
</div>
|
|
2595
2659
|
<button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" :title="t('input.stopGeneration')">
|
|
2596
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>
|
package/web/css/ask-question.css
CHANGED
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
padding: 0.5rem 0.9rem;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
.typing-indicator span {
|
|
274
|
+
.typing-indicator span:not(.typing-label) {
|
|
275
275
|
width: 6px;
|
|
276
276
|
height: 6px;
|
|
277
277
|
border-radius: 50%;
|
|
@@ -279,11 +279,11 @@
|
|
|
279
279
|
animation: typing 1.2s infinite ease-in-out;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
.typing-indicator span:nth-child(2) {
|
|
282
|
+
.typing-indicator span:not(.typing-label):nth-child(2) {
|
|
283
283
|
animation-delay: 0.2s;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
.typing-indicator span:nth-child(3) {
|
|
286
|
+
.typing-indicator span:not(.typing-label):nth-child(3) {
|
|
287
287
|
animation-delay: 0.4s;
|
|
288
288
|
}
|
|
289
289
|
|
|
@@ -292,6 +292,18 @@
|
|
|
292
292
|
30% { opacity: 1; transform: scale(1); }
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
.typing-label {
|
|
296
|
+
font-size: 0.78rem;
|
|
297
|
+
color: var(--text-secondary);
|
|
298
|
+
margin-left: 4px;
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
animation: none;
|
|
301
|
+
width: auto;
|
|
302
|
+
height: auto;
|
|
303
|
+
border-radius: 0;
|
|
304
|
+
background: none;
|
|
305
|
+
}
|
|
306
|
+
|
|
295
307
|
/* ── Context compaction inline message ── */
|
|
296
308
|
.compact-msg {
|
|
297
309
|
display: inline-flex;
|
package/web/css/base.css
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
--border: #353535;
|
|
31
31
|
--code-bg: #1a1a1a;
|
|
32
32
|
--code-header-bg: #222222;
|
|
33
|
+
--plan-mode: #d4a24c;
|
|
34
|
+
--plan-mode-bg: rgba(212, 162, 76, 0.1);
|
|
33
35
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -48,6 +50,8 @@
|
|
|
48
50
|
--border: #d1d5db;
|
|
49
51
|
--code-bg: #f1f3f5;
|
|
50
52
|
--code-header-bg: #e9ecef;
|
|
53
|
+
--plan-mode: #d97706;
|
|
54
|
+
--plan-mode-bg: rgba(217, 119, 6, 0.08);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
html {
|
|
@@ -236,6 +240,25 @@ body {
|
|
|
236
240
|
margin-top: 0.75rem;
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
/* ── Shared keyframes ── */
|
|
244
|
+
@keyframes spin {
|
|
245
|
+
to { transform: rotate(360deg); }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@keyframes pulse {
|
|
249
|
+
0%, 100% { opacity: 1; }
|
|
250
|
+
50% { opacity: 0.3; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── Reduced motion ── */
|
|
254
|
+
@media (prefers-reduced-motion: reduce) {
|
|
255
|
+
*, *::before, *::after {
|
|
256
|
+
animation-duration: 0.01ms !important;
|
|
257
|
+
animation-iteration-count: 1 !important;
|
|
258
|
+
transition-duration: 0.01ms !important;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
239
262
|
/* ── Main body (sidebar + chat) ── */
|
|
240
263
|
.main-body {
|
|
241
264
|
flex: 1;
|
package/web/css/chat.css
CHANGED
package/web/css/file-browser.css
CHANGED
package/web/css/input.css
CHANGED
|
@@ -225,7 +225,6 @@
|
|
|
225
225
|
align-items: center;
|
|
226
226
|
justify-content: center;
|
|
227
227
|
gap: 16px;
|
|
228
|
-
backdrop-filter: blur(2px);
|
|
229
228
|
}
|
|
230
229
|
.workdir-switching-spinner {
|
|
231
230
|
width: 36px;
|
|
@@ -233,10 +232,7 @@
|
|
|
233
232
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
|
234
233
|
border-top-color: rgba(255, 255, 255, 0.8);
|
|
235
234
|
border-radius: 50%;
|
|
236
|
-
animation:
|
|
237
|
-
}
|
|
238
|
-
@keyframes workdir-spin {
|
|
239
|
-
to { transform: rotate(360deg); }
|
|
235
|
+
animation: spin 0.7s linear infinite;
|
|
240
236
|
}
|
|
241
237
|
.workdir-switching-text {
|
|
242
238
|
color: rgba(255, 255, 255, 0.9);
|
|
@@ -618,6 +614,56 @@
|
|
|
618
614
|
color: var(--text-secondary);
|
|
619
615
|
}
|
|
620
616
|
|
|
617
|
+
/* ── Plan Mode toggle button ── */
|
|
618
|
+
.plan-mode-btn {
|
|
619
|
+
background: none;
|
|
620
|
+
border: 1px solid var(--border);
|
|
621
|
+
color: var(--text-secondary);
|
|
622
|
+
cursor: pointer;
|
|
623
|
+
display: flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
gap: 4px;
|
|
626
|
+
height: 28px;
|
|
627
|
+
padding: 0 8px;
|
|
628
|
+
border-radius: 6px;
|
|
629
|
+
font-size: 0.75rem;
|
|
630
|
+
font-weight: 600;
|
|
631
|
+
letter-spacing: 0.02em;
|
|
632
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
633
|
+
white-space: nowrap;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.plan-mode-btn:hover:not(:disabled) {
|
|
637
|
+
color: var(--text-primary);
|
|
638
|
+
background: var(--bg-tertiary);
|
|
639
|
+
border-color: var(--text-secondary);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.plan-mode-btn.active {
|
|
643
|
+
color: var(--plan-mode);
|
|
644
|
+
background: var(--plan-mode-bg);
|
|
645
|
+
border-color: var(--plan-mode);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.plan-mode-btn.active:hover:not(:disabled) {
|
|
649
|
+
background: rgba(212, 162, 76, 0.18);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.plan-mode-btn:disabled {
|
|
653
|
+
opacity: 0.35;
|
|
654
|
+
cursor: not-allowed;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.plan-mode-btn svg {
|
|
658
|
+
flex-shrink: 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
/* ── Input card plan mode accent ── */
|
|
663
|
+
.input-card.plan-mode {
|
|
664
|
+
border-top: 2px solid var(--plan-mode);
|
|
665
|
+
}
|
|
666
|
+
|
|
621
667
|
/* ── Sidebar backdrop (mobile overlay) ── */
|
|
622
668
|
.sidebar-backdrop {
|
|
623
669
|
display: none;
|
package/web/css/loop.css
CHANGED
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
border: 1px solid var(--border);
|
|
186
186
|
border-radius: 6px;
|
|
187
187
|
cursor: pointer;
|
|
188
|
-
transition:
|
|
188
|
+
transition: color 0.15s, border-color 0.15s;
|
|
189
189
|
white-space: nowrap;
|
|
190
190
|
}
|
|
191
191
|
.loop-action-btn:hover:not(:disabled) {
|
|
@@ -353,11 +353,7 @@
|
|
|
353
353
|
}
|
|
354
354
|
.loop-exec-status-running {
|
|
355
355
|
color: var(--accent);
|
|
356
|
-
animation:
|
|
357
|
-
}
|
|
358
|
-
@keyframes loop-spin {
|
|
359
|
-
from { transform: rotate(0deg); }
|
|
360
|
-
to { transform: rotate(360deg); }
|
|
356
|
+
animation: spin 1s linear infinite;
|
|
361
357
|
}
|
|
362
358
|
.loop-exec-status-success {
|
|
363
359
|
color: #10B981;
|
|
@@ -428,11 +424,7 @@
|
|
|
428
424
|
height: 8px;
|
|
429
425
|
border-radius: 50%;
|
|
430
426
|
background: var(--accent);
|
|
431
|
-
animation:
|
|
432
|
-
}
|
|
433
|
-
@keyframes loop-pulse {
|
|
434
|
-
0%, 100% { opacity: 1; }
|
|
435
|
-
50% { opacity: 0.3; }
|
|
427
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
436
428
|
}
|
|
437
429
|
|
|
438
430
|
/* ── Modal dialog (generic) ── */
|
package/web/css/sidebar.css
CHANGED
|
@@ -210,11 +210,6 @@
|
|
|
210
210
|
transform: rotate(-90deg);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
@keyframes spin {
|
|
214
|
-
from { transform: rotate(0deg); }
|
|
215
|
-
to { transform: rotate(360deg); }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
213
|
.spinning {
|
|
219
214
|
animation: spin 0.8s linear infinite;
|
|
220
215
|
}
|
|
@@ -329,12 +324,7 @@
|
|
|
329
324
|
background: var(--accent);
|
|
330
325
|
margin-right: 6px;
|
|
331
326
|
vertical-align: middle;
|
|
332
|
-
animation: pulse
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
@keyframes pulse-dot {
|
|
336
|
-
0%, 100% { opacity: 1; }
|
|
337
|
-
50% { opacity: 0.3; }
|
|
327
|
+
animation: pulse 1.5s ease-in-out infinite;
|
|
338
328
|
}
|
|
339
329
|
|
|
340
330
|
.session-meta {
|
package/web/css/team.css
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
font-size: 0.75rem;
|
|
22
22
|
font-weight: 500;
|
|
23
23
|
cursor: pointer;
|
|
24
|
-
transition:
|
|
24
|
+
transition: color 0.15s, background 0.15s, box-shadow 0.15s;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.team-mode-btn.active {
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
color: var(--text-secondary);
|
|
195
195
|
font-size: 0.78rem;
|
|
196
196
|
cursor: pointer;
|
|
197
|
-
transition:
|
|
197
|
+
transition: color 0.15s, border-color 0.15s;
|
|
198
198
|
}
|
|
199
199
|
.team-lead-prompt-reset:hover {
|
|
200
200
|
color: var(--text-primary);
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
color: var(--text-secondary);
|
|
259
259
|
font-size: 0.85rem;
|
|
260
260
|
cursor: pointer;
|
|
261
|
-
transition:
|
|
261
|
+
transition: color 0.15s, border-color 0.15s;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
.team-create-cancel:hover {
|
|
@@ -293,7 +293,7 @@
|
|
|
293
293
|
padding: 14px 16px;
|
|
294
294
|
border: 1px solid var(--border);
|
|
295
295
|
border-radius: 10px;
|
|
296
|
-
transition:
|
|
296
|
+
transition: border-color 0.15s, background 0.15s;
|
|
297
297
|
background: linear-gradient(135deg, rgba(255,255,255,0.02) 0%, transparent 100%);
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
border: 1px solid var(--accent);
|
|
355
355
|
border-radius: 6px;
|
|
356
356
|
cursor: pointer;
|
|
357
|
-
transition:
|
|
357
|
+
transition: color 0.15s, background 0.15s;
|
|
358
358
|
white-space: nowrap;
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -500,7 +500,7 @@
|
|
|
500
500
|
color: var(--error);
|
|
501
501
|
font-size: 0.8rem;
|
|
502
502
|
cursor: pointer;
|
|
503
|
-
transition:
|
|
503
|
+
transition: background 0.15s;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
.team-dissolve-btn:hover {
|
|
@@ -515,7 +515,7 @@
|
|
|
515
515
|
color: var(--text-secondary);
|
|
516
516
|
font-size: 0.8rem;
|
|
517
517
|
cursor: pointer;
|
|
518
|
-
transition:
|
|
518
|
+
transition: color 0.15s, border-color 0.15s;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
.team-back-btn:hover {
|
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
color: var(--accent);
|
|
532
532
|
font-size: 0.8rem;
|
|
533
533
|
cursor: pointer;
|
|
534
|
-
transition:
|
|
534
|
+
transition: background 0.15s;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
.team-new-btn:hover {
|
|
@@ -1111,7 +1111,7 @@
|
|
|
1111
1111
|
color: var(--text-secondary);
|
|
1112
1112
|
font-size: 0.78rem;
|
|
1113
1113
|
cursor: pointer;
|
|
1114
|
-
transition:
|
|
1114
|
+
transition: color 0.15s, border-color 0.15s;
|
|
1115
1115
|
margin-right: 4px;
|
|
1116
1116
|
}
|
|
1117
1117
|
|
package/web/css/tools.css
CHANGED
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
@keyframes toolExpand {
|
|
100
|
-
from { opacity: 0;
|
|
101
|
-
to { opacity: 1;
|
|
100
|
+
from { opacity: 0; }
|
|
101
|
+
to { opacity: 1; }
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
.tool-block {
|
|
@@ -304,3 +304,24 @@
|
|
|
304
304
|
margin: 0.75rem 0;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
/* ── Plan mode divider ── */
|
|
308
|
+
.plan-mode-divider {
|
|
309
|
+
display: flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 12px;
|
|
312
|
+
margin: 8px 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.plan-mode-divider-line {
|
|
316
|
+
flex: 1;
|
|
317
|
+
height: 1px;
|
|
318
|
+
background: var(--border);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.plan-mode-divider-text {
|
|
322
|
+
font-size: 0.75rem;
|
|
323
|
+
color: var(--text-secondary);
|
|
324
|
+
white-space: nowrap;
|
|
325
|
+
font-weight: 500;
|
|
326
|
+
}
|
|
327
|
+
|
package/web/locales/en.json
CHANGED
|
@@ -245,6 +245,10 @@
|
|
|
245
245
|
"tool.inPath": "in {path}",
|
|
246
246
|
"tool.done": "{done}/{total} done",
|
|
247
247
|
"tool.replaceAll": "(replace all)",
|
|
248
|
+
"tool.enteredPlanMode": "Entered Plan Mode",
|
|
249
|
+
"tool.exitedPlanMode": "Exited Plan Mode",
|
|
250
|
+
"tool.enteringPlanMode": "Entering Plan Mode...",
|
|
251
|
+
"tool.exitingPlanMode": "Exiting Plan Mode...",
|
|
248
252
|
|
|
249
253
|
"usage.context": "Context",
|
|
250
254
|
"usage.cost": "Cost",
|
package/web/locales/zh.json
CHANGED
|
@@ -245,6 +245,10 @@
|
|
|
245
245
|
"tool.inPath": "在 {path}",
|
|
246
246
|
"tool.done": "{done}/{total} 已完成",
|
|
247
247
|
"tool.replaceAll": "(全部替换)",
|
|
248
|
+
"tool.enteredPlanMode": "已进入计划模式",
|
|
249
|
+
"tool.exitedPlanMode": "已退出计划模式",
|
|
250
|
+
"tool.enteringPlanMode": "正在进入计划模式...",
|
|
251
|
+
"tool.exitingPlanMode": "正在退出计划模式...",
|
|
248
252
|
|
|
249
253
|
"usage.context": "上下文",
|
|
250
254
|
"usage.cost": "费用",
|
|
@@ -44,7 +44,8 @@ export function createHighlightScheduler() {
|
|
|
44
44
|
_hlTimer = setTimeout(() => {
|
|
45
45
|
_hlTimer = null;
|
|
46
46
|
if (typeof hljs !== 'undefined') {
|
|
47
|
-
document.
|
|
47
|
+
const root = document.querySelector('.message-list') || document;
|
|
48
|
+
root.querySelectorAll('pre code:not([data-highlighted])').forEach(block => {
|
|
48
49
|
hljs.highlightElement(block);
|
|
49
50
|
block.dataset.highlighted = 'true';
|
|
50
51
|
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// ── History batch building & background conversation routing ──────────────────
|
|
2
2
|
import { isContextSummary } from './messageHelpers.js';
|
|
3
3
|
|
|
4
|
+
function findLast(arr, predicate) {
|
|
5
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
6
|
+
if (predicate(arr[i])) return arr[i];
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
4
11
|
/**
|
|
5
12
|
* Convert a history array (from conversation_resumed) into a batch of UI messages.
|
|
6
13
|
* @param {Array} history - Array of {role, content, ...} from the agent
|
|
@@ -133,11 +140,14 @@ export function routeToBackgroundConversation(deps, convId, msg) {
|
|
|
133
140
|
const msgs = cache.messages;
|
|
134
141
|
const last = msgs.length > 0 ? msgs[msgs.length - 1] : null;
|
|
135
142
|
if (last && last.role === 'assistant' && last.isStreaming) {
|
|
136
|
-
last.
|
|
143
|
+
if (!last._chunks) last._chunks = [last.content];
|
|
144
|
+
last._chunks.push(data.delta);
|
|
145
|
+
last.content = last._chunks.join('');
|
|
137
146
|
} else {
|
|
138
147
|
msgs.push({
|
|
139
148
|
id: ++cache.messageIdCounter, role: 'assistant',
|
|
140
149
|
content: data.delta, isStreaming: true, timestamp: new Date(),
|
|
150
|
+
_chunks: [data.delta],
|
|
141
151
|
});
|
|
142
152
|
}
|
|
143
153
|
} else if (data.type === 'tool_use' && data.tools) {
|
|
@@ -206,7 +216,7 @@ export function routeToBackgroundConversation(deps, convId, msg) {
|
|
|
206
216
|
});
|
|
207
217
|
} else if (msg.status === 'completed') {
|
|
208
218
|
cache.isCompacting = false;
|
|
209
|
-
const startMsg =
|
|
219
|
+
const startMsg = findLast(cache.messages, m => m.isCompactStart && !m.compactDone);
|
|
210
220
|
if (startMsg) {
|
|
211
221
|
startMsg.content = 'Context compacted';
|
|
212
222
|
startMsg.compactDone = true;
|
|
@@ -7,6 +7,13 @@ const MAX_RECONNECT_ATTEMPTS = 50;
|
|
|
7
7
|
const RECONNECT_BASE_DELAY = 1000;
|
|
8
8
|
const RECONNECT_MAX_DELAY = 15000;
|
|
9
9
|
|
|
10
|
+
function findLast(arr, predicate) {
|
|
11
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
12
|
+
if (predicate(arr[i])) return arr[i];
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Creates the WebSocket connection controller.
|
|
12
19
|
* @param {object} deps - All reactive state and callbacks needed
|
|
@@ -29,6 +36,8 @@ export function createConnection(deps) {
|
|
|
29
36
|
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
30
37
|
// Side question (/btw)
|
|
31
38
|
btwState, btwPending,
|
|
39
|
+
// Plan mode
|
|
40
|
+
setPlanMode,
|
|
32
41
|
// i18n
|
|
33
42
|
t,
|
|
34
43
|
} = deps;
|
|
@@ -469,7 +478,7 @@ export function createConnection(deps) {
|
|
|
469
478
|
} else if (msg.status === 'completed') {
|
|
470
479
|
isCompacting.value = false;
|
|
471
480
|
// Update the start message to show completed
|
|
472
|
-
const startMsg =
|
|
481
|
+
const startMsg = findLast(messages.value, m => m.isCompactStart && !m.compactDone);
|
|
473
482
|
if (startMsg) {
|
|
474
483
|
startMsg.content = t('system.contextCompacted');
|
|
475
484
|
startMsg.compactDone = true;
|
|
@@ -540,6 +549,10 @@ export function createConnection(deps) {
|
|
|
540
549
|
messages.value = buildHistoryBatch(msg.history, () => streaming.nextId());
|
|
541
550
|
toolMsgMap.clear();
|
|
542
551
|
}
|
|
552
|
+
// Detect plan mode from agent-provided flag
|
|
553
|
+
if (msg.planMode != null) {
|
|
554
|
+
if (setPlanMode) setPlanMode(!!msg.planMode);
|
|
555
|
+
}
|
|
543
556
|
loadingHistory.value = false;
|
|
544
557
|
// Restore live status from agent (compacting / processing)
|
|
545
558
|
if (msg.isCompacting) {
|
|
@@ -604,6 +617,17 @@ export function createConnection(deps) {
|
|
|
604
617
|
btwState.value.done = true;
|
|
605
618
|
}
|
|
606
619
|
}
|
|
620
|
+
} else if (msg.type === 'plan_mode_changed') {
|
|
621
|
+
if (setPlanMode) setPlanMode(msg.enabled);
|
|
622
|
+
// For the immediate path (no injected turn), clear isProcessing here
|
|
623
|
+
// because turn_completed will never arrive.
|
|
624
|
+
if (msg.immediate) {
|
|
625
|
+
isProcessing.value = false;
|
|
626
|
+
if (currentConversationId.value) {
|
|
627
|
+
processingConversations.value[currentConversationId.value] = false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// For the injected path, turn_completed handles isProcessing naturally.
|
|
607
631
|
} else if (msg.type === 'workdir_changed') {
|
|
608
632
|
workdirSwitching.value = false;
|
|
609
633
|
workDir.value = msg.workDir;
|
|
@@ -4,6 +4,13 @@ import { renderMarkdown } from './markdown.js';
|
|
|
4
4
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
5
|
const CONTEXT_SUMMARY_PREFIX = 'This session is being continued from a previous conversation';
|
|
6
6
|
|
|
7
|
+
function parseToolInput(msg) {
|
|
8
|
+
if (msg._parsedInput !== undefined) return msg._parsedInput;
|
|
9
|
+
try { msg._parsedInput = JSON.parse(msg.toolInput); }
|
|
10
|
+
catch { msg._parsedInput = null; }
|
|
11
|
+
return msg._parsedInput;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export function isContextSummary(text) {
|
|
8
15
|
return typeof text === 'string' && text.trimStart().startsWith(CONTEXT_SUMMARY_PREFIX);
|
|
9
16
|
}
|
|
@@ -28,6 +35,10 @@ export function formatTimestamp(ts) {
|
|
|
28
35
|
|
|
29
36
|
export function getRenderedContent(msg) {
|
|
30
37
|
if (msg.role !== 'assistant' && !msg.isCommandOutput) return msg.content;
|
|
38
|
+
if (msg.isStreaming) {
|
|
39
|
+
const t = msg.content || '';
|
|
40
|
+
return t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>');
|
|
41
|
+
}
|
|
31
42
|
return renderMarkdown(msg.content);
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -55,9 +66,9 @@ export function toggleTool(msg) {
|
|
|
55
66
|
|
|
56
67
|
export function getToolSummary(msg, t) {
|
|
57
68
|
const name = msg.toolName;
|
|
58
|
-
const
|
|
69
|
+
const obj = parseToolInput(msg);
|
|
70
|
+
if (!obj) return '';
|
|
59
71
|
try {
|
|
60
|
-
const obj = JSON.parse(input);
|
|
61
72
|
if (name === 'Read' && obj.file_path) return obj.file_path;
|
|
62
73
|
if (name === 'Edit' && obj.file_path) return obj.file_path;
|
|
63
74
|
if (name === 'Write' && obj.file_path) return obj.file_path;
|
|
@@ -83,8 +94,9 @@ export function isEditTool(msg) {
|
|
|
83
94
|
|
|
84
95
|
export function getFormattedToolInput(msg, t) {
|
|
85
96
|
if (!msg.toolInput) return null;
|
|
97
|
+
const obj = parseToolInput(msg);
|
|
98
|
+
if (!obj) return null;
|
|
86
99
|
try {
|
|
87
|
-
const obj = JSON.parse(msg.toolInput);
|
|
88
100
|
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
89
101
|
const name = msg.toolName;
|
|
90
102
|
|
|
@@ -169,8 +181,9 @@ export function getFormattedToolInput(msg, t) {
|
|
|
169
181
|
}
|
|
170
182
|
|
|
171
183
|
export function getEditDiffHtml(msg, t) {
|
|
184
|
+
const obj = parseToolInput(msg);
|
|
185
|
+
if (!obj) return null;
|
|
172
186
|
try {
|
|
173
|
-
const obj = JSON.parse(msg.toolInput);
|
|
174
187
|
if (!obj.old_string && !obj.new_string) return null;
|
|
175
188
|
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
176
189
|
const filePath = obj.file_path || '';
|
package/web/modules/streaming.js
CHANGED
|
@@ -34,19 +34,21 @@ export function createStreaming({ messages, scrollToBottom }) {
|
|
|
34
34
|
? messages.value.find(m => m.id === streamingMessageId)
|
|
35
35
|
: null;
|
|
36
36
|
|
|
37
|
+
const chunk = pendingText.slice(0, CHARS_PER_TICK);
|
|
38
|
+
pendingText = pendingText.slice(CHARS_PER_TICK);
|
|
39
|
+
|
|
37
40
|
if (!streamMsg) {
|
|
38
41
|
const id = ++messageIdCounter;
|
|
39
|
-
const
|
|
40
|
-
pendingText = pendingText.slice(CHARS_PER_TICK);
|
|
41
|
-
messages.value.push({
|
|
42
|
+
const newMsg = {
|
|
42
43
|
id, role: 'assistant', content: chunk,
|
|
43
44
|
isStreaming: true, timestamp: new Date(),
|
|
44
|
-
|
|
45
|
+
_chunks: [chunk],
|
|
46
|
+
};
|
|
47
|
+
messages.value.push(newMsg);
|
|
45
48
|
streamingMessageId = id;
|
|
46
49
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
streamMsg.content += chunk;
|
|
50
|
+
streamMsg._chunks.push(chunk);
|
|
51
|
+
streamMsg.content = streamMsg._chunks.join('');
|
|
50
52
|
}
|
|
51
53
|
scrollToBottom();
|
|
52
54
|
if (pendingText) revealTimer = setTimeout(revealTick, TICK_MS);
|
|
@@ -58,13 +60,17 @@ export function createStreaming({ messages, scrollToBottom }) {
|
|
|
58
60
|
const streamMsg = streamingMessageId !== null
|
|
59
61
|
? messages.value.find(m => m.id === streamingMessageId) : null;
|
|
60
62
|
if (streamMsg) {
|
|
61
|
-
streamMsg.
|
|
63
|
+
if (!streamMsg._chunks) streamMsg._chunks = [streamMsg.content];
|
|
64
|
+
streamMsg._chunks.push(pendingText);
|
|
65
|
+
streamMsg.content = streamMsg._chunks.join('');
|
|
62
66
|
} else {
|
|
63
67
|
const id = ++messageIdCounter;
|
|
64
|
-
|
|
68
|
+
const newMsg = {
|
|
65
69
|
id, role: 'assistant', content: pendingText,
|
|
66
70
|
isStreaming: true, timestamp: new Date(),
|
|
67
|
-
|
|
71
|
+
_chunks: [pendingText],
|
|
72
|
+
};
|
|
73
|
+
messages.value.push(newMsg);
|
|
68
74
|
streamingMessageId = id;
|
|
69
75
|
}
|
|
70
76
|
pendingText = '';
|