@agent-link/server 0.1.162 → 0.1.164

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-link/server",
3
- "version": "0.1.162",
3
+ "version": "0.1.164",
4
4
  "description": "AgentLink relay server",
5
5
  "license": "MIT",
6
6
  "repository": {
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?.content && filePreview.isMarkdownFile(previewFile.fileName)"
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="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
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 class="preview-panel-close" @click="filePreview.closePreview()" :title="t('preview.closePreview')">&times;</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')">&times;</button>
2308
2527
  </div>
2309
2528
  <div class="preview-panel-body">
2310
- <div v-if="previewLoading" class="preview-loading">{{ t('preview.loading') }}</div>
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>
@@ -0,0 +1,321 @@
1
+ /* ── AskUserQuestion interactive card ── */
2
+ .ask-question-wrapper {
3
+ max-width: 100%;
4
+ padding-left: 0.25rem;
5
+ margin: 0.5rem 0;
6
+ }
7
+
8
+ .ask-question-card {
9
+ background: var(--bg-secondary);
10
+ border: 1px solid var(--accent);
11
+ border-radius: 10px;
12
+ padding: 0.8rem 1rem;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 0.75rem;
16
+ }
17
+
18
+ .ask-question-block {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 0.5rem;
22
+ }
23
+
24
+ .ask-question-header {
25
+ font-size: 0.7rem;
26
+ font-weight: 600;
27
+ text-transform: uppercase;
28
+ letter-spacing: 0.04em;
29
+ color: var(--accent);
30
+ }
31
+
32
+ .ask-question-text {
33
+ font-size: 0.9rem;
34
+ color: var(--text-primary);
35
+ line-height: 1.5;
36
+ }
37
+
38
+ .ask-question-options {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 6px;
42
+ }
43
+
44
+ .ask-question-option {
45
+ padding: 8px 12px;
46
+ border: 1px solid var(--border);
47
+ border-radius: 8px;
48
+ cursor: pointer;
49
+ transition: border-color 0.15s, background 0.15s;
50
+ }
51
+
52
+ .ask-question-option:hover {
53
+ border-color: var(--text-secondary);
54
+ background: var(--bg-tertiary);
55
+ }
56
+
57
+ .ask-question-option.selected {
58
+ border-color: var(--accent);
59
+ background: rgba(107, 159, 206, 0.12);
60
+ }
61
+
62
+ .ask-option-label {
63
+ font-size: 0.88rem;
64
+ font-weight: 500;
65
+ color: var(--text-primary);
66
+ }
67
+
68
+ .ask-option-desc {
69
+ font-size: 0.78rem;
70
+ color: var(--text-secondary);
71
+ margin-top: 2px;
72
+ line-height: 1.4;
73
+ }
74
+
75
+ .ask-question-custom {
76
+ margin-top: 2px;
77
+ }
78
+
79
+ .ask-question-custom input {
80
+ width: 100%;
81
+ padding: 6px 10px;
82
+ background: var(--bg-primary);
83
+ border: 1px solid var(--border);
84
+ border-radius: 6px;
85
+ color: var(--text-primary);
86
+ font-size: 0.85rem;
87
+ font-family: inherit;
88
+ outline: none;
89
+ transition: border-color 0.15s;
90
+ }
91
+
92
+ .ask-question-custom input::placeholder {
93
+ color: var(--text-secondary);
94
+ }
95
+
96
+ .ask-question-custom input:focus {
97
+ border-color: var(--accent);
98
+ }
99
+
100
+ .ask-question-actions {
101
+ display: flex;
102
+ justify-content: flex-end;
103
+ }
104
+
105
+ .ask-question-submit {
106
+ padding: 6px 20px;
107
+ background: var(--accent);
108
+ color: #fff;
109
+ border: none;
110
+ border-radius: 8px;
111
+ font-size: 0.85rem;
112
+ font-weight: 600;
113
+ cursor: pointer;
114
+ transition: background 0.15s, opacity 0.15s;
115
+ }
116
+
117
+ .ask-question-submit:hover:not(:disabled) {
118
+ background: var(--accent-hover);
119
+ }
120
+
121
+ .ask-question-submit:disabled {
122
+ opacity: 0.3;
123
+ cursor: not-allowed;
124
+ }
125
+
126
+ .ask-question-answered {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 6px;
130
+ padding: 4px 8px;
131
+ font-size: 0.82rem;
132
+ color: var(--text-secondary);
133
+ }
134
+
135
+ .ask-answered-icon {
136
+ color: var(--success);
137
+ font-size: 0.8rem;
138
+ }
139
+
140
+ .ask-answered-text {
141
+ font-style: italic;
142
+ }
143
+
144
+ .context-summary-bar {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 6px;
148
+ padding: 6px 10px;
149
+ border-radius: 6px;
150
+ border: 1px dashed var(--border);
151
+ font-size: 0.78rem;
152
+ color: var(--text-secondary);
153
+ cursor: pointer;
154
+ transition: background 0.15s;
155
+ }
156
+
157
+ .context-summary-bar:hover {
158
+ background: var(--bg-tertiary);
159
+ }
160
+
161
+ .context-summary-icon {
162
+ flex-shrink: 0;
163
+ opacity: 0.5;
164
+ }
165
+
166
+ .context-summary-label {
167
+ flex: 1;
168
+ }
169
+
170
+ .context-summary-toggle {
171
+ font-size: 0.72rem;
172
+ opacity: 0.6;
173
+ flex-shrink: 0;
174
+ }
175
+
176
+ .context-summary-body {
177
+ margin-top: 6px;
178
+ padding: 0.6rem 0.8rem;
179
+ background: var(--bg-secondary);
180
+ border-radius: 6px;
181
+ border: 1px solid var(--border);
182
+ font-size: 0.82rem;
183
+ color: var(--text-secondary);
184
+ max-height: 400px;
185
+ overflow-y: auto;
186
+ }
187
+
188
+ .context-summary-body .markdown-body {
189
+ font-size: 0.82rem;
190
+ color: var(--text-secondary);
191
+ }
192
+
193
+ /* ── System message ── */
194
+ .system-msg {
195
+ text-align: center;
196
+ color: var(--text-secondary);
197
+ font-size: 0.8rem;
198
+ font-style: italic;
199
+ padding: 0.25rem 0;
200
+ }
201
+
202
+ .system-msg.command-output-msg {
203
+ text-align: left;
204
+ font-style: normal;
205
+ font-size: 0.85rem;
206
+ padding: 0.5rem 0;
207
+ }
208
+
209
+ .system-msg.error-msg {
210
+ text-align: left;
211
+ font-style: normal;
212
+ color: #d45454;
213
+ background: rgba(212, 84, 84, 0.08);
214
+ border: 1px solid rgba(212, 84, 84, 0.2);
215
+ border-radius: 8px;
216
+ padding: 0.75rem 1rem;
217
+ font-size: 0.85rem;
218
+ word-break: break-word;
219
+ overflow-wrap: break-word;
220
+ }
221
+
222
+ /* ── History loading indicator ── */
223
+ .history-loading {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ gap: 0.5rem;
228
+ padding: 2rem 0;
229
+ color: var(--text-secondary);
230
+ font-size: 0.85rem;
231
+ }
232
+
233
+ .history-loading-spinner {
234
+ width: 16px;
235
+ height: 16px;
236
+ border: 2px solid var(--border);
237
+ border-top-color: var(--accent);
238
+ border-radius: 50%;
239
+ animation: spin 0.8s linear infinite;
240
+ }
241
+
242
+ /* ── Load more button ── */
243
+ .load-more-wrapper {
244
+ display: flex;
245
+ justify-content: center;
246
+ padding: 0.75rem 0;
247
+ }
248
+
249
+ .load-more-btn {
250
+ background: var(--bg-tertiary);
251
+ color: var(--text-secondary);
252
+ border: 1px solid var(--border);
253
+ border-radius: 6px;
254
+ padding: 0.4rem 1.2rem;
255
+ font-size: 0.8rem;
256
+ cursor: pointer;
257
+ transition: background 0.15s, color 0.15s;
258
+ }
259
+
260
+ .load-more-btn:hover {
261
+ background: var(--accent);
262
+ color: #fff;
263
+ border-color: var(--accent);
264
+ }
265
+
266
+ /* ── Typing indicator ── */
267
+ .typing-indicator {
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 4px;
271
+ padding: 0.5rem 0.9rem;
272
+ }
273
+
274
+ .typing-indicator span {
275
+ width: 6px;
276
+ height: 6px;
277
+ border-radius: 50%;
278
+ background: var(--text-secondary);
279
+ animation: typing 1.2s infinite ease-in-out;
280
+ }
281
+
282
+ .typing-indicator span:nth-child(2) {
283
+ animation-delay: 0.2s;
284
+ }
285
+
286
+ .typing-indicator span:nth-child(3) {
287
+ animation-delay: 0.4s;
288
+ }
289
+
290
+ @keyframes typing {
291
+ 0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); }
292
+ 30% { opacity: 1; transform: scale(1); }
293
+ }
294
+
295
+ /* ── Context compaction inline message ── */
296
+ .compact-msg {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 6px;
300
+ color: var(--warning) !important;
301
+ font-style: normal !important;
302
+ font-weight: 500;
303
+ }
304
+
305
+ .compact-inline-spinner {
306
+ display: inline-block;
307
+ width: 12px;
308
+ height: 12px;
309
+ border: 2px solid rgba(212, 162, 76, 0.3);
310
+ border-top-color: var(--warning);
311
+ border-radius: 50%;
312
+ animation: spin 0.8s linear infinite;
313
+ flex-shrink: 0;
314
+ }
315
+
316
+ .compact-done-icon {
317
+ color: var(--success, #4ead6a);
318
+ font-weight: 700;
319
+ font-style: normal;
320
+ }
321
+