jinzd-ai-cli 0.1.89 → 0.1.91
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/dist/{chunk-3RRSDUVU.js → chunk-CXWWAWE7.js} +1 -1
- package/dist/{chunk-6T7KHDLM.js → chunk-KYHUMNQO.js} +1 -8
- package/dist/index.js +4 -4
- package/dist/{run-tests-U3HF44WA.js → run-tests-TLJ53CV5.js} +1 -1
- package/dist/{server-IDSC72WU.js → server-QG62ZDQI.js} +24 -15
- package/dist/web/client/app.js +268 -4
- package/dist/web/client/index.html +49 -2
- package/dist/web/client/style.css +106 -0
- package/package.json +1 -1
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/tools/builtin/run-tests.ts
|
|
10
4
|
import { execSync } from "child_process";
|
|
@@ -14,7 +8,7 @@ import { platform } from "os";
|
|
|
14
8
|
import chalk from "chalk";
|
|
15
9
|
|
|
16
10
|
// src/core/constants.ts
|
|
17
|
-
var VERSION = "0.1.
|
|
11
|
+
var VERSION = "0.1.91";
|
|
18
12
|
var APP_NAME = "ai-cli";
|
|
19
13
|
var CONFIG_DIR_NAME = ".aicli";
|
|
20
14
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -447,7 +441,6 @@ var runTestsTool = {
|
|
|
447
441
|
};
|
|
448
442
|
|
|
449
443
|
export {
|
|
450
|
-
__require,
|
|
451
444
|
VERSION,
|
|
452
445
|
APP_NAME,
|
|
453
446
|
CONFIG_DIR_NAME,
|
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-CXWWAWE7.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-KYHUMNQO.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-
|
|
1907
|
+
const { executeTests } = await import("./run-tests-TLJ53CV5.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-QG62ZDQI.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-CXWWAWE7.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
29
29
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -34,16 +34,15 @@ import {
|
|
|
34
34
|
PLAN_MODE_READONLY_TOOLS,
|
|
35
35
|
PLAN_MODE_SYSTEM_ADDON,
|
|
36
36
|
SKILLS_DIR_NAME,
|
|
37
|
-
VERSION
|
|
38
|
-
|
|
39
|
-
} from "./chunk-6T7KHDLM.js";
|
|
37
|
+
VERSION
|
|
38
|
+
} from "./chunk-KYHUMNQO.js";
|
|
40
39
|
|
|
41
40
|
// src/web/server.ts
|
|
42
41
|
import express from "express";
|
|
43
42
|
import { createServer } from "http";
|
|
44
43
|
import { WebSocketServer } from "ws";
|
|
45
|
-
import { join as join3, dirname, resolve as resolve2 } from "path";
|
|
46
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
44
|
+
import { join as join3, dirname, resolve as resolve2, relative } from "path";
|
|
45
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
47
46
|
|
|
48
47
|
// src/web/tool-executor-web.ts
|
|
49
48
|
import { randomUUID } from "crypto";
|
|
@@ -64,6 +63,8 @@ var ToolExecutorWeb = class {
|
|
|
64
63
|
pendingBatchConfirms = /* @__PURE__ */ new Map();
|
|
65
64
|
/** Publicly readable by SessionHandler to check if confirm is active */
|
|
66
65
|
confirming = false;
|
|
66
|
+
/** Track tool start times for duration calculation */
|
|
67
|
+
toolStartTimes = /* @__PURE__ */ new Map();
|
|
67
68
|
setRoundInfo(current, total) {
|
|
68
69
|
this.round = current;
|
|
69
70
|
this.totalRounds = total;
|
|
@@ -110,6 +111,8 @@ var ToolExecutorWeb = class {
|
|
|
110
111
|
}
|
|
111
112
|
sendToolCallStart(call) {
|
|
112
113
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
114
|
+
const startTime = Date.now();
|
|
115
|
+
this.toolStartTimes.set(call.id, startTime);
|
|
113
116
|
const msg = {
|
|
114
117
|
type: "tool_call_start",
|
|
115
118
|
callId: call.id,
|
|
@@ -117,17 +120,22 @@ var ToolExecutorWeb = class {
|
|
|
117
120
|
args: call.arguments,
|
|
118
121
|
dangerLevel,
|
|
119
122
|
round: this.round,
|
|
120
|
-
totalRounds: this.totalRounds
|
|
123
|
+
totalRounds: this.totalRounds,
|
|
124
|
+
startTime
|
|
121
125
|
};
|
|
122
126
|
this.send(msg);
|
|
123
127
|
}
|
|
124
128
|
sendToolCallResult(call, content, isError) {
|
|
129
|
+
const startTime = this.toolStartTimes.get(call.id);
|
|
130
|
+
const durationMs = startTime ? Date.now() - startTime : void 0;
|
|
131
|
+
this.toolStartTimes.delete(call.id);
|
|
125
132
|
const msg = {
|
|
126
133
|
type: "tool_call_result",
|
|
127
134
|
callId: call.id,
|
|
128
135
|
toolName: call.name,
|
|
129
136
|
content: content.length > 500 ? content.slice(0, 500) + "..." : content,
|
|
130
|
-
isError
|
|
137
|
+
isError,
|
|
138
|
+
durationMs
|
|
131
139
|
};
|
|
132
140
|
this.send(msg);
|
|
133
141
|
}
|
|
@@ -1356,17 +1364,19 @@ async function startWebServer(options = {}) {
|
|
|
1356
1364
|
app.get("/api/status", (_req, res) => {
|
|
1357
1365
|
res.json({
|
|
1358
1366
|
version: VERSION,
|
|
1359
|
-
providers: availableProviders
|
|
1367
|
+
providers: availableProviders.map((p) => ({
|
|
1368
|
+
id: p.info.id,
|
|
1369
|
+
displayName: p.info.displayName,
|
|
1370
|
+
models: p.info.models.map((m) => ({ id: m.id, name: m.name ?? m.id }))
|
|
1371
|
+
})),
|
|
1360
1372
|
tools: toolRegistry.getDefinitions().length,
|
|
1361
1373
|
cwd: process.cwd()
|
|
1362
1374
|
});
|
|
1363
1375
|
});
|
|
1364
1376
|
app.get("/api/files", (req, res) => {
|
|
1365
|
-
const { readdirSync, statSync } = __require("fs");
|
|
1366
|
-
const { join: pjoin, relative } = __require("path");
|
|
1367
1377
|
const cwd = process.cwd();
|
|
1368
1378
|
const prefix = req.query.prefix || "";
|
|
1369
|
-
const targetDir =
|
|
1379
|
+
const targetDir = join3(cwd, prefix);
|
|
1370
1380
|
if (!resolve2(targetDir).startsWith(resolve2(cwd))) {
|
|
1371
1381
|
res.json({ files: [] });
|
|
1372
1382
|
return;
|
|
@@ -1376,7 +1386,7 @@ async function startWebServer(options = {}) {
|
|
|
1376
1386
|
const entries = readdirSync(targetDir, { withFileTypes: true });
|
|
1377
1387
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
1378
1388
|
name: e.name,
|
|
1379
|
-
path: relative(cwd,
|
|
1389
|
+
path: relative(cwd, join3(targetDir, e.name)).replace(/\\/g, "/"),
|
|
1380
1390
|
isDir: e.isDirectory()
|
|
1381
1391
|
}));
|
|
1382
1392
|
res.json({ files });
|
|
@@ -1414,13 +1424,12 @@ async function startWebServer(options = {}) {
|
|
|
1414
1424
|
return;
|
|
1415
1425
|
}
|
|
1416
1426
|
try {
|
|
1417
|
-
const { statSync, readFileSync: readFileSync5 } = __require("fs");
|
|
1418
1427
|
const stat = statSync(fullPath);
|
|
1419
1428
|
if (stat.size > 512 * 1024) {
|
|
1420
1429
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
1421
1430
|
return;
|
|
1422
1431
|
}
|
|
1423
|
-
const content =
|
|
1432
|
+
const content = readFileSync4(fullPath, "utf-8");
|
|
1424
1433
|
res.json({ content, size: stat.size });
|
|
1425
1434
|
} catch (err) {
|
|
1426
1435
|
res.json({ error: `Cannot read: ${err.message}` });
|
package/dist/web/client/app.js
CHANGED
|
@@ -17,6 +17,7 @@ let pendingImages = []; // { name, data (base64), mime }
|
|
|
17
17
|
let inputHistory = []; // Previous user inputs for ↑/↓ navigation
|
|
18
18
|
let historyIndex = -1; // -1 = not browsing history
|
|
19
19
|
let savedInputDraft = ''; // Saved current input when entering history mode
|
|
20
|
+
let toolTimers = new Map(); // callId → { startTime, intervalId }
|
|
20
21
|
|
|
21
22
|
// ── DOM refs ───────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -153,8 +154,8 @@ function handleServerMessage(msg) {
|
|
|
153
154
|
case 'export_data': handleExportData(msg); break;
|
|
154
155
|
case 'memory_content': handleMemoryContent(msg); break;
|
|
155
156
|
case 'info': addInfoMessage(msg.message); break;
|
|
156
|
-
case 'error': addErrorMessage(msg.message); setProcessing(false); break;
|
|
157
|
-
case 'round_progress': break;
|
|
157
|
+
case 'error': addErrorMessage(msg.message); hideRoundProgress(); clearAllToolTimers(); setProcessing(false); break;
|
|
158
|
+
case 'round_progress': handleRoundProgress(msg); break;
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -186,6 +187,8 @@ function handleResponseDone(msg) {
|
|
|
186
187
|
|
|
187
188
|
currentAssistantEl = null;
|
|
188
189
|
currentAssistantContent = '';
|
|
190
|
+
hideRoundProgress();
|
|
191
|
+
clearAllToolTimers();
|
|
189
192
|
setProcessing(false);
|
|
190
193
|
scrollToBottom();
|
|
191
194
|
}
|
|
@@ -208,6 +211,7 @@ function handleToolCallStart(msg) {
|
|
|
208
211
|
<summary class="flex items-center gap-2 w-full cursor-pointer select-none py-1">
|
|
209
212
|
<span class="badge ${levelBadge} badge-sm gap-1">${levelIcon} ${escapeHtml(msg.toolName)}</span>
|
|
210
213
|
<span class="text-xs opacity-50">${msg.round}/${msg.totalRounds}</span>
|
|
214
|
+
<span class="tool-timer timing" data-call-id="${msg.callId}">⏱ 0.0s</span>
|
|
211
215
|
<span class="tool-result-badge text-xs ml-auto"></span>
|
|
212
216
|
</summary>
|
|
213
217
|
<div class="tool-details-body pt-1">
|
|
@@ -215,12 +219,42 @@ function handleToolCallStart(msg) {
|
|
|
215
219
|
</div>
|
|
216
220
|
`;
|
|
217
221
|
messagesEl.appendChild(el);
|
|
222
|
+
|
|
223
|
+
// Start live timer
|
|
224
|
+
const startTime = msg.startTime || Date.now();
|
|
225
|
+
const timerEl = el.querySelector(`[data-call-id="${msg.callId}"]`);
|
|
226
|
+
const intervalId = setInterval(() => {
|
|
227
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
228
|
+
if (timerEl) timerEl.textContent = `⏱ ${elapsed.toFixed(1)}s`;
|
|
229
|
+
}, 100);
|
|
230
|
+
toolTimers.set(msg.callId, { startTime, intervalId });
|
|
231
|
+
|
|
218
232
|
scrollToBottom();
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
function handleToolCallResult(msg) {
|
|
236
|
+
// Stop live timer
|
|
237
|
+
const timer = toolTimers.get(msg.callId);
|
|
238
|
+
if (timer) {
|
|
239
|
+
clearInterval(timer.intervalId);
|
|
240
|
+
toolTimers.delete(msg.callId);
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
const el = document.getElementById(`tool-${msg.callId}`);
|
|
223
244
|
if (el) {
|
|
245
|
+
// Replace timer with final duration
|
|
246
|
+
const timerEl = el.querySelector(`[data-call-id="${msg.callId}"]`);
|
|
247
|
+
if (timerEl && msg.durationMs != null) {
|
|
248
|
+
const secs = msg.durationMs / 1000;
|
|
249
|
+
const speedClass = secs < 1 ? 'fast' : secs < 5 ? 'medium' : 'slow';
|
|
250
|
+
timerEl.className = `tool-duration ${speedClass}`;
|
|
251
|
+
timerEl.textContent = formatDuration(msg.durationMs);
|
|
252
|
+
} else if (timerEl) {
|
|
253
|
+
// No duration data — just clear the timer
|
|
254
|
+
timerEl.className = 'tool-duration';
|
|
255
|
+
timerEl.textContent = '';
|
|
256
|
+
}
|
|
257
|
+
|
|
224
258
|
// Add result inside the details body
|
|
225
259
|
const body = el.querySelector('.tool-details-body');
|
|
226
260
|
if (body) {
|
|
@@ -308,6 +342,38 @@ function handleAskUserRequest(msg) {
|
|
|
308
342
|
setTimeout(() => document.getElementById(`ask-input-${msg.requestId}`)?.focus(), 100);
|
|
309
343
|
}
|
|
310
344
|
|
|
345
|
+
function handleRoundProgress(msg) {
|
|
346
|
+
const progressBar = document.getElementById('round-progress');
|
|
347
|
+
const progressBarEl = document.getElementById('round-progress-bar');
|
|
348
|
+
const progressLabel = document.getElementById('round-progress-label');
|
|
349
|
+
if (!progressBar || !progressBarEl || !progressLabel) return;
|
|
350
|
+
|
|
351
|
+
progressBar.classList.remove('hidden');
|
|
352
|
+
progressLabel.textContent = `Round ${msg.current}/${msg.total}`;
|
|
353
|
+
progressBarEl.value = Math.round((msg.current / msg.total) * 100);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function hideRoundProgress() {
|
|
357
|
+
const progressBar = document.getElementById('round-progress');
|
|
358
|
+
if (progressBar) progressBar.classList.add('hidden');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function formatDuration(ms) {
|
|
362
|
+
if (ms < 1000) return `${ms}ms`;
|
|
363
|
+
if (ms < 10000) return `${(ms / 1000).toFixed(1)}s`;
|
|
364
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(0)}s`;
|
|
365
|
+
const mins = Math.floor(ms / 60000);
|
|
366
|
+
const secs = Math.round((ms % 60000) / 1000);
|
|
367
|
+
return `${mins}m${secs}s`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function clearAllToolTimers() {
|
|
371
|
+
for (const { intervalId } of toolTimers.values()) {
|
|
372
|
+
clearInterval(intervalId);
|
|
373
|
+
}
|
|
374
|
+
toolTimers.clear();
|
|
375
|
+
}
|
|
376
|
+
|
|
311
377
|
function handleThinkingStart() {
|
|
312
378
|
const details = document.createElement('details');
|
|
313
379
|
details.className = 'thinking-block my-1 collapse collapse-arrow bg-base-200';
|
|
@@ -421,9 +487,21 @@ window.respondAskUser = function(requestId) {
|
|
|
421
487
|
}
|
|
422
488
|
};
|
|
423
489
|
|
|
490
|
+
// DaisyUI light themes → highlight.js light stylesheet; others → dark
|
|
491
|
+
const LIGHT_DAISYUI_THEMES = new Set(['light', 'cupcake', 'bumblebee', 'emerald', 'corporate', 'garden', 'lofi', 'pastel', 'fantasy', 'wireframe', 'cmyk', 'autumn', 'acid', 'lemonade', 'winter', 'nord']);
|
|
492
|
+
const HLJS_CDN = 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles';
|
|
493
|
+
|
|
494
|
+
function updateCodeTheme(daisyTheme) {
|
|
495
|
+
const isLight = LIGHT_DAISYUI_THEMES.has(daisyTheme);
|
|
496
|
+
const hljsFile = isLight ? 'github.min.css' : 'github-dark.min.css';
|
|
497
|
+
const link = document.getElementById('hljs-theme');
|
|
498
|
+
if (link) link.href = `${HLJS_CDN}/${hljsFile}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
424
501
|
window.setTheme = function(theme) {
|
|
425
502
|
document.documentElement.setAttribute('data-theme', theme);
|
|
426
503
|
localStorage.setItem('aicli-theme', theme);
|
|
504
|
+
updateCodeTheme(theme);
|
|
427
505
|
};
|
|
428
506
|
|
|
429
507
|
// ── UI helpers ─────────────────────────────────────────────────────
|
|
@@ -1178,6 +1256,189 @@ function handleMemoryContent(msg) {
|
|
|
1178
1256
|
scrollToBottom();
|
|
1179
1257
|
}
|
|
1180
1258
|
|
|
1259
|
+
// ── Prompt Templates ───────────────────────────────────────────────
|
|
1260
|
+
|
|
1261
|
+
const TEMPLATES_KEY = 'aicli-templates';
|
|
1262
|
+
|
|
1263
|
+
function loadTemplates() {
|
|
1264
|
+
try {
|
|
1265
|
+
return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
|
|
1266
|
+
} catch { return []; }
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function saveTemplatesToStorage(templates) {
|
|
1270
|
+
localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function openTemplatesModal() {
|
|
1274
|
+
const modal = document.getElementById('templates-modal');
|
|
1275
|
+
if (!modal) return;
|
|
1276
|
+
cancelTemplateForm();
|
|
1277
|
+
renderTemplateList();
|
|
1278
|
+
document.getElementById('template-search').value = '';
|
|
1279
|
+
modal.showModal();
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function renderTemplateList(filter) {
|
|
1283
|
+
const listEl = document.getElementById('template-list');
|
|
1284
|
+
if (!listEl) return;
|
|
1285
|
+
const templates = loadTemplates();
|
|
1286
|
+
const q = (filter || '').toLowerCase();
|
|
1287
|
+
const filtered = q
|
|
1288
|
+
? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
|
|
1289
|
+
: templates;
|
|
1290
|
+
|
|
1291
|
+
if (filtered.length === 0) {
|
|
1292
|
+
listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
listEl.innerHTML = filtered.map(t => {
|
|
1297
|
+
const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
|
|
1298
|
+
const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
|
|
1299
|
+
return `
|
|
1300
|
+
<div class="template-item" data-tpl-id="${t.id}" onclick="useTemplate('${t.id}')">
|
|
1301
|
+
<div class="template-item-body">
|
|
1302
|
+
<div class="template-item-name">${escapeHtml(t.name)}</div>
|
|
1303
|
+
<div class="template-item-preview">${escapeHtml(preview)}</div>
|
|
1304
|
+
${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
|
|
1305
|
+
</div>
|
|
1306
|
+
<div class="template-item-actions">
|
|
1307
|
+
<button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); editTemplate('${t.id}')" title="Edit">✏️</button>
|
|
1308
|
+
<button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); deleteTemplate('${t.id}')" title="Delete">🗑️</button>
|
|
1309
|
+
</div>
|
|
1310
|
+
</div>`;
|
|
1311
|
+
}).join('');
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function useTemplate(id) {
|
|
1315
|
+
const templates = loadTemplates();
|
|
1316
|
+
const tpl = templates.find(t => t.id === id);
|
|
1317
|
+
if (!tpl) return;
|
|
1318
|
+
userInput.value = tpl.content;
|
|
1319
|
+
userInput.focus();
|
|
1320
|
+
userInput.style.height = 'auto';
|
|
1321
|
+
userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
|
|
1322
|
+
document.getElementById('templates-modal')?.close();
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function showAddTemplate() {
|
|
1326
|
+
const form = document.getElementById('template-form');
|
|
1327
|
+
form.classList.remove('hidden');
|
|
1328
|
+
document.getElementById('tpl-edit-id').value = '';
|
|
1329
|
+
document.getElementById('tpl-name').value = '';
|
|
1330
|
+
document.getElementById('tpl-content').value = '';
|
|
1331
|
+
document.getElementById('tpl-tags').value = '';
|
|
1332
|
+
document.getElementById('tpl-name').focus();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function editTemplate(id) {
|
|
1336
|
+
const templates = loadTemplates();
|
|
1337
|
+
const tpl = templates.find(t => t.id === id);
|
|
1338
|
+
if (!tpl) return;
|
|
1339
|
+
const form = document.getElementById('template-form');
|
|
1340
|
+
form.classList.remove('hidden');
|
|
1341
|
+
document.getElementById('tpl-edit-id').value = tpl.id;
|
|
1342
|
+
document.getElementById('tpl-name').value = tpl.name;
|
|
1343
|
+
document.getElementById('tpl-content').value = tpl.content;
|
|
1344
|
+
document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
|
|
1345
|
+
document.getElementById('tpl-name').focus();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function cancelTemplateForm() {
|
|
1349
|
+
document.getElementById('template-form')?.classList.add('hidden');
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function saveTemplateForm() {
|
|
1353
|
+
const name = document.getElementById('tpl-name').value.trim();
|
|
1354
|
+
const content = document.getElementById('tpl-content').value.trim();
|
|
1355
|
+
const tagsRaw = document.getElementById('tpl-tags').value.trim();
|
|
1356
|
+
const editId = document.getElementById('tpl-edit-id').value;
|
|
1357
|
+
|
|
1358
|
+
if (!name || !content) return;
|
|
1359
|
+
|
|
1360
|
+
const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
1361
|
+
const templates = loadTemplates();
|
|
1362
|
+
|
|
1363
|
+
if (editId) {
|
|
1364
|
+
const idx = templates.findIndex(t => t.id === editId);
|
|
1365
|
+
if (idx >= 0) {
|
|
1366
|
+
templates[idx].name = name;
|
|
1367
|
+
templates[idx].content = content;
|
|
1368
|
+
templates[idx].tags = tags;
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
templates.unshift({
|
|
1372
|
+
id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
1373
|
+
name,
|
|
1374
|
+
content,
|
|
1375
|
+
tags,
|
|
1376
|
+
createdAt: new Date().toISOString(),
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
saveTemplatesToStorage(templates);
|
|
1381
|
+
cancelTemplateForm();
|
|
1382
|
+
renderTemplateList();
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function deleteTemplate(id) {
|
|
1386
|
+
const templates = loadTemplates().filter(t => t.id !== id);
|
|
1387
|
+
saveTemplatesToStorage(templates);
|
|
1388
|
+
renderTemplateList(document.getElementById('template-search')?.value);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function exportTemplates() {
|
|
1392
|
+
const templates = loadTemplates();
|
|
1393
|
+
if (templates.length === 0) return;
|
|
1394
|
+
const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
|
|
1395
|
+
const url = URL.createObjectURL(blob);
|
|
1396
|
+
const a = document.createElement('a');
|
|
1397
|
+
a.href = url;
|
|
1398
|
+
a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
|
|
1399
|
+
a.click();
|
|
1400
|
+
URL.revokeObjectURL(url);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function importTemplates() {
|
|
1404
|
+
const input = document.createElement('input');
|
|
1405
|
+
input.type = 'file';
|
|
1406
|
+
input.accept = '.json';
|
|
1407
|
+
input.onchange = () => {
|
|
1408
|
+
const file = input.files[0];
|
|
1409
|
+
if (!file) return;
|
|
1410
|
+
const reader = new FileReader();
|
|
1411
|
+
reader.onload = () => {
|
|
1412
|
+
try {
|
|
1413
|
+
const imported = JSON.parse(reader.result);
|
|
1414
|
+
if (!Array.isArray(imported)) throw new Error('Not an array');
|
|
1415
|
+
const existing = loadTemplates();
|
|
1416
|
+
const existingIds = new Set(existing.map(t => t.id));
|
|
1417
|
+
let added = 0;
|
|
1418
|
+
for (const t of imported) {
|
|
1419
|
+
if (t.id && t.name && t.content && !existingIds.has(t.id)) {
|
|
1420
|
+
existing.push(t);
|
|
1421
|
+
added++;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
saveTemplatesToStorage(existing);
|
|
1425
|
+
renderTemplateList();
|
|
1426
|
+
addInfoMessage(`📥 Imported ${added} template(s).`);
|
|
1427
|
+
} catch {
|
|
1428
|
+
addErrorMessage('Failed to import templates: invalid JSON format.');
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
reader.readAsText(file);
|
|
1432
|
+
};
|
|
1433
|
+
input.click();
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Template button + search binding
|
|
1437
|
+
document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
|
|
1438
|
+
document.getElementById('template-search')?.addEventListener('input', (e) => {
|
|
1439
|
+
renderTemplateList(e.target.value);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1181
1442
|
// ── File Tree ──────────────────────────────────────────────────────
|
|
1182
1443
|
|
|
1183
1444
|
const fileTreeEl = document.getElementById('file-tree');
|
|
@@ -1355,9 +1616,12 @@ if (btnFileTreeRefresh) {
|
|
|
1355
1616
|
|
|
1356
1617
|
// ── Initialize ─────────────────────────────────────────────────────
|
|
1357
1618
|
|
|
1358
|
-
// Restore theme
|
|
1619
|
+
// Restore theme + sync code highlight
|
|
1359
1620
|
const savedTheme = localStorage.getItem('aicli-theme');
|
|
1360
|
-
if (savedTheme)
|
|
1621
|
+
if (savedTheme) {
|
|
1622
|
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
1623
|
+
updateCodeTheme(savedTheme);
|
|
1624
|
+
}
|
|
1361
1625
|
|
|
1362
1626
|
connect();
|
|
1363
1627
|
userInput.focus();
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
10
|
<!-- Markdown + Code highlighting -->
|
|
11
11
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
12
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
|
12
|
+
<link id="hljs-theme" rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
|
13
13
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
14
14
|
<link rel="stylesheet" href="style.css">
|
|
15
15
|
</head>
|
|
@@ -88,7 +88,14 @@
|
|
|
88
88
|
</aside>
|
|
89
89
|
|
|
90
90
|
<!-- Chat Area -->
|
|
91
|
-
<main id="chat-area" class="flex-1 overflow-y-auto px-4 py-4">
|
|
91
|
+
<main id="chat-area" class="flex-1 overflow-y-auto px-4 py-4 relative">
|
|
92
|
+
<!-- Round progress bar (sticky top, hidden by default) -->
|
|
93
|
+
<div id="round-progress" class="round-progress-bar hidden">
|
|
94
|
+
<div class="round-progress-inner">
|
|
95
|
+
<span id="round-progress-label" class="round-progress-label">Round 1/25</span>
|
|
96
|
+
<progress id="round-progress-bar" class="progress progress-primary progress-sm flex-1" value="0" max="100"></progress>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
92
99
|
<div id="messages" class="max-w-4xl mx-auto flex flex-col gap-3">
|
|
93
100
|
<!-- Welcome message -->
|
|
94
101
|
<div class="chat chat-start">
|
|
@@ -105,6 +112,9 @@
|
|
|
105
112
|
<!-- ── Input Area ─────────────────────────────────── -->
|
|
106
113
|
<footer class="bg-base-200 border-t border-base-content/10 px-4 py-3 flex-shrink-0">
|
|
107
114
|
<div class="max-w-4xl mx-auto flex gap-2 items-end">
|
|
115
|
+
<button id="btn-templates" class="btn btn-ghost btn-square btn-sm self-center" title="Prompt templates">
|
|
116
|
+
<span class="text-lg">📝</span>
|
|
117
|
+
</button>
|
|
108
118
|
<textarea id="user-input"
|
|
109
119
|
class="textarea textarea-bordered flex-1 min-h-[2.75rem] max-h-[200px] resize-none leading-relaxed"
|
|
110
120
|
placeholder="Type a message... (Shift+Enter for newline)"
|
|
@@ -130,6 +140,43 @@
|
|
|
130
140
|
|
|
131
141
|
</div>
|
|
132
142
|
|
|
143
|
+
<!-- ── Prompt Templates Modal ───────────────────────── -->
|
|
144
|
+
<dialog id="templates-modal" class="modal">
|
|
145
|
+
<div class="modal-box max-w-2xl bg-base-200">
|
|
146
|
+
<div class="flex items-center justify-between mb-3">
|
|
147
|
+
<h3 class="font-bold text-lg">📝 Prompt Templates</h3>
|
|
148
|
+
<div class="flex gap-1">
|
|
149
|
+
<button class="btn btn-xs btn-ghost" onclick="importTemplates()" title="Import">📥 Import</button>
|
|
150
|
+
<button class="btn btn-xs btn-ghost" onclick="exportTemplates()" title="Export">📤 Export</button>
|
|
151
|
+
<button class="btn btn-xs btn-ghost" onclick="showAddTemplate()" title="New template">+ New</button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<!-- Add/Edit form (hidden by default) -->
|
|
156
|
+
<div id="template-form" class="hidden mb-3 p-3 bg-base-300 rounded-lg">
|
|
157
|
+
<input id="tpl-name" type="text" class="input input-sm input-bordered w-full mb-2" placeholder="Template name">
|
|
158
|
+
<textarea id="tpl-content" class="textarea textarea-bordered w-full text-sm mb-2" rows="4" placeholder="Prompt content..."></textarea>
|
|
159
|
+
<input id="tpl-tags" type="text" class="input input-sm input-bordered w-full mb-2" placeholder="Tags (comma-separated, optional)">
|
|
160
|
+
<div class="flex gap-2 justify-end">
|
|
161
|
+
<button class="btn btn-sm btn-ghost" onclick="cancelTemplateForm()">Cancel</button>
|
|
162
|
+
<button class="btn btn-sm btn-primary" onclick="saveTemplateForm()">Save</button>
|
|
163
|
+
</div>
|
|
164
|
+
<input type="hidden" id="tpl-edit-id">
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<!-- Search -->
|
|
168
|
+
<input id="template-search" type="text" class="input input-sm input-bordered w-full mb-3" placeholder="Search templates...">
|
|
169
|
+
|
|
170
|
+
<!-- Template list -->
|
|
171
|
+
<div id="template-list" class="flex flex-col gap-1 max-h-[50vh] overflow-y-auto"></div>
|
|
172
|
+
|
|
173
|
+
<div class="modal-action mt-3">
|
|
174
|
+
<form method="dialog"><button class="btn btn-sm">Close</button></form>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
178
|
+
</dialog>
|
|
179
|
+
|
|
133
180
|
<script src="app.js"></script>
|
|
134
181
|
</body>
|
|
135
182
|
</html>
|
|
@@ -453,6 +453,112 @@
|
|
|
453
453
|
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
/* ── Round progress bar (sticky top of chat area) ──── */
|
|
457
|
+
.round-progress-bar {
|
|
458
|
+
position: sticky;
|
|
459
|
+
top: 0;
|
|
460
|
+
z-index: 10;
|
|
461
|
+
padding: 0.4rem 1rem;
|
|
462
|
+
background: oklch(var(--b2) / 0.92);
|
|
463
|
+
backdrop-filter: blur(8px);
|
|
464
|
+
border-bottom: 1px solid oklch(var(--bc) / 0.1);
|
|
465
|
+
max-width: 64rem;
|
|
466
|
+
margin: -1rem auto 0.5rem;
|
|
467
|
+
}
|
|
468
|
+
.round-progress-inner {
|
|
469
|
+
display: flex;
|
|
470
|
+
align-items: center;
|
|
471
|
+
gap: 0.75rem;
|
|
472
|
+
}
|
|
473
|
+
.round-progress-label {
|
|
474
|
+
font-size: 0.75rem;
|
|
475
|
+
font-weight: 600;
|
|
476
|
+
opacity: 0.7;
|
|
477
|
+
white-space: nowrap;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* ── Tool duration & timer ─────────────────────────── */
|
|
481
|
+
.tool-timer {
|
|
482
|
+
font-size: 0.7rem;
|
|
483
|
+
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
|
|
484
|
+
opacity: 0.5;
|
|
485
|
+
white-space: nowrap;
|
|
486
|
+
}
|
|
487
|
+
.tool-timer.timing {
|
|
488
|
+
color: oklch(var(--wa));
|
|
489
|
+
opacity: 0.7;
|
|
490
|
+
}
|
|
491
|
+
.tool-duration {
|
|
492
|
+
font-size: 0.7rem;
|
|
493
|
+
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
|
|
494
|
+
opacity: 0.6;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
.tool-duration.fast { color: oklch(var(--su)); }
|
|
498
|
+
.tool-duration.medium { color: oklch(var(--wa)); }
|
|
499
|
+
.tool-duration.slow { color: oklch(var(--er)); }
|
|
500
|
+
|
|
501
|
+
/* ── Prompt templates ──────────────────────────────── */
|
|
502
|
+
.template-item {
|
|
503
|
+
display: flex;
|
|
504
|
+
align-items: flex-start;
|
|
505
|
+
gap: 0.5rem;
|
|
506
|
+
padding: 0.5rem 0.75rem;
|
|
507
|
+
border-radius: 0.375rem;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
transition: background 0.15s;
|
|
510
|
+
border: 1px solid oklch(var(--bc) / 0.08);
|
|
511
|
+
}
|
|
512
|
+
.template-item:hover {
|
|
513
|
+
background: oklch(var(--b3));
|
|
514
|
+
border-color: oklch(var(--p) / 0.3);
|
|
515
|
+
}
|
|
516
|
+
.template-item-body {
|
|
517
|
+
flex: 1;
|
|
518
|
+
min-width: 0;
|
|
519
|
+
}
|
|
520
|
+
.template-item-name {
|
|
521
|
+
font-weight: 600;
|
|
522
|
+
font-size: 0.9rem;
|
|
523
|
+
}
|
|
524
|
+
.template-item-preview {
|
|
525
|
+
font-size: 0.78rem;
|
|
526
|
+
opacity: 0.55;
|
|
527
|
+
white-space: nowrap;
|
|
528
|
+
overflow: hidden;
|
|
529
|
+
text-overflow: ellipsis;
|
|
530
|
+
max-width: 100%;
|
|
531
|
+
}
|
|
532
|
+
.template-item-tags {
|
|
533
|
+
display: flex;
|
|
534
|
+
gap: 0.25rem;
|
|
535
|
+
flex-wrap: wrap;
|
|
536
|
+
margin-top: 0.2rem;
|
|
537
|
+
}
|
|
538
|
+
.template-item-tag {
|
|
539
|
+
font-size: 0.65rem;
|
|
540
|
+
padding: 0.05rem 0.35rem;
|
|
541
|
+
border-radius: 0.25rem;
|
|
542
|
+
background: oklch(var(--p) / 0.12);
|
|
543
|
+
color: oklch(var(--p));
|
|
544
|
+
}
|
|
545
|
+
.template-item-actions {
|
|
546
|
+
display: flex;
|
|
547
|
+
gap: 0.25rem;
|
|
548
|
+
flex-shrink: 0;
|
|
549
|
+
opacity: 0;
|
|
550
|
+
transition: opacity 0.15s;
|
|
551
|
+
}
|
|
552
|
+
.template-item:hover .template-item-actions {
|
|
553
|
+
opacity: 1;
|
|
554
|
+
}
|
|
555
|
+
.template-empty {
|
|
556
|
+
text-align: center;
|
|
557
|
+
padding: 2rem;
|
|
558
|
+
opacity: 0.4;
|
|
559
|
+
font-size: 0.85rem;
|
|
560
|
+
}
|
|
561
|
+
|
|
456
562
|
/* ── Responsive ─────────────────────────────────────── */
|
|
457
563
|
@media (max-width: 768px) {
|
|
458
564
|
.sidebar { width: 0; padding: 0; border: none; }
|