jinzd-ai-cli 0.4.70 → 0.4.72

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.
@@ -8,7 +8,7 @@ import {
8
8
  RateLimitError,
9
9
  schemaToJsonSchema,
10
10
  truncateForPersist
11
- } from "./chunk-IVTWWDWZ.js";
11
+ } from "./chunk-XH65H3BT.js";
12
12
  import {
13
13
  APP_NAME,
14
14
  CONFIG_DIR_NAME,
@@ -21,7 +21,7 @@ import {
21
21
  MCP_TOOL_PREFIX,
22
22
  PLUGINS_DIR_NAME,
23
23
  VERSION
24
- } from "./chunk-ND3O5NQU.js";
24
+ } from "./chunk-73UI5AH7.js";
25
25
 
26
26
  // src/config/config-manager.ts
27
27
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.4.70";
11
+ var VERSION = "0.4.72";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -6,7 +6,7 @@ import { platform } from "os";
6
6
  import chalk from "chalk";
7
7
 
8
8
  // src/core/constants.ts
9
- var VERSION = "0.4.70";
9
+ var VERSION = "0.4.72";
10
10
  var APP_NAME = "ai-cli";
11
11
  var CONFIG_DIR_NAME = ".aicli";
12
12
  var CONFIG_FILE_NAME = "config.json";
@@ -10,7 +10,7 @@ import {
10
10
  SUBAGENT_DEFAULT_MAX_ROUNDS,
11
11
  SUBAGENT_MAX_ROUNDS_LIMIT,
12
12
  runTestsTool
13
- } from "./chunk-ND3O5NQU.js";
13
+ } from "./chunk-73UI5AH7.js";
14
14
 
15
15
  // src/tools/builtin/bash.ts
16
16
  import { execSync } from "child_process";
@@ -1783,6 +1783,118 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
1783
1783
 
1784
1784
  // src/tools/builtin/edit-file.ts
1785
1785
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
1786
+
1787
+ // src/tools/builtin/patch-apply.ts
1788
+ function parseUnifiedDiff(patch) {
1789
+ const lines = patch.split("\n");
1790
+ const hunks = [];
1791
+ let current = null;
1792
+ const headerRe = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/;
1793
+ for (let i = 0; i < lines.length; i++) {
1794
+ const line = lines[i];
1795
+ const m = line.match(headerRe);
1796
+ if (m) {
1797
+ if (current) hunks.push(current);
1798
+ current = {
1799
+ oldStart: parseInt(m[1], 10) || 1,
1800
+ oldLines: [],
1801
+ newLines: [],
1802
+ header: line
1803
+ };
1804
+ continue;
1805
+ }
1806
+ if (!current) continue;
1807
+ if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) {
1808
+ hunks.push(current);
1809
+ current = null;
1810
+ continue;
1811
+ }
1812
+ if (line.startsWith("\\")) continue;
1813
+ const tag = line[0];
1814
+ const body = line.slice(1);
1815
+ if (tag === " " || tag === void 0) {
1816
+ current.oldLines.push(body ?? "");
1817
+ current.newLines.push(body ?? "");
1818
+ } else if (tag === "-") {
1819
+ current.oldLines.push(body);
1820
+ } else if (tag === "+") {
1821
+ current.newLines.push(body);
1822
+ } else {
1823
+ current.oldLines.push(line);
1824
+ current.newLines.push(line);
1825
+ }
1826
+ }
1827
+ if (current) hunks.push(current);
1828
+ return hunks;
1829
+ }
1830
+ function locate(haystack, needle, hintLine1Based, searchWindow = 200) {
1831
+ if (needle.length === 0) return { index: Math.max(0, hintLine1Based - 1), mode: "exact" };
1832
+ const hint = Math.max(0, hintLine1Based - 1);
1833
+ const matchExactAt = (start) => {
1834
+ if (start < 0 || start + needle.length > haystack.length) return false;
1835
+ for (let j = 0; j < needle.length; j++) {
1836
+ if (haystack[start + j] !== needle[j]) return false;
1837
+ }
1838
+ return true;
1839
+ };
1840
+ const matchWsAt = (start) => {
1841
+ if (start < 0 || start + needle.length > haystack.length) return false;
1842
+ for (let j = 0; j < needle.length; j++) {
1843
+ if ((haystack[start + j] ?? "").trim() !== needle[j].trim()) return false;
1844
+ }
1845
+ return true;
1846
+ };
1847
+ if (matchExactAt(hint)) return { index: hint, mode: "exact" };
1848
+ for (let d = 1; d <= searchWindow; d++) {
1849
+ if (matchExactAt(hint - d)) return { index: hint - d, mode: "fuzzy" };
1850
+ if (matchExactAt(hint + d)) return { index: hint + d, mode: "fuzzy" };
1851
+ }
1852
+ for (let d = 0; d <= searchWindow; d++) {
1853
+ if (matchWsAt(hint - d)) return { index: hint - d, mode: "ws" };
1854
+ if (d > 0 && matchWsAt(hint + d)) return { index: hint + d, mode: "ws" };
1855
+ }
1856
+ for (let i = 0; i <= haystack.length - needle.length; i++) {
1857
+ if (matchWsAt(i)) return { index: i, mode: "ws-global" };
1858
+ }
1859
+ return null;
1860
+ }
1861
+ function applyUnifiedPatch(content, hunks, options = {}) {
1862
+ const stopOnError = options.stopOnError !== false;
1863
+ const lines = content.split("\n");
1864
+ let working = lines.slice();
1865
+ let offset = 0;
1866
+ const reports = [];
1867
+ let appliedCount = 0;
1868
+ for (let i = 0; i < hunks.length; i++) {
1869
+ const h = hunks[i];
1870
+ const hint = h.oldStart + offset;
1871
+ const loc = locate(working, h.oldLines, hint);
1872
+ if (!loc) {
1873
+ reports.push({
1874
+ index: i,
1875
+ ok: false,
1876
+ detail: `Hunk @${h.header} \u2014 context not found near line ${h.oldStart} (${h.oldLines.length} old lines).`
1877
+ });
1878
+ if (stopOnError) {
1879
+ return { ok: false, content: void 0, reports, appliedCount };
1880
+ }
1881
+ continue;
1882
+ }
1883
+ const before = working.slice(0, loc.index);
1884
+ const after = working.slice(loc.index + h.oldLines.length);
1885
+ working = before.concat(h.newLines, after);
1886
+ offset += h.newLines.length - h.oldLines.length;
1887
+ appliedCount++;
1888
+ reports.push({
1889
+ index: i,
1890
+ ok: true,
1891
+ detail: `Applied at line ${loc.index + 1} (${loc.mode}${loc.index + 1 === hint ? "" : `, drift ${loc.index + 1 - hint}`}): -${h.oldLines.length} +${h.newLines.length}`
1892
+ });
1893
+ }
1894
+ return { ok: true, content: working.join("\n"), reports, appliedCount };
1895
+ }
1896
+
1897
+ // src/tools/builtin/edit-file.ts
1786
1898
  function similarityScore(a, b) {
1787
1899
  if (a === b) return 1;
1788
1900
  if (a.length < 2 || b.length < 2) return 0;
@@ -1948,11 +2060,12 @@ function parseEditsArg(raw) {
1948
2060
  var editFileTool = {
1949
2061
  definition: {
1950
2062
  name: "edit_file",
1951
- description: `Precisely edit file contents. Four modes:
2063
+ description: `Precisely edit file contents. Five modes:
1952
2064
  1. String replace (most common): Provide old_str and new_str to replace an exact match. old_str must appear exactly once (unless replace_all is true).
1953
2065
  2. Line insert: Provide insert_after_line (1-based) and insert_content.
1954
2066
  3. Line delete: Provide delete_from_line and delete_to_line (inclusive).
1955
2067
  4. Batch edits: Provide edits=[{old_str, new_str, ignore_whitespace?, replace_all?}, ...] to apply MULTIPLE edits in ONE call \u2014 saves tool rounds/tokens when refactoring a file. Edits are applied sequentially in-memory; by default any failure rolls back ALL edits (set stop_on_error=false to apply successful ones and report failures).
2068
+ 5. Unified diff patch: Provide patch=<unified-diff string> with one or more '@@ -a,b +c,d @@' hunks. Use this for MANY scattered small changes in a LARGE file \u2014 most compact format (context lines are shared between old/new). Supports line-number drift (\xB1200 lines) and whitespace-tolerant fallback. Format each hunk as standard unified diff: ' ' context / '-' remove / '+' add. File headers (---/+++) are optional and ignored.
1956
2069
  Optional ignore_whitespace: true ignores indentation differences during matching.
1957
2070
  Optional replace_all: true replaces ALL occurrences of old_str.
1958
2071
  Note: Path can be absolute or relative to cwd.`,
@@ -2009,7 +2122,12 @@ Note: Path can be absolute or relative to cwd.`,
2009
2122
  },
2010
2123
  stop_on_error: {
2011
2124
  type: "boolean",
2012
- description: "[Batch mode] If true (default), any failing edit rolls back the whole batch and writes nothing. If false, successful edits are written and failed ones are reported.",
2125
+ description: "[Batch mode / Patch mode] If true (default), any failing edit/hunk rolls back the whole operation and writes nothing. If false, successful edits are written and failed ones are reported.",
2126
+ required: false
2127
+ },
2128
+ patch: {
2129
+ type: "string",
2130
+ description: '[Patch mode] A unified-diff string with one or more "@@ -oldStart,oldLines +newStart,newLines @@" hunks. Most compact for many scattered small changes. Tolerates line-number drift (\xB1200 lines) and whitespace differences.',
2013
2131
  required: false
2014
2132
  },
2015
2133
  encoding: {
@@ -2027,6 +2145,39 @@ Note: Path can be absolute or relative to cwd.`,
2027
2145
  if (!filePath) throw new ToolError("edit_file", "path is required");
2028
2146
  if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
2029
2147
  const original = readFileSync4(filePath, encoding);
2148
+ if (args["patch"] !== void 0) {
2149
+ const patchText = String(args["patch"] ?? "");
2150
+ const stopOnError = args["stop_on_error"] !== false;
2151
+ if (!patchText.trim()) {
2152
+ throw new ToolError("edit_file", "patch is empty");
2153
+ }
2154
+ const hunks = parseUnifiedDiff(patchText);
2155
+ if (hunks.length === 0) {
2156
+ throw new ToolError(
2157
+ "edit_file",
2158
+ 'patch contained no hunks. Expected unified diff format with "@@ -a,b +c,d @@" headers.'
2159
+ );
2160
+ }
2161
+ const res = applyUnifiedPatch(original, hunks, { stopOnError });
2162
+ const lines = [];
2163
+ if (!res.ok) {
2164
+ lines.push(`ERROR: Patch aborted \u2014 ${res.appliedCount}/${hunks.length} hunks applied, then a hunk failed. No changes written (stop_on_error=true).`);
2165
+ } else if (res.appliedCount < hunks.length) {
2166
+ lines.push(`Partial success: ${res.appliedCount}/${hunks.length} hunks applied to ${filePath} (stop_on_error=false).`);
2167
+ } else {
2168
+ lines.push(`Successfully applied ${res.appliedCount}/${hunks.length} hunk(s) to ${filePath}.`);
2169
+ }
2170
+ lines.push("");
2171
+ for (const r of res.reports) {
2172
+ lines.push(r.ok ? ` \u2713 #${r.index + 1}: ${r.detail}` : ` \u2717 #${r.index + 1}: ${r.detail}`);
2173
+ }
2174
+ if (res.ok && res.appliedCount > 0 && res.content !== void 0) {
2175
+ undoStack.push(filePath, `edit_file (patch ${res.appliedCount}/${hunks.length}): ${filePath}`);
2176
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
2177
+ writeFileSync3(filePath, res.content, encoding);
2178
+ }
2179
+ return lines.join("\n");
2180
+ }
2030
2181
  if (args["edits"] !== void 0) {
2031
2182
  const edits = parseEditsArg(args["edits"]);
2032
2183
  const stopOnError = args["stop_on_error"] !== false;
@@ -2140,7 +2291,7 @@ Please read the file first and use exact text.`;
2140
2291
  }
2141
2292
  throw new ToolError(
2142
2293
  "edit_file",
2143
- "No operation specified. Provide one of: (old_str + new_str), (insert_after_line + insert_content), (delete_from_line + delete_to_line), or edits=[...]."
2294
+ "No operation specified. Provide one of: (old_str + new_str), (insert_after_line + insert_content), (delete_from_line + delete_to_line), edits=[...], or patch=<unified diff>."
2144
2295
  );
2145
2296
  }
2146
2297
  };
@@ -385,7 +385,7 @@ ${content}`);
385
385
  }
386
386
  }
387
387
  async function runTaskMode(config, providers, configManager, topic) {
388
- const { TaskOrchestrator } = await import("./task-orchestrator-EB2XPY5S.js");
388
+ const { TaskOrchestrator } = await import("./task-orchestrator-R33SWTHO.js");
389
389
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
390
390
  let interrupted = false;
391
391
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  saveDevState,
32
32
  sessionHasMeaningfulContent,
33
33
  setupProxy
34
- } from "./chunk-3YT2DUUT.js";
34
+ } from "./chunk-57HHY5ZX.js";
35
35
  import {
36
36
  ToolExecutor,
37
37
  ToolRegistry,
@@ -47,7 +47,7 @@ import {
47
47
  spawnAgentContext,
48
48
  theme,
49
49
  undoStack
50
- } from "./chunk-IVTWWDWZ.js";
50
+ } from "./chunk-XH65H3BT.js";
51
51
  import {
52
52
  fileCheckpoints
53
53
  } from "./chunk-4BKXL7SM.js";
@@ -72,7 +72,7 @@ import {
72
72
  SKILLS_DIR_NAME,
73
73
  VERSION,
74
74
  buildUserIdentityPrompt
75
- } from "./chunk-ND3O5NQU.js";
75
+ } from "./chunk-73UI5AH7.js";
76
76
 
77
77
  // src/index.ts
78
78
  import { program } from "commander";
@@ -2267,7 +2267,7 @@ ${hint}` : "")
2267
2267
  usage: "/test [command|filter]",
2268
2268
  async execute(args, ctx) {
2269
2269
  try {
2270
- const { executeTests } = await import("./run-tests-NPRCZYN3.js");
2270
+ const { executeTests } = await import("./run-tests-4565WOUK.js");
2271
2271
  const argStr = args.join(" ").trim();
2272
2272
  let testArgs = {};
2273
2273
  if (argStr) {
@@ -6139,7 +6139,7 @@ program.command("web").description("Start Web UI server with browser-based chat
6139
6139
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
6140
6140
  process.exit(1);
6141
6141
  }
6142
- const { startWebServer } = await import("./server-WYL3OD5N.js");
6142
+ const { startWebServer } = await import("./server-Y4CT5IJJ.js");
6143
6143
  await startWebServer({ port, host: options.host });
6144
6144
  });
6145
6145
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -6372,7 +6372,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
6372
6372
  }),
6373
6373
  config.get("customProviders")
6374
6374
  );
6375
- const { startHub } = await import("./hub-BGO4X72R.js");
6375
+ const { startHub } = await import("./hub-NQADIYTS.js");
6376
6376
  await startHub(
6377
6377
  {
6378
6378
  topic: topic ?? "",
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-ND3O5NQU.js";
5
+ } from "./chunk-73UI5AH7.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-YJKPARSH.js";
4
+ } from "./chunk-OKNKH5D7.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -21,7 +21,7 @@ import {
21
21
  persistToolRound,
22
22
  rebuildExtraMessages,
23
23
  setupProxy
24
- } from "./chunk-3YT2DUUT.js";
24
+ } from "./chunk-57HHY5ZX.js";
25
25
  import {
26
26
  AuthManager
27
27
  } from "./chunk-BYNY5JPB.js";
@@ -42,7 +42,7 @@ import {
42
42
  spawnAgentContext,
43
43
  truncateOutput,
44
44
  undoStack
45
- } from "./chunk-IVTWWDWZ.js";
45
+ } from "./chunk-XH65H3BT.js";
46
46
  import "./chunk-4BKXL7SM.js";
47
47
  import {
48
48
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -62,7 +62,7 @@ import {
62
62
  SKILLS_DIR_NAME,
63
63
  VERSION,
64
64
  buildUserIdentityPrompt
65
- } from "./chunk-ND3O5NQU.js";
65
+ } from "./chunk-73UI5AH7.js";
66
66
 
67
67
  // src/web/server.ts
68
68
  import express from "express";
@@ -1948,7 +1948,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1948
1948
  case "test": {
1949
1949
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
1950
1950
  try {
1951
- const { executeTests } = await import("./run-tests-NPRCZYN3.js");
1951
+ const { executeTests } = await import("./run-tests-4565WOUK.js");
1952
1952
  const argStr = args.join(" ").trim();
1953
1953
  let testArgs = {};
1954
1954
  if (argStr) {
@@ -3010,6 +3010,26 @@ async function startWebServer(options = {}) {
3010
3010
  res.json({ sessions: [] });
3011
3011
  }
3012
3012
  });
3013
+ app.get("/api/sessions/:id/replay", requireAuth, (req, res) => {
3014
+ const id = req.params.id;
3015
+ if (!/^[a-f0-9-]{36}$/i.test(id)) {
3016
+ res.status(400).json({ error: "Invalid session id" });
3017
+ return;
3018
+ }
3019
+ try {
3020
+ const authUser = req._authUser;
3021
+ const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
3022
+ const filePath = join3(histDir, `${id}.json`);
3023
+ if (!existsSync4(filePath)) {
3024
+ res.status(404).json({ error: "Session not found" });
3025
+ return;
3026
+ }
3027
+ const data = JSON.parse(readFileSync4(filePath, "utf-8"));
3028
+ res.json({ session: data });
3029
+ } catch (err) {
3030
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
3031
+ }
3032
+ });
3013
3033
  app.get("/api/file-content", requireAuth, (req, res) => {
3014
3034
  const filePath = req.query.path;
3015
3035
  if (!filePath) {
@@ -4,11 +4,11 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-IVTWWDWZ.js";
7
+ } from "./chunk-XH65H3BT.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
9
  import {
10
10
  SUBAGENT_ALLOWED_TOOLS
11
- } from "./chunk-ND3O5NQU.js";
11
+ } from "./chunk-73UI5AH7.js";
12
12
 
13
13
  // src/hub/task-orchestrator.ts
14
14
  import { createInterface } from "readline";
@@ -965,7 +965,7 @@ function renderFilteredSessions(filter) {
965
965
  <div class="flex items-center gap-1">
966
966
  ${checkbox}
967
967
  <div class="session-title flex-1">${escapeHtml(title)}</div>
968
- ${batchSelectMode ? '' : `<button class="session-delete-btn opacity-0 hover:opacity-100 text-error text-xs px-1 flex-shrink-0" data-delete-id="${s.id}" title="Delete session">&times;</button>`}
968
+ ${batchSelectMode ? '' : `<button class="session-replay-btn opacity-0 hover:opacity-100 text-xs px-1 flex-shrink-0" data-replay-id="${s.id}" title="Replay session">🎬</button><button class="session-delete-btn opacity-0 hover:opacity-100 text-error text-xs px-1 flex-shrink-0" data-delete-id="${s.id}" title="Delete session">&times;</button>`}
969
969
  </div>
970
970
  <div class="session-meta">${s.messageCount} msgs · ${timeStr}</div>
971
971
  </div>`;
@@ -976,7 +976,7 @@ function renderFilteredSessions(filter) {
976
976
  let clickTimer = null;
977
977
 
978
978
  el.addEventListener('click', (e) => {
979
- if (e.target.closest('.session-delete-btn') || e.target.closest('.session-batch-cb') || e.target.closest('.session-rename-input')) return;
979
+ if (e.target.closest('.session-delete-btn') || e.target.closest('.session-replay-btn') || e.target.closest('.session-batch-cb') || e.target.closest('.session-rename-input')) return;
980
980
  if (batchSelectMode) {
981
981
  const cb = el.querySelector('.session-batch-cb');
982
982
  if (cb) { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }
@@ -1032,9 +1032,121 @@ function renderFilteredSessions(filter) {
1032
1032
  }
1033
1033
  });
1034
1034
  });
1035
+
1036
+ sessionListEl.querySelectorAll('.session-replay-btn').forEach(btn => {
1037
+ btn.addEventListener('click', (e) => {
1038
+ e.stopPropagation();
1039
+ const id = btn.dataset.replayId;
1040
+ if (id) openReplay(id);
1041
+ });
1042
+ });
1035
1043
  }
1036
1044
  }
1037
1045
 
1046
+ // ── Session Replay (B1) ─────────────────────────────────
1047
+ async function openReplay(sessionId) {
1048
+ const modal = document.getElementById('replay-modal');
1049
+ const metaEl = document.getElementById('replay-meta');
1050
+ const usageEl = document.getElementById('replay-usage');
1051
+ const timelineEl = document.getElementById('replay-timeline');
1052
+ if (!modal || !metaEl || !usageEl || !timelineEl) return;
1053
+
1054
+ metaEl.textContent = 'Loading…';
1055
+ usageEl.textContent = '';
1056
+ timelineEl.innerHTML = '';
1057
+ modal.showModal();
1058
+
1059
+ try {
1060
+ const headers = {};
1061
+ if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
1062
+ const resp = await fetch('/api/sessions/' + encodeURIComponent(sessionId) + '/replay', { headers });
1063
+ if (!resp.ok) {
1064
+ const body = await resp.json().catch(() => ({}));
1065
+ metaEl.textContent = 'Failed: ' + (body.error || resp.status);
1066
+ return;
1067
+ }
1068
+ const { session } = await resp.json();
1069
+ renderReplay(session, metaEl, usageEl, timelineEl);
1070
+ } catch (err) {
1071
+ metaEl.textContent = 'Failed to load: ' + (err && err.message ? err.message : err);
1072
+ }
1073
+ }
1074
+
1075
+ function renderReplay(session, metaEl, usageEl, timelineEl) {
1076
+ const created = session.created ? new Date(session.created).toLocaleString() : '-';
1077
+ const updated = session.updated ? new Date(session.updated).toLocaleString() : '-';
1078
+ const title = session.title || 'Untitled';
1079
+ metaEl.innerHTML =
1080
+ `<div><b>${escapeHtml(title)}</b></div>` +
1081
+ `<div>${escapeHtml(session.provider || '?')} / ${escapeHtml(session.model || '?')} · ${escapeHtml(session.id || '')}</div>` +
1082
+ `<div>Created ${created} · Updated ${updated} · ${(session.messages || []).length} messages</div>`;
1083
+
1084
+ const tu = session.tokenUsage || {};
1085
+ const total = (tu.inputTokens || 0) + (tu.outputTokens || 0);
1086
+ usageEl.innerHTML =
1087
+ `<span class="badge badge-sm badge-ghost">total ${total}</span> ` +
1088
+ `<span class="badge badge-sm badge-ghost">in ${tu.inputTokens || 0}</span> ` +
1089
+ `<span class="badge badge-sm badge-ghost">out ${tu.outputTokens || 0}</span> ` +
1090
+ `<span class="badge badge-sm badge-ghost">cache-write ${tu.cacheCreationTokens || 0}</span> ` +
1091
+ `<span class="badge badge-sm badge-ghost">cache-read ${tu.cacheReadTokens || 0}</span>`;
1092
+
1093
+ const messages = Array.isArray(session.messages) ? session.messages : [];
1094
+ timelineEl.innerHTML = messages.map((m, i) => renderReplayStep(m, i)).join('');
1095
+ }
1096
+
1097
+ function renderReplayStep(m, idx) {
1098
+ const role = m.role || 'user';
1099
+ const ts = m.timestamp ? new Date(m.timestamp).toLocaleTimeString() : '';
1100
+ const isError = !!m.isError;
1101
+ const cls = 'replay-step role-' + role + (isError ? ' error' : '');
1102
+ const roleTag = role + (m.toolName ? ' · ' + m.toolName : '') + (isError ? ' · ERROR' : '');
1103
+
1104
+ // Extract text content
1105
+ let text = '';
1106
+ if (typeof m.content === 'string') {
1107
+ text = m.content;
1108
+ } else if (Array.isArray(m.content)) {
1109
+ text = m.content
1110
+ .map(p => p && p.type === 'text' ? (p.text || '') : (p && p.type === 'image_url' ? '[image]' : ''))
1111
+ .join('');
1112
+ }
1113
+
1114
+ let body = '';
1115
+ if (text) {
1116
+ const isTool = role === 'tool';
1117
+ const bodyCls = isTool ? 'replay-step-body' : 'replay-step-body text-body';
1118
+ body += `<div class="${bodyCls}">${escapeHtml(text)}</div>`;
1119
+ }
1120
+
1121
+ // Tool calls (assistant asking to invoke tools)
1122
+ if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
1123
+ body += m.toolCalls.map(tc => {
1124
+ const name = tc.name || tc.function?.name || '(unknown)';
1125
+ let args = tc.arguments ?? tc.function?.arguments ?? {};
1126
+ if (typeof args === 'string') {
1127
+ try { args = JSON.parse(args); } catch { /* keep as string */ }
1128
+ }
1129
+ const argsStr = typeof args === 'string' ? args : JSON.stringify(args, null, 2);
1130
+ return `<div class="replay-tool-block"><div><span class="tool-name">→ ${escapeHtml(name)}</span> <span class="opacity-60">${escapeHtml(tc.id || '')}</span></div><pre>${escapeHtml(argsStr)}</pre></div>`;
1131
+ }).join('');
1132
+ }
1133
+
1134
+ // Reasoning content (thinking models)
1135
+ if (m.reasoningContent) {
1136
+ body += `<details class="replay-tool-block"><summary class="opacity-70">💭 reasoning</summary><pre>${escapeHtml(m.reasoningContent)}</pre></details>`;
1137
+ }
1138
+
1139
+ return `<div class="${cls}">
1140
+ <div class="replay-step-header">
1141
+ <span class="opacity-50">#${idx + 1}</span>
1142
+ <span class="role-tag">${escapeHtml(roleTag)}</span>
1143
+ <span class="opacity-50">${ts}</span>
1144
+ ${m.toolCallId ? `<span class="opacity-40">↳ ${escapeHtml(m.toolCallId)}</span>` : ''}
1145
+ </div>
1146
+ ${body || '<div class="opacity-40 text-xs">(empty)</div>'}
1147
+ </div>`;
1148
+ }
1149
+
1038
1150
  function startSessionRename(itemEl, titleEl) {
1039
1151
  const sessionId = itemEl.dataset.sessionId;
1040
1152
  const currentTitle = titleEl.textContent.trim();
@@ -249,6 +249,20 @@
249
249
  <form method="dialog" class="modal-backdrop"><button>close</button></form>
250
250
  </dialog>
251
251
 
252
+ <!-- ── Session Replay Modal (B1) ───────────────────────── -->
253
+ <dialog id="replay-modal" class="modal">
254
+ <div class="modal-box max-w-5xl bg-base-200 w-11/12">
255
+ <div class="flex items-center justify-between mb-3">
256
+ <h3 class="font-bold text-lg">🎬 Session Replay</h3>
257
+ <form method="dialog"><button class="btn btn-sm btn-ghost">✕</button></form>
258
+ </div>
259
+ <div id="replay-meta" class="text-xs opacity-70 mb-2"></div>
260
+ <div id="replay-usage" class="text-xs mb-3"></div>
261
+ <div id="replay-timeline" class="flex flex-col gap-2 max-h-[70vh] overflow-y-auto pr-2"></div>
262
+ </div>
263
+ <form method="dialog" class="modal-backdrop"><button>close</button></form>
264
+ </dialog>
265
+
252
266
  <script src="app.js"></script>
253
267
  <script>
254
268
  if ('serviceWorker' in navigator) {
@@ -845,3 +845,61 @@ button, a, .session-item, .file-tree-row, .template-item, .tool-item, .mcp-serve
845
845
  @media (display-mode: standalone) {
846
846
  .navbar { padding-top: env(safe-area-inset-top, 0px); }
847
847
  }
848
+
849
+ /* ── Session Replay (B1) ───────────────────────────── */
850
+ .replay-step {
851
+ border-left: 3px solid hsl(var(--b3));
852
+ padding: 0.5rem 0.6rem;
853
+ background: hsl(var(--b1));
854
+ border-radius: 0 0.35rem 0.35rem 0;
855
+ font-size: 0.85rem;
856
+ }
857
+ .replay-step.role-user { border-left-color: #3b82f6; }
858
+ .replay-step.role-assistant { border-left-color: #10b981; }
859
+ .replay-step.role-tool { border-left-color: #f59e0b; }
860
+ .replay-step.role-tool.error { border-left-color: #ef4444; }
861
+ .replay-step-header {
862
+ display: flex;
863
+ gap: 0.5rem;
864
+ align-items: center;
865
+ font-size: 0.72rem;
866
+ opacity: 0.75;
867
+ margin-bottom: 0.25rem;
868
+ }
869
+ .replay-step-header .role-tag {
870
+ font-weight: 600;
871
+ padding: 0 0.35rem;
872
+ border-radius: 0.25rem;
873
+ background: hsl(var(--b3));
874
+ }
875
+ .replay-step-body {
876
+ white-space: pre-wrap;
877
+ word-break: break-word;
878
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
879
+ font-size: 0.78rem;
880
+ max-height: 18rem;
881
+ overflow-y: auto;
882
+ }
883
+ .replay-step-body.text-body {
884
+ font-family: inherit;
885
+ font-size: 0.85rem;
886
+ }
887
+ .replay-tool-block {
888
+ margin-top: 0.3rem;
889
+ padding: 0.4rem;
890
+ background: hsl(var(--b2));
891
+ border-radius: 0.3rem;
892
+ font-size: 0.78rem;
893
+ }
894
+ .replay-tool-block .tool-name {
895
+ font-weight: 600;
896
+ color: #f59e0b;
897
+ }
898
+ .replay-tool-block pre {
899
+ margin: 0.2rem 0 0;
900
+ white-space: pre-wrap;
901
+ word-break: break-word;
902
+ font-size: 0.72rem;
903
+ max-height: 12rem;
904
+ overflow-y: auto;
905
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.70",
3
+ "version": "0.4.72",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",