@agent-link/server 0.1.162 → 0.1.163
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 +229 -7
- package/web/locales/en.json +18 -1
- package/web/locales/zh.json +18 -1
- package/web/modules/connection.js +25 -0
- package/web/modules/filePreview.js +10 -0
- package/web/style.css +159 -0
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -25,6 +25,13 @@ import { LOOP_TEMPLATES, LOOP_TEMPLATE_KEYS, buildCronExpression, formatSchedule
|
|
|
25
25
|
import { createScrollManager, createHighlightScheduler, formatUsage } from './modules/appHelpers.js';
|
|
26
26
|
import { createI18n } from './modules/i18n.js';
|
|
27
27
|
|
|
28
|
+
// ── Slash commands ──────────────────────────────────────────────────────────
|
|
29
|
+
const SLASH_COMMANDS = [
|
|
30
|
+
{ command: '/cost', descKey: 'slash.cost' },
|
|
31
|
+
{ command: '/context', descKey: 'slash.context' },
|
|
32
|
+
{ command: '/compact', descKey: 'slash.compact' },
|
|
33
|
+
];
|
|
34
|
+
|
|
28
35
|
// ── App ─────────────────────────────────────────────────────────────────────
|
|
29
36
|
const App = {
|
|
30
37
|
setup() {
|
|
@@ -59,6 +66,7 @@ const App = {
|
|
|
59
66
|
const queuedMessages = ref([]);
|
|
60
67
|
const usageStats = ref(null);
|
|
61
68
|
const inputRef = ref(null);
|
|
69
|
+
const slashMenuIndex = ref(0);
|
|
62
70
|
|
|
63
71
|
// Sidebar state
|
|
64
72
|
const sidebarOpen = ref(window.innerWidth > 768);
|
|
@@ -119,12 +127,21 @@ const App = {
|
|
|
119
127
|
const fileTreeRoot = ref(null);
|
|
120
128
|
const fileTreeLoading = ref(false);
|
|
121
129
|
const fileContextMenu = ref(null);
|
|
122
|
-
const sidebarView = ref('sessions'); // 'sessions' | 'files' | 'preview' (mobile only)
|
|
130
|
+
const sidebarView = ref('sessions'); // 'sessions' | 'files' | 'preview' | 'memory' (mobile only)
|
|
123
131
|
const isMobile = ref(window.innerWidth <= 768);
|
|
124
132
|
const workdirMenuOpen = ref(false);
|
|
125
133
|
const teamsCollapsed = ref(false);
|
|
126
134
|
const chatsCollapsed = ref(false);
|
|
127
135
|
const loopsCollapsed = ref(false);
|
|
136
|
+
|
|
137
|
+
// Memory management state
|
|
138
|
+
const memoryPanelOpen = ref(false);
|
|
139
|
+
const memoryFiles = ref([]);
|
|
140
|
+
const memoryDir = ref(null);
|
|
141
|
+
const memoryLoading = ref(false);
|
|
142
|
+
const memoryEditing = ref(false);
|
|
143
|
+
const memoryEditContent = ref('');
|
|
144
|
+
const memorySaving = ref(false);
|
|
128
145
|
const _sidebarCollapseKey = () => hostname.value ? `agentlink-sidebar-collapsed-${hostname.value}` : null;
|
|
129
146
|
const loadingTeams = ref(false);
|
|
130
147
|
const loadingLoops = ref(false);
|
|
@@ -184,6 +201,12 @@ const App = {
|
|
|
184
201
|
const previewFile = ref(null);
|
|
185
202
|
const previewLoading = ref(false);
|
|
186
203
|
const previewMarkdownRendered = ref(false);
|
|
204
|
+
const isMemoryPreview = computed(() => {
|
|
205
|
+
if (!previewFile.value?.filePath || !memoryDir.value) return false;
|
|
206
|
+
const fp = previewFile.value.filePath.replace(/\\/g, '/');
|
|
207
|
+
const md = memoryDir.value.replace(/\\/g, '/');
|
|
208
|
+
return fp.startsWith(md);
|
|
209
|
+
});
|
|
187
210
|
|
|
188
211
|
// ── switchConversation: save current → load target ──
|
|
189
212
|
// Defined here and used by sidebar.newConversation, sidebar.resumeSession, workdir_changed
|
|
@@ -331,6 +354,8 @@ const App = {
|
|
|
331
354
|
// Multi-session parallel
|
|
332
355
|
currentConversationId, processingConversations, conversationCache,
|
|
333
356
|
switchConversation,
|
|
357
|
+
// Memory management
|
|
358
|
+
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
334
359
|
// i18n
|
|
335
360
|
t,
|
|
336
361
|
});
|
|
@@ -398,6 +423,17 @@ const App = {
|
|
|
398
423
|
&& !messages.value.some(m => m.role === 'ask-question' && !m.answered)
|
|
399
424
|
);
|
|
400
425
|
|
|
426
|
+
// ── Slash command menu ──
|
|
427
|
+
const slashMenuVisible = computed(() => {
|
|
428
|
+
const txt = inputText.value;
|
|
429
|
+
return txt.startsWith('/') && !/\s/.test(txt.slice(1));
|
|
430
|
+
});
|
|
431
|
+
const filteredSlashCommands = computed(() => {
|
|
432
|
+
const txt = inputText.value.toLowerCase();
|
|
433
|
+
return SLASH_COMMANDS.filter(c => c.command.startsWith(txt));
|
|
434
|
+
});
|
|
435
|
+
watch(filteredSlashCommands, () => { slashMenuIndex.value = 0; });
|
|
436
|
+
|
|
401
437
|
// ── Auto-resize textarea ──
|
|
402
438
|
function autoResize() {
|
|
403
439
|
const ta = inputRef.value;
|
|
@@ -487,7 +523,42 @@ const App = {
|
|
|
487
523
|
if (idx !== -1) queuedMessages.value.splice(idx, 1);
|
|
488
524
|
}
|
|
489
525
|
|
|
526
|
+
function selectSlashCommand(cmd) {
|
|
527
|
+
inputText.value = cmd.command;
|
|
528
|
+
sendMessage();
|
|
529
|
+
}
|
|
530
|
+
|
|
490
531
|
function handleKeydown(e) {
|
|
532
|
+
// Slash menu key handling
|
|
533
|
+
if (slashMenuVisible.value && filteredSlashCommands.value.length > 0 && !e.isComposing) {
|
|
534
|
+
const len = filteredSlashCommands.value.length;
|
|
535
|
+
if (e.key === 'ArrowDown') {
|
|
536
|
+
e.preventDefault();
|
|
537
|
+
slashMenuIndex.value = (slashMenuIndex.value + 1) % len;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (e.key === 'ArrowUp') {
|
|
541
|
+
e.preventDefault();
|
|
542
|
+
slashMenuIndex.value = (slashMenuIndex.value - 1 + len) % len;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (e.key === 'Enter') {
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
selectSlashCommand(filteredSlashCommands.value[slashMenuIndex.value]);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (e.key === 'Tab') {
|
|
551
|
+
e.preventDefault();
|
|
552
|
+
inputText.value = filteredSlashCommands.value[slashMenuIndex.value].command;
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (e.key === 'Escape') {
|
|
556
|
+
e.preventDefault();
|
|
557
|
+
inputText.value = '';
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
491
562
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
492
563
|
e.preventDefault();
|
|
493
564
|
sendMessage();
|
|
@@ -554,6 +625,7 @@ const App = {
|
|
|
554
625
|
serverVersion, agentVersion, latency,
|
|
555
626
|
messages, visibleMessages, hasMoreMessages, loadMoreMessages,
|
|
556
627
|
inputText, isProcessing, isCompacting, canSend, hasInput, inputRef, queuedMessages, usageStats,
|
|
628
|
+
slashMenuVisible, filteredSlashCommands, slashMenuIndex, selectSlashCommand,
|
|
557
629
|
sendMessage, handleKeydown, cancelExecution, removeQueuedMessage, onMessageListScroll,
|
|
558
630
|
getRenderedContent, copyMessage, toggleTool,
|
|
559
631
|
isPrevAssistant: _isPrevAssistant,
|
|
@@ -670,6 +742,59 @@ const App = {
|
|
|
670
742
|
workdirMenuOpen.value = false;
|
|
671
743
|
navigator.clipboard.writeText(workDir.value);
|
|
672
744
|
},
|
|
745
|
+
// Memory management
|
|
746
|
+
memoryPanelOpen, memoryFiles, memoryDir, memoryLoading,
|
|
747
|
+
memoryEditing, memoryEditContent, memorySaving, isMemoryPreview,
|
|
748
|
+
workdirMenuMemory() {
|
|
749
|
+
workdirMenuOpen.value = false;
|
|
750
|
+
if (isMobile.value) {
|
|
751
|
+
sidebarView.value = 'memory';
|
|
752
|
+
} else {
|
|
753
|
+
memoryPanelOpen.value = !memoryPanelOpen.value;
|
|
754
|
+
if (memoryPanelOpen.value) filePanelOpen.value = false;
|
|
755
|
+
}
|
|
756
|
+
if (!memoryFiles.value.length) {
|
|
757
|
+
memoryLoading.value = true;
|
|
758
|
+
wsSend({ type: 'list_memory' });
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
refreshMemory() {
|
|
762
|
+
memoryLoading.value = true;
|
|
763
|
+
wsSend({ type: 'list_memory' });
|
|
764
|
+
},
|
|
765
|
+
openMemoryFile(file) {
|
|
766
|
+
memoryEditing.value = false;
|
|
767
|
+
memoryEditContent.value = '';
|
|
768
|
+
if (memoryDir.value) {
|
|
769
|
+
const sep = memoryDir.value.includes('\\') ? '\\' : '/';
|
|
770
|
+
filePreview.openPreview(memoryDir.value + sep + file.name);
|
|
771
|
+
}
|
|
772
|
+
if (isMobile.value) sidebarView.value = 'preview';
|
|
773
|
+
},
|
|
774
|
+
startMemoryEdit() {
|
|
775
|
+
memoryEditing.value = true;
|
|
776
|
+
memoryEditContent.value = previewFile.value?.content || '';
|
|
777
|
+
},
|
|
778
|
+
cancelMemoryEdit() {
|
|
779
|
+
if (memoryEditContent.value !== (previewFile.value?.content || '')) {
|
|
780
|
+
if (!confirm(t('memory.discardChanges'))) return;
|
|
781
|
+
}
|
|
782
|
+
memoryEditing.value = false;
|
|
783
|
+
memoryEditContent.value = '';
|
|
784
|
+
},
|
|
785
|
+
saveMemoryEdit() {
|
|
786
|
+
if (!previewFile.value) return;
|
|
787
|
+
memorySaving.value = true;
|
|
788
|
+
wsSend({
|
|
789
|
+
type: 'update_memory',
|
|
790
|
+
filename: previewFile.value.fileName,
|
|
791
|
+
content: memoryEditContent.value,
|
|
792
|
+
});
|
|
793
|
+
},
|
|
794
|
+
deleteMemoryFile(file) {
|
|
795
|
+
if (!confirm(t('memory.deleteConfirm', { name: file.name }))) return;
|
|
796
|
+
wsSend({ type: 'delete_memory', filename: file.name });
|
|
797
|
+
},
|
|
673
798
|
// Team mode
|
|
674
799
|
team,
|
|
675
800
|
teamState: team.teamState,
|
|
@@ -1077,18 +1202,28 @@ const App = {
|
|
|
1077
1202
|
<!-- Mobile: file preview view -->
|
|
1078
1203
|
<div v-else-if="isMobile && sidebarView === 'preview'" class="file-preview-mobile">
|
|
1079
1204
|
<div class="file-preview-mobile-header">
|
|
1080
|
-
<button class="file-panel-mobile-back" @click="filePreview.closePreview()">
|
|
1205
|
+
<button class="file-panel-mobile-back" @click="filePreview.closePreview(); memoryEditing = false">
|
|
1081
1206
|
<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>
|
|
1082
1207
|
{{ t('sidebar.files') }}
|
|
1083
1208
|
</button>
|
|
1084
1209
|
<div class="preview-header-actions">
|
|
1085
|
-
<button v-if="previewFile
|
|
1210
|
+
<button v-if="isMemoryPreview && previewFile && !memoryEditing"
|
|
1211
|
+
class="preview-edit-btn" @click="startMemoryEdit()" :title="t('memory.edit')">
|
|
1212
|
+
<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>
|
|
1213
|
+
{{ t('memory.edit') }}
|
|
1214
|
+
</button>
|
|
1215
|
+
<span v-if="memoryEditing" class="preview-edit-label">{{ t('memory.editing') }}</span>
|
|
1216
|
+
<button v-if="memoryEditing" class="memory-header-cancel" @click="cancelMemoryEdit()">{{ t('loop.cancel') }}</button>
|
|
1217
|
+
<button v-if="memoryEditing" class="memory-header-save" @click="saveMemoryEdit()" :disabled="memorySaving">
|
|
1218
|
+
{{ memorySaving ? t('memory.saving') : t('memory.save') }}
|
|
1219
|
+
</button>
|
|
1220
|
+
<button v-if="previewFile?.content && !memoryEditing && filePreview.isMarkdownFile(previewFile.fileName)"
|
|
1086
1221
|
class="preview-md-toggle" :class="{ active: previewMarkdownRendered }"
|
|
1087
1222
|
@click="previewMarkdownRendered = !previewMarkdownRendered"
|
|
1088
1223
|
:title="previewMarkdownRendered ? t('preview.showSource') : t('preview.renderMarkdown')">
|
|
1089
1224
|
<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>
|
|
1090
1225
|
</button>
|
|
1091
|
-
<span v-if="previewFile" class="file-preview-mobile-size">
|
|
1226
|
+
<span v-if="previewFile && !memoryEditing" class="file-preview-mobile-size">
|
|
1092
1227
|
{{ filePreview.formatFileSize(previewFile.totalSize) }}
|
|
1093
1228
|
</span>
|
|
1094
1229
|
</div>
|
|
@@ -1097,7 +1232,10 @@ const App = {
|
|
|
1097
1232
|
{{ previewFile?.fileName || t('preview.preview') }}
|
|
1098
1233
|
</div>
|
|
1099
1234
|
<div class="preview-panel-body">
|
|
1100
|
-
<div v-if="
|
|
1235
|
+
<div v-if="memoryEditing" class="memory-edit-container">
|
|
1236
|
+
<textarea class="memory-edit-textarea" v-model="memoryEditContent"></textarea>
|
|
1237
|
+
</div>
|
|
1238
|
+
<div v-else-if="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
|
|
1101
1239
|
<div v-else-if="previewFile?.error" class="preview-error">
|
|
1102
1240
|
{{ previewFile.error }}
|
|
1103
1241
|
</div>
|
|
@@ -1122,6 +1260,31 @@ const App = {
|
|
|
1122
1260
|
</div>
|
|
1123
1261
|
</div>
|
|
1124
1262
|
|
|
1263
|
+
<!-- Mobile: memory view -->
|
|
1264
|
+
<div v-else-if="isMobile && sidebarView === 'memory'" class="file-panel-mobile">
|
|
1265
|
+
<div class="file-panel-mobile-header">
|
|
1266
|
+
<button class="file-panel-mobile-back" @click="sidebarView = 'sessions'">
|
|
1267
|
+
<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>
|
|
1268
|
+
{{ t('sidebar.sessions') }}
|
|
1269
|
+
</button>
|
|
1270
|
+
<button class="file-panel-btn" @click="refreshMemory()" :title="t('sidebar.refresh')">
|
|
1271
|
+
<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>
|
|
1272
|
+
</button>
|
|
1273
|
+
</div>
|
|
1274
|
+
<div v-if="memoryLoading" class="file-panel-loading">{{ t('memory.loading') }}</div>
|
|
1275
|
+
<div v-else-if="memoryFiles.length === 0" class="memory-empty">
|
|
1276
|
+
<p>{{ t('memory.noFiles') }}</p>
|
|
1277
|
+
<p class="memory-empty-hint">{{ t('memory.noFilesHint') }}</p>
|
|
1278
|
+
</div>
|
|
1279
|
+
<div v-else class="file-tree">
|
|
1280
|
+
<div v-for="file in memoryFiles" :key="file.name"
|
|
1281
|
+
class="file-tree-item" @click="openMemoryFile(file)">
|
|
1282
|
+
<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>
|
|
1283
|
+
<span class="file-tree-name">{{ file.name }}</span>
|
|
1284
|
+
</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
</div>
|
|
1287
|
+
|
|
1125
1288
|
<!-- Normal sidebar content (sessions view) -->
|
|
1126
1289
|
<template v-else>
|
|
1127
1290
|
<div class="sidebar-section">
|
|
@@ -1150,6 +1313,10 @@ const App = {
|
|
|
1150
1313
|
<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>
|
|
1151
1314
|
<span>{{ t('sidebar.copyPath') }}</span>
|
|
1152
1315
|
</div>
|
|
1316
|
+
<div class="workdir-menu-item" @click.stop="workdirMenuMemory()">
|
|
1317
|
+
<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>
|
|
1318
|
+
<span>{{ t('sidebar.memory') }}</span>
|
|
1319
|
+
</div>
|
|
1153
1320
|
</div>
|
|
1154
1321
|
<div v-if="filteredWorkdirHistory.length > 0" class="workdir-history">
|
|
1155
1322
|
<div class="workdir-history-label">{{ t('sidebar.recentDirectories') }}</div>
|
|
@@ -1421,6 +1588,39 @@ const App = {
|
|
|
1421
1588
|
</div>
|
|
1422
1589
|
</Transition>
|
|
1423
1590
|
|
|
1591
|
+
<!-- Memory panel (desktop) -->
|
|
1592
|
+
<Transition name="file-panel">
|
|
1593
|
+
<div v-if="memoryPanelOpen && !isMobile" class="file-panel memory-panel">
|
|
1594
|
+
<div class="file-panel-header">
|
|
1595
|
+
<span class="file-panel-title">{{ t('memory.title') }}</span>
|
|
1596
|
+
<div class="file-panel-actions">
|
|
1597
|
+
<button class="file-panel-btn" @click="refreshMemory()" :title="t('sidebar.refresh')">
|
|
1598
|
+
<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>
|
|
1599
|
+
</button>
|
|
1600
|
+
<button class="file-panel-btn" @click="memoryPanelOpen = false" :title="t('sidebar.close')">
|
|
1601
|
+
<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>
|
|
1602
|
+
</button>
|
|
1603
|
+
</div>
|
|
1604
|
+
</div>
|
|
1605
|
+
<div v-if="memoryLoading" class="file-panel-loading">{{ t('memory.loading') }}</div>
|
|
1606
|
+
<div v-else-if="memoryFiles.length === 0" class="memory-empty">
|
|
1607
|
+
<p>{{ t('memory.noFiles') }}</p>
|
|
1608
|
+
<p class="memory-empty-hint">{{ t('memory.noFilesHint') }}</p>
|
|
1609
|
+
</div>
|
|
1610
|
+
<div v-else class="file-tree">
|
|
1611
|
+
<div v-for="file in memoryFiles" :key="file.name" class="file-tree-item memory-file-item">
|
|
1612
|
+
<div class="memory-file-row" @click="openMemoryFile(file)">
|
|
1613
|
+
<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>
|
|
1614
|
+
<span class="file-tree-name">{{ file.name }}</span>
|
|
1615
|
+
</div>
|
|
1616
|
+
<button class="memory-delete-btn" @click.stop="deleteMemoryFile(file)" :title="t('memory.deleteFile')">
|
|
1617
|
+
<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>
|
|
1618
|
+
</button>
|
|
1619
|
+
</div>
|
|
1620
|
+
</div>
|
|
1621
|
+
</div>
|
|
1622
|
+
</Transition>
|
|
1623
|
+
|
|
1424
1624
|
<!-- Chat area -->
|
|
1425
1625
|
<div class="chat-area">
|
|
1426
1626
|
|
|
@@ -2241,6 +2441,15 @@ const App = {
|
|
|
2241
2441
|
</div>
|
|
2242
2442
|
</div>
|
|
2243
2443
|
<div v-if="usageStats" class="usage-bar">{{ formatUsage(usageStats) }}</div>
|
|
2444
|
+
<div v-if="slashMenuVisible && filteredSlashCommands.length > 0" class="slash-menu">
|
|
2445
|
+
<div v-for="(cmd, i) in filteredSlashCommands" :key="cmd.command"
|
|
2446
|
+
:class="['slash-menu-item', { active: i === slashMenuIndex }]"
|
|
2447
|
+
@mouseenter="slashMenuIndex = i"
|
|
2448
|
+
@click="selectSlashCommand(cmd)">
|
|
2449
|
+
<span class="slash-menu-cmd">{{ cmd.command }}</span>
|
|
2450
|
+
<span class="slash-menu-desc">{{ t(cmd.descKey) }}</span>
|
|
2451
|
+
</div>
|
|
2452
|
+
</div>
|
|
2244
2453
|
<div
|
|
2245
2454
|
:class="['input-card', { 'drag-over': dragOver }]"
|
|
2246
2455
|
@dragover="handleDragOver"
|
|
@@ -2304,10 +2513,23 @@ const App = {
|
|
|
2304
2513
|
<span v-if="previewFile" class="preview-panel-size">
|
|
2305
2514
|
{{ filePreview.formatFileSize(previewFile.totalSize) }}
|
|
2306
2515
|
</span>
|
|
2307
|
-
<button
|
|
2516
|
+
<button v-if="isMemoryPreview && previewFile && !memoryEditing"
|
|
2517
|
+
class="preview-edit-btn" @click="startMemoryEdit()" :title="t('memory.edit')">
|
|
2518
|
+
<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>
|
|
2519
|
+
{{ t('memory.edit') }}
|
|
2520
|
+
</button>
|
|
2521
|
+
<span v-if="memoryEditing" class="preview-edit-label">{{ t('memory.editing') }}</span>
|
|
2522
|
+
<button v-if="memoryEditing" class="memory-header-cancel" @click="cancelMemoryEdit()">{{ t('loop.cancel') }}</button>
|
|
2523
|
+
<button v-if="memoryEditing" class="memory-header-save" @click="saveMemoryEdit()" :disabled="memorySaving">
|
|
2524
|
+
{{ memorySaving ? t('memory.saving') : t('memory.save') }}
|
|
2525
|
+
</button>
|
|
2526
|
+
<button class="preview-panel-close" @click="filePreview.closePreview(); memoryEditing = false" :title="t('preview.closePreview')">×</button>
|
|
2308
2527
|
</div>
|
|
2309
2528
|
<div class="preview-panel-body">
|
|
2310
|
-
<div v-if="
|
|
2529
|
+
<div v-if="memoryEditing" class="memory-edit-container">
|
|
2530
|
+
<textarea class="memory-edit-textarea" v-model="memoryEditContent"></textarea>
|
|
2531
|
+
</div>
|
|
2532
|
+
<div v-else-if="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
|
|
2311
2533
|
<div v-else-if="previewFile?.error" class="preview-error">
|
|
2312
2534
|
{{ previewFile.error }}
|
|
2313
2535
|
</div>
|
package/web/locales/en.json
CHANGED
|
@@ -246,5 +246,22 @@
|
|
|
246
246
|
"tool.replaceAll": "(replace all)",
|
|
247
247
|
|
|
248
248
|
"usage.context": "Context",
|
|
249
|
-
"usage.cost": "Cost"
|
|
249
|
+
"usage.cost": "Cost",
|
|
250
|
+
|
|
251
|
+
"sidebar.memory": "Memory",
|
|
252
|
+
"memory.title": "Memory Files",
|
|
253
|
+
"memory.loading": "Loading memory files...",
|
|
254
|
+
"memory.noFiles": "No memory files found.",
|
|
255
|
+
"memory.noFilesHint": "Claude will create memory files as you work.",
|
|
256
|
+
"memory.edit": "Edit",
|
|
257
|
+
"memory.editing": "Editing",
|
|
258
|
+
"memory.save": "Save",
|
|
259
|
+
"memory.saving": "Saving...",
|
|
260
|
+
"memory.deleteFile": "Delete",
|
|
261
|
+
"memory.deleteConfirm": "Delete {name}?",
|
|
262
|
+
"memory.discardChanges": "Discard changes?",
|
|
263
|
+
|
|
264
|
+
"slash.cost": "Show token usage and cost",
|
|
265
|
+
"slash.context": "Show context usage",
|
|
266
|
+
"slash.compact": "Compact context"
|
|
250
267
|
}
|
package/web/locales/zh.json
CHANGED
|
@@ -246,5 +246,22 @@
|
|
|
246
246
|
"tool.replaceAll": "(全部替换)",
|
|
247
247
|
|
|
248
248
|
"usage.context": "上下文",
|
|
249
|
-
"usage.cost": "费用"
|
|
249
|
+
"usage.cost": "费用",
|
|
250
|
+
|
|
251
|
+
"sidebar.memory": "记忆",
|
|
252
|
+
"memory.title": "记忆文件",
|
|
253
|
+
"memory.loading": "加载记忆文件...",
|
|
254
|
+
"memory.noFiles": "未找到记忆文件。",
|
|
255
|
+
"memory.noFilesHint": "Claude 会在工作过程中自动创建记忆文件。",
|
|
256
|
+
"memory.edit": "编辑",
|
|
257
|
+
"memory.editing": "编辑中",
|
|
258
|
+
"memory.save": "保存",
|
|
259
|
+
"memory.saving": "保存中...",
|
|
260
|
+
"memory.deleteFile": "删除",
|
|
261
|
+
"memory.deleteConfirm": "删除 {name}?",
|
|
262
|
+
"memory.discardChanges": "放弃更改?",
|
|
263
|
+
|
|
264
|
+
"slash.cost": "显示 Token 用量和费用",
|
|
265
|
+
"slash.context": "显示上下文用量",
|
|
266
|
+
"slash.compact": "压缩上下文"
|
|
250
267
|
}
|
|
@@ -25,6 +25,8 @@ export function createConnection(deps) {
|
|
|
25
25
|
// Multi-session parallel
|
|
26
26
|
currentConversationId, processingConversations, conversationCache,
|
|
27
27
|
switchConversation,
|
|
28
|
+
// Memory management
|
|
29
|
+
memoryFiles, memoryDir, memoryLoading, memoryEditing, memoryEditContent, memorySaving, memoryPanelOpen,
|
|
28
30
|
// i18n
|
|
29
31
|
t,
|
|
30
32
|
} = deps;
|
|
@@ -539,6 +541,25 @@ export function createConnection(deps) {
|
|
|
539
541
|
}
|
|
540
542
|
} else if (msg.type === 'file_content') {
|
|
541
543
|
if (filePreview) filePreview.handleFileContent(msg);
|
|
544
|
+
} else if (msg.type === 'memory_list') {
|
|
545
|
+
memoryLoading.value = false;
|
|
546
|
+
memoryFiles.value = msg.files || [];
|
|
547
|
+
memoryDir.value = msg.memoryDir || null;
|
|
548
|
+
} else if (msg.type === 'memory_updated') {
|
|
549
|
+
memorySaving.value = false;
|
|
550
|
+
if (msg.success) {
|
|
551
|
+
memoryEditing.value = false;
|
|
552
|
+
memoryEditContent.value = '';
|
|
553
|
+
// Refresh list and preview
|
|
554
|
+
wsSend({ type: 'list_memory' });
|
|
555
|
+
if (filePreview) filePreview.refreshPreview();
|
|
556
|
+
}
|
|
557
|
+
} else if (msg.type === 'memory_deleted') {
|
|
558
|
+
if (msg.success) {
|
|
559
|
+
memoryFiles.value = memoryFiles.value.filter(f => f.name !== msg.filename);
|
|
560
|
+
// Close preview if open (might be showing the deleted file)
|
|
561
|
+
if (filePreview) filePreview.closePreview();
|
|
562
|
+
}
|
|
542
563
|
} else if (msg.type === 'workdir_changed') {
|
|
543
564
|
workdirSwitching.value = false;
|
|
544
565
|
workDir.value = msg.workDir;
|
|
@@ -573,6 +594,10 @@ export function createConnection(deps) {
|
|
|
573
594
|
historySessions.value = [];
|
|
574
595
|
if (team) team.teamsList.value = [];
|
|
575
596
|
if (loop) loop.loopsList.value = [];
|
|
597
|
+
memoryFiles.value = [];
|
|
598
|
+
memoryDir.value = null;
|
|
599
|
+
memoryPanelOpen.value = false;
|
|
600
|
+
memoryEditing.value = false;
|
|
576
601
|
sidebar.requestSessionList();
|
|
577
602
|
if (team) team.requestTeamsList();
|
|
578
603
|
if (loop) loop.requestLoopsList();
|
|
@@ -188,9 +188,19 @@ export function createFilePreview(deps) {
|
|
|
188
188
|
return renderMarkdown(content || '');
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/** Force re-fetch the currently open preview file (e.g. after editing) */
|
|
192
|
+
function refreshPreview() {
|
|
193
|
+
if (!previewFile.value?.filePath) return;
|
|
194
|
+
const filePath = previewFile.value.filePath;
|
|
195
|
+
previewFile.value = null;
|
|
196
|
+
previewLoading.value = true;
|
|
197
|
+
wsSend({ type: 'read_file', filePath });
|
|
198
|
+
}
|
|
199
|
+
|
|
191
200
|
return {
|
|
192
201
|
openPreview,
|
|
193
202
|
closePreview,
|
|
203
|
+
refreshPreview,
|
|
194
204
|
handleFileContent,
|
|
195
205
|
onWorkdirChanged,
|
|
196
206
|
detectLanguage,
|
package/web/style.css
CHANGED
|
@@ -1823,6 +1823,42 @@ body {
|
|
|
1823
1823
|
position: relative;
|
|
1824
1824
|
}
|
|
1825
1825
|
|
|
1826
|
+
.slash-menu {
|
|
1827
|
+
max-width: 768px;
|
|
1828
|
+
margin: 0 auto 4px;
|
|
1829
|
+
background: var(--bg-secondary);
|
|
1830
|
+
border: 1px solid var(--border);
|
|
1831
|
+
border-radius: 12px;
|
|
1832
|
+
padding: 4px 0;
|
|
1833
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.slash-menu-item {
|
|
1837
|
+
display: flex;
|
|
1838
|
+
align-items: center;
|
|
1839
|
+
gap: 12px;
|
|
1840
|
+
padding: 8px 16px;
|
|
1841
|
+
cursor: pointer;
|
|
1842
|
+
transition: background 0.1s;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
.slash-menu-item.active {
|
|
1846
|
+
background: var(--bg-tertiary);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
.slash-menu-cmd {
|
|
1850
|
+
font-weight: 600;
|
|
1851
|
+
color: var(--accent);
|
|
1852
|
+
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
|
1853
|
+
font-size: 0.9rem;
|
|
1854
|
+
min-width: 90px;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
.slash-menu-desc {
|
|
1858
|
+
color: var(--text-secondary);
|
|
1859
|
+
font-size: 0.85rem;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1826
1862
|
.input-card {
|
|
1827
1863
|
max-width: 768px;
|
|
1828
1864
|
margin: 0 auto;
|
|
@@ -5010,3 +5046,126 @@ body {
|
|
|
5010
5046
|
justify-content: center;
|
|
5011
5047
|
padding: 12px 0 4px;
|
|
5012
5048
|
}
|
|
5049
|
+
|
|
5050
|
+
/* ── Memory management ── */
|
|
5051
|
+
.memory-empty {
|
|
5052
|
+
padding: 24px 16px;
|
|
5053
|
+
text-align: center;
|
|
5054
|
+
color: var(--text-secondary);
|
|
5055
|
+
font-size: 0.85rem;
|
|
5056
|
+
}
|
|
5057
|
+
.memory-empty-hint {
|
|
5058
|
+
margin-top: 6px;
|
|
5059
|
+
font-size: 0.78rem;
|
|
5060
|
+
opacity: 0.7;
|
|
5061
|
+
}
|
|
5062
|
+
.memory-file-item {
|
|
5063
|
+
display: flex;
|
|
5064
|
+
align-items: center;
|
|
5065
|
+
justify-content: space-between;
|
|
5066
|
+
}
|
|
5067
|
+
.memory-file-row {
|
|
5068
|
+
display: flex;
|
|
5069
|
+
align-items: center;
|
|
5070
|
+
gap: 6px;
|
|
5071
|
+
flex: 1;
|
|
5072
|
+
min-width: 0;
|
|
5073
|
+
cursor: pointer;
|
|
5074
|
+
padding: 4px 8px;
|
|
5075
|
+
border-radius: 4px;
|
|
5076
|
+
}
|
|
5077
|
+
.memory-file-row:hover {
|
|
5078
|
+
background: var(--hover-bg);
|
|
5079
|
+
}
|
|
5080
|
+
.memory-delete-btn {
|
|
5081
|
+
background: none;
|
|
5082
|
+
border: none;
|
|
5083
|
+
color: var(--text-secondary);
|
|
5084
|
+
cursor: pointer;
|
|
5085
|
+
padding: 4px;
|
|
5086
|
+
border-radius: 4px;
|
|
5087
|
+
opacity: 0;
|
|
5088
|
+
transition: opacity 0.15s;
|
|
5089
|
+
flex-shrink: 0;
|
|
5090
|
+
}
|
|
5091
|
+
.memory-file-item:hover .memory-delete-btn {
|
|
5092
|
+
opacity: 1;
|
|
5093
|
+
}
|
|
5094
|
+
.memory-delete-btn:hover {
|
|
5095
|
+
color: var(--error-color, #e53e3e);
|
|
5096
|
+
background: var(--hover-bg);
|
|
5097
|
+
}
|
|
5098
|
+
.memory-edit-container {
|
|
5099
|
+
display: flex;
|
|
5100
|
+
flex-direction: column;
|
|
5101
|
+
height: 100%;
|
|
5102
|
+
}
|
|
5103
|
+
.memory-edit-textarea {
|
|
5104
|
+
flex: 1;
|
|
5105
|
+
min-height: 0;
|
|
5106
|
+
width: 100%;
|
|
5107
|
+
border: none;
|
|
5108
|
+
resize: none;
|
|
5109
|
+
padding: 12px;
|
|
5110
|
+
font-family: var(--font-mono);
|
|
5111
|
+
font-size: 0.85rem;
|
|
5112
|
+
line-height: 1.5;
|
|
5113
|
+
background: var(--bg-primary);
|
|
5114
|
+
color: var(--text-primary);
|
|
5115
|
+
box-sizing: border-box;
|
|
5116
|
+
}
|
|
5117
|
+
.memory-edit-textarea:focus {
|
|
5118
|
+
outline: none;
|
|
5119
|
+
}
|
|
5120
|
+
.memory-header-cancel {
|
|
5121
|
+
background: none;
|
|
5122
|
+
border: 1px solid var(--border);
|
|
5123
|
+
color: var(--text-secondary);
|
|
5124
|
+
padding: 3px 10px;
|
|
5125
|
+
border-radius: 4px;
|
|
5126
|
+
cursor: pointer;
|
|
5127
|
+
font-size: 0.75rem;
|
|
5128
|
+
white-space: nowrap;
|
|
5129
|
+
}
|
|
5130
|
+
.memory-header-cancel:hover {
|
|
5131
|
+
background: var(--bg-tertiary);
|
|
5132
|
+
}
|
|
5133
|
+
.memory-header-save {
|
|
5134
|
+
background: var(--accent);
|
|
5135
|
+
border: none;
|
|
5136
|
+
color: white;
|
|
5137
|
+
padding: 3px 10px;
|
|
5138
|
+
border-radius: 4px;
|
|
5139
|
+
cursor: pointer;
|
|
5140
|
+
font-size: 0.75rem;
|
|
5141
|
+
white-space: nowrap;
|
|
5142
|
+
}
|
|
5143
|
+
.memory-header-save:hover {
|
|
5144
|
+
opacity: 0.9;
|
|
5145
|
+
}
|
|
5146
|
+
.memory-header-save:disabled {
|
|
5147
|
+
opacity: 0.5;
|
|
5148
|
+
cursor: not-allowed;
|
|
5149
|
+
}
|
|
5150
|
+
.preview-edit-btn {
|
|
5151
|
+
display: inline-flex;
|
|
5152
|
+
align-items: center;
|
|
5153
|
+
gap: 4px;
|
|
5154
|
+
background: none;
|
|
5155
|
+
border: 1px solid var(--border-color);
|
|
5156
|
+
color: var(--text-secondary);
|
|
5157
|
+
padding: 2px 8px;
|
|
5158
|
+
border-radius: 4px;
|
|
5159
|
+
cursor: pointer;
|
|
5160
|
+
font-size: 0.75rem;
|
|
5161
|
+
margin-left: auto;
|
|
5162
|
+
}
|
|
5163
|
+
.preview-edit-btn:hover {
|
|
5164
|
+
background: var(--hover-bg);
|
|
5165
|
+
color: var(--text-primary);
|
|
5166
|
+
}
|
|
5167
|
+
.preview-edit-label {
|
|
5168
|
+
font-size: 0.75rem;
|
|
5169
|
+
color: var(--accent-color);
|
|
5170
|
+
margin-left: auto;
|
|
5171
|
+
}
|