jinzd-ai-cli 0.1.86 → 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 +2 -2
- package/dist/{chunk-PCT4OP6Q.js → chunk-3RRSDUVU.js} +3 -1
- package/dist/{chunk-SMLN357K.js → chunk-6T7KHDLM.js} +1 -1
- package/dist/index.js +5 -5
- package/dist/{run-tests-Y2PN5FYJ.js → run-tests-U3HF44WA.js} +1 -1
- package/dist/{server-BFNZIL3O.js → server-IDSC72WU.js} +20 -13
- package/dist/web/client/app.js +205 -1
- package/dist/web/client/index.html +11 -0
- package/dist/web/client/style.css +92 -0
- package/package.json +1 -1
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 会话** | [
|
|
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-
|
|
19
|
+
} from "./chunk-6T7KHDLM.js";
|
|
20
20
|
|
|
21
21
|
// src/config/config-manager.ts
|
|
22
22
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -1449,6 +1449,8 @@ function detectsCodeBlockPseudoCall(content) {
|
|
|
1449
1449
|
var DEEPSEEK_CODE_BLOCK_CORRECTION = "You wrote a code block in your response text, but you did NOT actually execute it. Code blocks in text are NOT executed by the system. You MUST use the function calling API to invoke the appropriate tool (e.g., mcp__postgres__query for SQL queries, bash for shell commands). Please call the correct tool NOW to execute the query/command.";
|
|
1450
1450
|
var DeepSeekProvider = class extends OpenAICompatibleProvider {
|
|
1451
1451
|
defaultBaseUrl = "https://api.deepseek.com/v1";
|
|
1452
|
+
/** 禁用流式工具调用,确保 chatWithTools 覆写(代码块检测)生效 */
|
|
1453
|
+
enableStreamingToolCalls = false;
|
|
1452
1454
|
info = {
|
|
1453
1455
|
id: "deepseek",
|
|
1454
1456
|
displayName: "DeepSeek",
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
theme,
|
|
36
36
|
truncateOutput,
|
|
37
37
|
undoStack
|
|
38
|
-
} from "./chunk-
|
|
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-
|
|
58
|
+
} from "./chunk-6T7KHDLM.js";
|
|
59
59
|
|
|
60
60
|
// src/index.ts
|
|
61
61
|
import { program } from "commander";
|
|
@@ -103,8 +103,8 @@ function createSpinner(text) {
|
|
|
103
103
|
if (timer) {
|
|
104
104
|
clearInterval(timer);
|
|
105
105
|
timer = null;
|
|
106
|
+
clear();
|
|
106
107
|
}
|
|
107
|
-
clear();
|
|
108
108
|
},
|
|
109
109
|
start(t) {
|
|
110
110
|
currentText = t;
|
|
@@ -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-
|
|
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-
|
|
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 () => {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setupProxy,
|
|
24
24
|
spawnAgentContext,
|
|
25
25
|
truncateOutput
|
|
26
|
-
} from "./chunk-
|
|
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-
|
|
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
|
-
|
|
1428
|
-
wss.on("connection", (ws) => {
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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(
|
|
1440
|
+
console.log(` \u2713 Tab connected: ${tabId.slice(0, 12)} (total: ${handlers.size + 1})`);
|
|
1434
1441
|
const handler = new SessionHandler(ws, shared);
|
|
1435
|
-
|
|
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(
|
|
1463
|
+
console.log(` \u2717 Tab disconnected: ${tabId.slice(0, 12)} (total: ${handlers.size - 1})`);
|
|
1457
1464
|
handler.onDisconnect();
|
|
1458
|
-
|
|
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
|
-
|
|
1481
|
+
for (const handler of handlers.values()) handler.onDisconnect();
|
|
1475
1482
|
if (mcpManager) mcpManager.closeAll();
|
|
1476
1483
|
wss.close();
|
|
1477
1484
|
server.close();
|
package/dist/web/client/app.js
CHANGED
|
@@ -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; }
|