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.
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-6T7KHDLM.js";
19
+ } from "./chunk-KYHUMNQO.js";
20
20
 
21
21
  // src/config/config-manager.ts
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -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.89";
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-3RRSDUVU.js";
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-6T7KHDLM.js";
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-U3HF44WA.js");
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-IDSC72WU.js");
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 () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-6T7KHDLM.js";
5
+ } from "./chunk-KYHUMNQO.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-3RRSDUVU.js";
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
- __require
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 = pjoin(cwd, prefix);
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, pjoin(targetDir, e.name)).replace(/\\/g, "/"),
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 = readFileSync5(fullPath, "utf-8");
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}` });
@@ -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) document.documentElement.setAttribute('data-theme', 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; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",