@agent-link/server 0.1.185 → 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 +57 -1
- package/web/css/ask-question.css +15 -3
- package/web/css/base.css +4 -0
- package/web/css/chat.css +2 -0
- package/web/css/input.css +50 -0
- package/web/css/tools.css +21 -0
- package/web/locales/en.json +4 -0
- package/web/locales/zh.json +4 -0
- package/web/modules/connection.js +17 -0
- package/web/modules/streaming.js +6 -4
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
|
});
|
|
@@ -566,6 +575,28 @@ const App = {
|
|
|
566
575
|
if (idx !== -1) queuedMessages.value.splice(idx, 1);
|
|
567
576
|
}
|
|
568
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
|
+
|
|
569
600
|
function selectSlashCommand(cmd) {
|
|
570
601
|
slashMenuOpen.value = false;
|
|
571
602
|
if (cmd.isPrefix) {
|
|
@@ -706,6 +737,8 @@ const App = {
|
|
|
706
737
|
inputText, isProcessing, isCompacting, canSend, hasInput, hasStreamingMessage, inputRef, queuedMessages, usageStats,
|
|
707
738
|
slashMenuVisible, filteredSlashCommands, slashMenuIndex, slashMenuOpen, selectSlashCommand, openSlashMenu,
|
|
708
739
|
sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
|
|
740
|
+
// Plan mode
|
|
741
|
+
planMode, pendingPlanMode, togglePlanMode,
|
|
709
742
|
// Side question (/btw)
|
|
710
743
|
btwState, btwPending, dismissBtw, renderMarkdown,
|
|
711
744
|
getRenderedContent, copyMessage, toggleTool,
|
|
@@ -2029,6 +2062,12 @@ const App = {
|
|
|
2029
2062
|
<div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
2030
2063
|
</div>
|
|
2031
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>
|
|
2032
2071
|
<!-- Agent tool use -->
|
|
2033
2072
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2034
2073
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
@@ -2087,6 +2126,11 @@ const App = {
|
|
|
2087
2126
|
<div class="message-content markdown-body" v-html="getRenderedContent(msg)"></div>
|
|
2088
2127
|
</div>
|
|
2089
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>
|
|
2090
2134
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2091
2135
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
2092
2136
|
<span class="tool-icon" v-html="getToolIcon(msg.toolName)"></span>
|
|
@@ -2411,6 +2455,13 @@ const App = {
|
|
|
2411
2455
|
</div>
|
|
2412
2456
|
</div>
|
|
2413
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
|
+
|
|
2414
2465
|
<!-- Tool use block (collapsible) -->
|
|
2415
2466
|
<div v-else-if="msg.role === 'tool'" class="tool-line-wrapper">
|
|
2416
2467
|
<div :class="['tool-line', { completed: msg.hasResult, running: !msg.hasResult }]" @click="toggleTool(msg)">
|
|
@@ -2500,6 +2551,7 @@ const App = {
|
|
|
2500
2551
|
|
|
2501
2552
|
<div v-if="isProcessing && !hasStreamingMessage" class="typing-indicator">
|
|
2502
2553
|
<span></span><span></span><span></span>
|
|
2554
|
+
<span v-if="pendingPlanMode" class="typing-label">{{ pendingPlanMode === 'enter' ? t('tool.enteringPlanMode') : t('tool.exitingPlanMode') }}</span>
|
|
2503
2555
|
</div>
|
|
2504
2556
|
</div>
|
|
2505
2557
|
</div>
|
|
@@ -2563,7 +2615,7 @@ const App = {
|
|
|
2563
2615
|
</div>
|
|
2564
2616
|
</div>
|
|
2565
2617
|
<div
|
|
2566
|
-
:class="['input-card', { 'drag-over': dragOver }]"
|
|
2618
|
+
:class="['input-card', { 'drag-over': dragOver, 'plan-mode': planMode }]"
|
|
2567
2619
|
@dragover="handleDragOver"
|
|
2568
2620
|
@dragleave="handleDragLeave"
|
|
2569
2621
|
@drop="handleDrop"
|
|
@@ -2599,6 +2651,10 @@ const App = {
|
|
|
2599
2651
|
<button class="slash-btn" @click="openSlashMenu" :disabled="status !== 'Connected'" :title="t('input.slashCommands')">
|
|
2600
2652
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 21 11 3h2L9 21H7Z"/></svg>
|
|
2601
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>
|
|
2602
2658
|
</div>
|
|
2603
2659
|
<button v-if="isProcessing && !hasInput" @click="cancelExecution" class="send-btn stop-btn" :title="t('input.stopGeneration')">
|
|
2604
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 {
|
package/web/css/chat.css
CHANGED
package/web/css/input.css
CHANGED
|
@@ -614,6 +614,56 @@
|
|
|
614
614
|
color: var(--text-secondary);
|
|
615
615
|
}
|
|
616
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
|
+
|
|
617
667
|
/* ── Sidebar backdrop (mobile overlay) ── */
|
|
618
668
|
.sidebar-backdrop {
|
|
619
669
|
display: none;
|
package/web/css/tools.css
CHANGED
|
@@ -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": "费用",
|
|
@@ -36,6 +36,8 @@ export function createConnection(deps) {
|
|
|
36
36
|
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
37
37
|
// Side question (/btw)
|
|
38
38
|
btwState, btwPending,
|
|
39
|
+
// Plan mode
|
|
40
|
+
setPlanMode,
|
|
39
41
|
// i18n
|
|
40
42
|
t,
|
|
41
43
|
} = deps;
|
|
@@ -547,6 +549,10 @@ export function createConnection(deps) {
|
|
|
547
549
|
messages.value = buildHistoryBatch(msg.history, () => streaming.nextId());
|
|
548
550
|
toolMsgMap.clear();
|
|
549
551
|
}
|
|
552
|
+
// Detect plan mode from agent-provided flag
|
|
553
|
+
if (msg.planMode != null) {
|
|
554
|
+
if (setPlanMode) setPlanMode(!!msg.planMode);
|
|
555
|
+
}
|
|
550
556
|
loadingHistory.value = false;
|
|
551
557
|
// Restore live status from agent (compacting / processing)
|
|
552
558
|
if (msg.isCompacting) {
|
|
@@ -611,6 +617,17 @@ export function createConnection(deps) {
|
|
|
611
617
|
btwState.value.done = true;
|
|
612
618
|
}
|
|
613
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.
|
|
614
631
|
} else if (msg.type === 'workdir_changed') {
|
|
615
632
|
workdirSwitching.value = false;
|
|
616
633
|
workDir.value = msg.workDir;
|
package/web/modules/streaming.js
CHANGED
|
@@ -39,11 +39,12 @@ export function createStreaming({ messages, scrollToBottom }) {
|
|
|
39
39
|
|
|
40
40
|
if (!streamMsg) {
|
|
41
41
|
const id = ++messageIdCounter;
|
|
42
|
-
|
|
42
|
+
const newMsg = {
|
|
43
43
|
id, role: 'assistant', content: chunk,
|
|
44
44
|
isStreaming: true, timestamp: new Date(),
|
|
45
45
|
_chunks: [chunk],
|
|
46
|
-
}
|
|
46
|
+
};
|
|
47
|
+
messages.value.push(newMsg);
|
|
47
48
|
streamingMessageId = id;
|
|
48
49
|
} else {
|
|
49
50
|
streamMsg._chunks.push(chunk);
|
|
@@ -64,11 +65,12 @@ export function createStreaming({ messages, scrollToBottom }) {
|
|
|
64
65
|
streamMsg.content = streamMsg._chunks.join('');
|
|
65
66
|
} else {
|
|
66
67
|
const id = ++messageIdCounter;
|
|
67
|
-
|
|
68
|
+
const newMsg = {
|
|
68
69
|
id, role: 'assistant', content: pendingText,
|
|
69
70
|
isStreaming: true, timestamp: new Date(),
|
|
70
71
|
_chunks: [pendingText],
|
|
71
|
-
}
|
|
72
|
+
};
|
|
73
|
+
messages.value.push(newMsg);
|
|
72
74
|
streamingMessageId = id;
|
|
73
75
|
}
|
|
74
76
|
pendingText = '';
|