jinzd-ai-cli 0.1.87 → 0.1.89

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/CLAUDE.md CHANGED
@@ -2040,8 +2040,8 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
2040
2040
 
2041
2041
  | # | 功能 | 状态 | 说明 |
2042
2042
  |---|------|------|------|
2043
- | P2-1 | **多 Tab 会话** | [ ] | 浏览器内多 Tab 并行对话(CLI 做不到),类似 ChatGPT |
2044
- | P2-2 | **文件树面板** | [ ] | 浏览项目目录结构,点击文件查看/插入上下文,可视化 `@` 引用 |
2043
+ | P2-1 | **多 Tab 会话** | [x] | 浏览器内多 Tab 并行对话(CLI 做不到),类似 ChatGPT |
2044
+ | P2-2 | **文件树面板** | [x] | 浏览项目目录结构,点击文件查看/插入上下文,可视化 `@` 引用 |
2045
2045
  | P2-3 | **工具执行可视化** | [ ] | 进度条、工具调用时间线、Agentic 循环图示 |
2046
2046
  | P2-4 | **Prompt 模板库** | [ ] | 保存常用 prompt 为模板,一键复用(localStorage) |
2047
2047
  | P2-5 | **代码主题联动** | [ ] | highlight.js 主题随 DaisyUI 主题切换(亮色 → github-light,暗色 → github-dark) |
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-L5YUW6T3.js";
19
+ } from "./chunk-6T7KHDLM.js";
20
20
 
21
21
  // src/config/config-manager.ts
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -14,7 +14,7 @@ import { platform } from "os";
14
14
  import chalk from "chalk";
15
15
 
16
16
  // src/core/constants.ts
17
- var VERSION = "0.1.87";
17
+ var VERSION = "0.1.89";
18
18
  var APP_NAME = "ai-cli";
19
19
  var CONFIG_DIR_NAME = ".aicli";
20
20
  var CONFIG_FILE_NAME = "config.json";
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  theme,
36
36
  truncateOutput,
37
37
  undoStack
38
- } from "./chunk-F2N4QBIY.js";
38
+ } from "./chunk-3RRSDUVU.js";
39
39
  import {
40
40
  AGENTIC_BEHAVIOR_GUIDELINE,
41
41
  AUTHOR,
@@ -55,7 +55,7 @@ import {
55
55
  REPO_URL,
56
56
  SKILLS_DIR_NAME,
57
57
  VERSION
58
- } from "./chunk-L5YUW6T3.js";
58
+ } from "./chunk-6T7KHDLM.js";
59
59
 
60
60
  // src/index.ts
61
61
  import { program } from "commander";
@@ -1904,7 +1904,7 @@ ${hint}` : "")
1904
1904
  description: "Run project tests and show structured report",
1905
1905
  usage: "/test [command|filter]",
1906
1906
  async execute(args, _ctx) {
1907
- const { executeTests } = await import("./run-tests-RCWE32G4.js");
1907
+ const { executeTests } = await import("./run-tests-U3HF44WA.js");
1908
1908
  const argStr = args.join(" ").trim();
1909
1909
  let testArgs = {};
1910
1910
  if (argStr) {
@@ -5292,7 +5292,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5292
5292
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5293
5293
  process.exit(1);
5294
5294
  }
5295
- const { startWebServer } = await import("./server-MDRGHPXN.js");
5295
+ const { startWebServer } = await import("./server-IDSC72WU.js");
5296
5296
  await startWebServer({ port, host: options.host });
5297
5297
  });
5298
5298
  program.command("sessions").description("List recent conversation sessions").action(async () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-L5YUW6T3.js";
5
+ } from "./chunk-6T7KHDLM.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -23,7 +23,7 @@ import {
23
23
  setupProxy,
24
24
  spawnAgentContext,
25
25
  truncateOutput
26
- } from "./chunk-F2N4QBIY.js";
26
+ } from "./chunk-3RRSDUVU.js";
27
27
  import {
28
28
  AGENTIC_BEHAVIOR_GUIDELINE,
29
29
  CONTEXT_FILE_CANDIDATES,
@@ -36,7 +36,7 @@ import {
36
36
  SKILLS_DIR_NAME,
37
37
  VERSION,
38
38
  __require
39
- } from "./chunk-L5YUW6T3.js";
39
+ } from "./chunk-6T7KHDLM.js";
40
40
 
41
41
  // src/web/server.ts
42
42
  import express from "express";
@@ -466,6 +466,7 @@ var SessionHandler = class {
466
466
  provider: this.currentProvider,
467
467
  model: this.currentModel,
468
468
  sessionId: this.sessions.current?.id ?? "",
469
+ sessionTitle: this.sessions.current?.title ?? void 0,
469
470
  messageCount: this.sessions.current?.messages.length ?? 0,
470
471
  planMode: this.planMode,
471
472
  thinkingMode: this.runtimeThinking ?? false,
@@ -1356,7 +1357,8 @@ async function startWebServer(options = {}) {
1356
1357
  res.json({
1357
1358
  version: VERSION,
1358
1359
  providers: availableProviders,
1359
- tools: toolRegistry.getDefinitions().length
1360
+ tools: toolRegistry.getDefinitions().length,
1361
+ cwd: process.cwd()
1360
1362
  });
1361
1363
  });
1362
1364
  app.get("/api/files", (req, res) => {
@@ -1424,15 +1426,20 @@ async function startWebServer(options = {}) {
1424
1426
  res.json({ error: `Cannot read: ${err.message}` });
1425
1427
  }
1426
1428
  });
1427
- let activeHandler = null;
1428
- wss.on("connection", (ws) => {
1429
- if (activeHandler) {
1430
- activeHandler.onDisconnect();
1431
- activeHandler = null;
1429
+ const handlers = /* @__PURE__ */ new Map();
1430
+ wss.on("connection", (ws, req) => {
1431
+ const urlStr = req.url ?? "";
1432
+ const qMark = urlStr.indexOf("?");
1433
+ const params = new URLSearchParams(qMark >= 0 ? urlStr.slice(qMark + 1) : "");
1434
+ const tabId = params.get("tabId") || `tab-${Date.now()}`;
1435
+ const existing = handlers.get(tabId);
1436
+ if (existing) {
1437
+ existing.onDisconnect();
1438
+ handlers.delete(tabId);
1432
1439
  }
1433
- console.log(" \u2713 Client connected");
1440
+ console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (total: ${handlers.size + 1})`);
1434
1441
  const handler = new SessionHandler(ws, shared);
1435
- activeHandler = handler;
1442
+ handlers.set(tabId, handler);
1436
1443
  ws.on("message", async (data) => {
1437
1444
  try {
1438
1445
  const raw = data.toString();
@@ -1453,9 +1460,9 @@ async function startWebServer(options = {}) {
1453
1460
  }
1454
1461
  });
1455
1462
  ws.on("close", () => {
1456
- console.log(" \u2717 Client disconnected");
1463
+ console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
1457
1464
  handler.onDisconnect();
1458
- if (activeHandler === handler) activeHandler = null;
1465
+ handlers.delete(tabId);
1459
1466
  });
1460
1467
  ws.on("error", (err) => {
1461
1468
  console.error(` WebSocket error: ${err.message}`);
@@ -1471,7 +1478,7 @@ async function startWebServer(options = {}) {
1471
1478
  });
1472
1479
  process.on("SIGINT", () => {
1473
1480
  console.log("\n Shutting down...");
1474
- if (activeHandler) activeHandler.onDisconnect();
1481
+ for (const handler of handlers.values()) handler.onDisconnect();
1475
1482
  if (mcpManager) mcpManager.closeAll();
1476
1483
  wss.close();
1477
1484
  server.close();
@@ -57,6 +57,14 @@ marked.setOptions({
57
57
  },
58
58
  });
59
59
 
60
+ // ── Tab identity (per browser tab, survives page reload) ──────────
61
+
62
+ let tabId = sessionStorage.getItem('aicli-tab-id');
63
+ if (!tabId) {
64
+ tabId = 'tab-' + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
65
+ sessionStorage.setItem('aicli-tab-id', tabId);
66
+ }
67
+
60
68
  // ── WebSocket ──────────────────────────────────────────────────────
61
69
 
62
70
  let heartbeatTimer = null;
@@ -64,7 +72,7 @@ let reconnectDelay = 1000; // Start at 1s, exponential backoff
64
72
 
65
73
  function connect() {
66
74
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
67
- ws = new WebSocket(`${protocol}//${location.host}`);
75
+ ws = new WebSocket(`${protocol}//${location.host}?tabId=${tabId}`);
68
76
 
69
77
  ws.onopen = () => {
70
78
  connected = true;
@@ -84,6 +92,11 @@ function connect() {
84
92
  setProcessing(false);
85
93
  addInfoMessage('⚡ Reconnected — previous generation may have been interrupted.');
86
94
  }
95
+ // Restore the last active session for this tab (page reload / reconnect)
96
+ const savedSession = sessionStorage.getItem('aicli-active-session');
97
+ if (savedSession && !processing) {
98
+ send({ type: 'command', name: 'session', args: ['load', savedSession] });
99
+ }
87
100
  };
88
101
 
89
102
  ws.onclose = () => {
@@ -351,6 +364,15 @@ function handleStatus(msg) {
351
364
  sessionListEl.querySelectorAll('.session-item').forEach(el => {
352
365
  el.classList.toggle('active', el.dataset.sessionId === msg.sessionId);
353
366
  });
367
+
368
+ // Persist active session for this tab (for page reload restore)
369
+ if (msg.sessionId) {
370
+ sessionStorage.setItem('aicli-active-session', msg.sessionId);
371
+ }
372
+
373
+ // Update browser tab title to reflect current session
374
+ const title = msg.sessionTitle || msg.sessionId?.slice(0, 8) || 'New Session';
375
+ document.title = `ai-cli — ${title}`;
354
376
  }
355
377
 
356
378
  // ── Response helpers ───────────────────────────────────────────────
@@ -723,6 +745,8 @@ if (sessionSearchInput) {
723
745
 
724
746
  // ── Sidebar tabs ──────────────────────────────────────────────────────
725
747
 
748
+ let fileTreeLoaded = false;
749
+
726
750
  function switchSidebarTab(tabName) {
727
751
  document.querySelectorAll('.sidebar-tab').forEach(btn => {
728
752
  btn.classList.toggle('active', btn.dataset.tab === tabName);
@@ -730,6 +754,11 @@ function switchSidebarTab(tabName) {
730
754
  document.querySelectorAll('.sidebar-tab-content').forEach(el => {
731
755
  el.classList.toggle('hidden', el.id !== `tab-${tabName}`);
732
756
  });
757
+ // Lazy-init file tree on first visit
758
+ if (tabName === 'files' && !fileTreeLoaded) {
759
+ fileTreeLoaded = true;
760
+ initFileTree();
761
+ }
733
762
  }
734
763
 
735
764
  document.querySelectorAll('.sidebar-tab').forEach(btn => {
@@ -1149,6 +1178,181 @@ function handleMemoryContent(msg) {
1149
1178
  scrollToBottom();
1150
1179
  }
1151
1180
 
1181
+ // ── File Tree ──────────────────────────────────────────────────────
1182
+
1183
+ const fileTreeEl = document.getElementById('file-tree');
1184
+ const fileTreeCwdEl = document.getElementById('file-tree-cwd');
1185
+ const btnFileTreeRefresh = document.getElementById('btn-file-tree-refresh');
1186
+
1187
+ // Track expanded directories: path → true
1188
+ const fileTreeExpanded = {};
1189
+
1190
+ async function initFileTree() {
1191
+ fileTreeEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">Loading...</div>';
1192
+ const files = await fetchFiles('');
1193
+
1194
+ // Show cwd hint via API status (already have it from /api/status, but we fetch fresh)
1195
+ fetch('/api/status').then(r => r.json()).then(data => {
1196
+ if (data.cwd && fileTreeCwdEl) {
1197
+ fileTreeCwdEl.textContent = data.cwd.replace(/\\/g, '/').split('/').pop() || data.cwd;
1198
+ fileTreeCwdEl.title = data.cwd;
1199
+ }
1200
+ }).catch(() => {});
1201
+
1202
+ renderFileTreeItems(fileTreeEl, files, 0);
1203
+ }
1204
+
1205
+ function renderFileTreeItems(container, items, depth) {
1206
+ if (!items || items.length === 0) {
1207
+ container.innerHTML = `<div class="text-xs opacity-30 px-${2 + depth} py-1">Empty</div>`;
1208
+ return;
1209
+ }
1210
+ container.innerHTML = '';
1211
+
1212
+ // Dirs first, then files
1213
+ const sorted = [...items].sort((a, b) => {
1214
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
1215
+ return a.name.localeCompare(b.name);
1216
+ });
1217
+
1218
+ for (const item of sorted) {
1219
+ const el = document.createElement('div');
1220
+ el.className = 'file-tree-item';
1221
+
1222
+ if (item.isDir) {
1223
+ const expanded = !!fileTreeExpanded[item.path];
1224
+ el.innerHTML = `
1225
+ <div class="file-tree-row file-tree-dir" title="${escapeHtml(item.path)}">
1226
+ <span class="file-tree-chevron${expanded ? ' expanded' : ''}">▶</span>
1227
+ <span class="file-tree-icon">📁</span>
1228
+ <span class="file-tree-name">${escapeHtml(item.name)}</span>
1229
+ </div>
1230
+ <div class="file-tree-children${expanded ? '' : ' hidden'}"></div>
1231
+ `;
1232
+ const row = el.querySelector('.file-tree-row');
1233
+ const chevron = el.querySelector('.file-tree-chevron');
1234
+ const children = el.querySelector('.file-tree-children');
1235
+
1236
+ row.addEventListener('click', async () => {
1237
+ const isNowExpanded = !fileTreeExpanded[item.path];
1238
+ fileTreeExpanded[item.path] = isNowExpanded;
1239
+ chevron.classList.toggle('expanded', isNowExpanded);
1240
+ children.classList.toggle('hidden', !isNowExpanded);
1241
+ if (isNowExpanded && children.children.length === 0) {
1242
+ children.innerHTML = '<div class="text-xs opacity-30 px-2 py-0.5">Loading...</div>';
1243
+ const subItems = await fetchFiles(item.path);
1244
+ renderFileTreeItems(children, subItems, depth + 1);
1245
+ }
1246
+ });
1247
+
1248
+ // If already expanded, load children immediately
1249
+ if (expanded) {
1250
+ fetchFiles(item.path).then(subItems => renderFileTreeItems(children, subItems, depth + 1));
1251
+ }
1252
+ } else {
1253
+ const ext = item.name.includes('.') ? item.name.split('.').pop() : '';
1254
+ const fileIcon = getFileIcon(ext);
1255
+ el.innerHTML = `
1256
+ <div class="file-tree-row file-tree-file" title="${escapeHtml(item.path)} — Click: insert @ref | Ctrl+Click: preview">
1257
+ <span class="file-tree-chevron" style="visibility:hidden">▶</span>
1258
+ <span class="file-tree-icon">${fileIcon}</span>
1259
+ <span class="file-tree-name">${escapeHtml(item.name)}</span>
1260
+ <button class="file-tree-insert-btn" title="Insert @reference">@</button>
1261
+ </div>
1262
+ `;
1263
+ const row = el.querySelector('.file-tree-row');
1264
+ const insertBtn = el.querySelector('.file-tree-insert-btn');
1265
+
1266
+ row.addEventListener('click', (e) => {
1267
+ if (e.target === insertBtn) return;
1268
+ if (e.ctrlKey || e.metaKey) {
1269
+ previewFileInChat(item.path);
1270
+ } else {
1271
+ insertFileRef(item.path);
1272
+ }
1273
+ });
1274
+
1275
+ insertBtn.addEventListener('click', (e) => {
1276
+ e.stopPropagation();
1277
+ insertFileRef(item.path);
1278
+ });
1279
+ }
1280
+
1281
+ container.appendChild(el);
1282
+ }
1283
+ }
1284
+
1285
+ function getFileIcon(ext) {
1286
+ const icons = {
1287
+ js: '🟨', ts: '🔷', jsx: '⚛️', tsx: '⚛️',
1288
+ py: '🐍', go: '🐹', rs: '🦀', java: '☕',
1289
+ md: '📝', json: '📋', yaml: '📋', yml: '📋', toml: '📋',
1290
+ html: '🌐', css: '🎨', scss: '🎨',
1291
+ sh: '⚡', bat: '⚡', ps1: '⚡',
1292
+ png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️', svg: '🖼️',
1293
+ pdf: '📕', zip: '📦', tar: '📦', gz: '📦',
1294
+ sql: '🗄️', db: '🗄️',
1295
+ env: '🔑', key: '🔑',
1296
+ lock: '🔒',
1297
+ };
1298
+ return icons[ext?.toLowerCase()] || '📄';
1299
+ }
1300
+
1301
+ function insertFileRef(filePath) {
1302
+ const val = userInput.value;
1303
+ const pos = userInput.selectionStart;
1304
+ const before = val.slice(0, pos);
1305
+ const after = val.slice(pos);
1306
+ const ref = `@${filePath}`;
1307
+ const needsSpace = after.length > 0 && !after.startsWith(' ') && !after.startsWith('\n');
1308
+ userInput.value = before + ref + (needsSpace ? ' ' : '') + after;
1309
+ const newPos = pos + ref.length + (needsSpace ? 1 : 0);
1310
+ userInput.setSelectionRange(newPos, newPos);
1311
+ userInput.focus();
1312
+ // Auto-resize textarea
1313
+ userInput.style.height = 'auto';
1314
+ userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
1315
+ }
1316
+
1317
+ async function previewFileInChat(filePath) {
1318
+ try {
1319
+ const resp = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
1320
+ const data = await resp.json();
1321
+ if (data.error) {
1322
+ addErrorMessage(`Cannot preview ${filePath}: ${data.error}`);
1323
+ return;
1324
+ }
1325
+ const ext = filePath.split('.').pop() || '';
1326
+ const sizeKb = (data.size / 1024).toFixed(1);
1327
+ const el = document.createElement('div');
1328
+ el.className = 'file-preview-card my-2';
1329
+ el.innerHTML = `
1330
+ <div class="flex items-center gap-2 mb-1">
1331
+ <span class="text-xs font-semibold opacity-70">📄 ${escapeHtml(filePath)}</span>
1332
+ <span class="badge badge-ghost badge-xs">${sizeKb} KB</span>
1333
+ <button class="btn btn-xs btn-ghost ml-auto opacity-50" onclick="insertFileRef('${escapeHtml(filePath).replace(/'/g, "\\'")}')">Insert @ref</button>
1334
+ </div>
1335
+ <pre style="max-height:300px"><code class="language-${escapeHtml(ext)}">${escapeHtml(data.content)}</code></pre>
1336
+ <button class="btn btn-xs btn-ghost mt-1 opacity-50 text-xs" onclick="this.previousElementSibling.style.maxHeight='none';this.remove()">▼ Show all</button>
1337
+ `;
1338
+ el.querySelectorAll('pre code').forEach(block => {
1339
+ try { hljs.highlightElement(block); } catch {}
1340
+ });
1341
+ messagesEl.appendChild(el);
1342
+ scrollToBottom();
1343
+ } catch {
1344
+ addErrorMessage(`Failed to preview ${filePath}`);
1345
+ }
1346
+ }
1347
+
1348
+ // Refresh button
1349
+ if (btnFileTreeRefresh) {
1350
+ btnFileTreeRefresh.addEventListener('click', () => {
1351
+ Object.keys(fileTreeExpanded).forEach(k => delete fileTreeExpanded[k]);
1352
+ initFileTree();
1353
+ });
1354
+ }
1355
+
1152
1356
  // ── Initialize ─────────────────────────────────────────────────────
1153
1357
 
1154
1358
  // Restore theme
@@ -54,6 +54,7 @@
54
54
  <div class="flex border-b border-base-content/10 flex-shrink-0">
55
55
  <button class="sidebar-tab active flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="sessions">📋 Sessions</button>
56
56
  <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="tools">🔧 Tools</button>
57
+ <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="files">📁 Files</button>
57
58
  </div>
58
59
  <!-- Sessions tab -->
59
60
  <div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
@@ -74,6 +75,16 @@
74
75
  <div class="text-xs opacity-40 text-center py-4">Type /tools to load</div>
75
76
  </div>
76
77
  </div>
78
+ <!-- Files tab -->
79
+ <div id="tab-files" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden hidden">
80
+ <div class="p-2 border-b border-base-content/10 flex items-center gap-1">
81
+ <span class="text-xs opacity-40 flex-1 truncate" id="file-tree-cwd" title="">cwd</span>
82
+ <button id="btn-file-tree-refresh" class="btn btn-xs btn-ghost opacity-50 hover:opacity-100 flex-shrink-0" title="Refresh file tree">↺</button>
83
+ </div>
84
+ <div id="file-tree" class="flex-1 overflow-y-auto py-1 text-sm">
85
+ <div class="text-xs opacity-40 text-center py-4">Click tab to load</div>
86
+ </div>
87
+ </div>
77
88
  </aside>
78
89
 
79
90
  <!-- Chat Area -->
@@ -361,6 +361,98 @@
361
361
  opacity: 0.6;
362
362
  }
363
363
 
364
+ /* ── File tree ──────────────────────────────────────── */
365
+ .file-tree-item {
366
+ user-select: none;
367
+ }
368
+ .file-tree-row {
369
+ display: flex;
370
+ align-items: center;
371
+ gap: 0.3rem;
372
+ padding: 0.2rem 0.5rem;
373
+ border-radius: 0.25rem;
374
+ cursor: pointer;
375
+ font-size: 0.8rem;
376
+ transition: background 0.1s;
377
+ min-width: 0;
378
+ }
379
+ .file-tree-row:hover {
380
+ background: oklch(var(--b3));
381
+ }
382
+ .file-tree-row.file-tree-dir {
383
+ font-weight: 500;
384
+ }
385
+ .file-tree-chevron {
386
+ font-size: 0.55rem;
387
+ opacity: 0.4;
388
+ flex-shrink: 0;
389
+ transition: transform 0.15s;
390
+ display: inline-block;
391
+ width: 0.75rem;
392
+ text-align: center;
393
+ }
394
+ .file-tree-chevron.expanded {
395
+ transform: rotate(90deg);
396
+ }
397
+ .file-tree-icon {
398
+ font-size: 0.8rem;
399
+ flex-shrink: 0;
400
+ line-height: 1;
401
+ }
402
+ .file-tree-name {
403
+ flex: 1;
404
+ overflow: hidden;
405
+ text-overflow: ellipsis;
406
+ white-space: nowrap;
407
+ min-width: 0;
408
+ }
409
+ .file-tree-insert-btn {
410
+ flex-shrink: 0;
411
+ font-size: 0.65rem;
412
+ font-weight: 700;
413
+ color: oklch(var(--p));
414
+ opacity: 0;
415
+ background: oklch(var(--p) / 0.12);
416
+ border: none;
417
+ border-radius: 0.2rem;
418
+ padding: 0 0.3rem;
419
+ cursor: pointer;
420
+ line-height: 1.4;
421
+ transition: opacity 0.15s, background 0.15s;
422
+ }
423
+ .file-tree-row:hover .file-tree-insert-btn {
424
+ opacity: 1;
425
+ }
426
+ .file-tree-insert-btn:hover {
427
+ background: oklch(var(--p) / 0.25) !important;
428
+ }
429
+ .file-tree-children {
430
+ padding-left: 1rem;
431
+ border-left: 1px solid oklch(var(--bc) / 0.08);
432
+ margin-left: 0.85rem;
433
+ }
434
+
435
+ /* file preview card in chat */
436
+ .file-preview-card {
437
+ background: oklch(var(--b2));
438
+ border-radius: var(--rounded-box, 1rem);
439
+ padding: 0.75rem 1rem;
440
+ border-left: 3px solid oklch(var(--in));
441
+ }
442
+ .file-preview-card pre {
443
+ background: oklch(var(--b3));
444
+ border-radius: 0.375rem;
445
+ overflow-x: auto;
446
+ margin: 0.5rem 0 0;
447
+ }
448
+ .file-preview-card pre code {
449
+ display: block;
450
+ padding: 0.75rem;
451
+ font-size: 0.78rem;
452
+ line-height: 1.5;
453
+ font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
454
+ }
455
+
364
456
  /* ── Responsive ─────────────────────────────────────── */
365
457
  @media (max-width: 768px) {
366
458
  .sidebar { width: 0; padding: 0; border: none; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",