jinzd-ai-cli 0.4.74 → 0.4.76

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.
@@ -20,13 +20,7 @@ import {
20
20
  persistToolRound,
21
21
  rebuildExtraMessages,
22
22
  setupProxy
23
- } from "./chunk-PLJUAA3J.js";
24
- import {
25
- AuthManager
26
- } from "./chunk-BYNY5JPB.js";
27
- import {
28
- ConfigManager
29
- } from "./chunk-VG3MFZYG.js";
23
+ } from "./chunk-XTH7S3AM.js";
30
24
  import {
31
25
  ToolExecutor,
32
26
  ToolRegistry,
@@ -44,9 +38,17 @@ import {
44
38
  spawnAgentContext,
45
39
  truncateOutput,
46
40
  undoStack
47
- } from "./chunk-D5ZDVEJJ.js";
41
+ } from "./chunk-KR4FTJWB.js";
48
42
  import "./chunk-4BKXL7SM.js";
49
- import "./chunk-FKVJRBPO.js";
43
+ import "./chunk-NHNWUBXB.js";
44
+ import "./chunk-6VRJGH25.js";
45
+ import "./chunk-K3JJX2Z5.js";
46
+ import {
47
+ AuthManager
48
+ } from "./chunk-BYNY5JPB.js";
49
+ import {
50
+ ConfigManager
51
+ } from "./chunk-H4DQNZZ6.js";
50
52
  import "./chunk-2ZD3YTVM.js";
51
53
  import {
52
54
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -66,7 +68,7 @@ import {
66
68
  SKILLS_DIR_NAME,
67
69
  VERSION,
68
70
  buildUserIdentityPrompt
69
- } from "./chunk-BT2TCINO.js";
71
+ } from "./chunk-2Q77FT3F.js";
70
72
 
71
73
  // src/web/server.ts
72
74
  import express from "express";
@@ -563,6 +565,15 @@ var SessionHandler = class _SessionHandler {
563
565
  models: p.info.models.map((m) => ({ id: m.id, name: m.displayName ?? m.id }))
564
566
  }));
565
567
  const costUsd = computeCost(this.currentProvider, this.currentModel, this.sessionTokenUsage);
568
+ const sess = this.sessions.current;
569
+ const branches = sess ? sess.listBranches().map((b) => ({
570
+ id: b.id,
571
+ title: b.title,
572
+ parentBranchId: b.parentBranchId,
573
+ parentMessageIndex: b.parentMessageIndex,
574
+ created: b.created.toISOString(),
575
+ messageCount: b.id === sess.activeBranchId ? sess.messages.length : sess.getBranchMessages(b.id)?.length ?? 0
576
+ })) : [];
566
577
  this.send({
567
578
  type: "status",
568
579
  provider: this.currentProvider,
@@ -574,7 +585,9 @@ var SessionHandler = class _SessionHandler {
574
585
  thinkingMode: this.runtimeThinking ?? false,
575
586
  tokenUsage: { ...this.sessionTokenUsage },
576
587
  costUsd,
577
- providers: providerList
588
+ providers: providerList,
589
+ branches,
590
+ activeBranchId: sess?.activeBranchId ?? "main"
578
591
  });
579
592
  }
580
593
  async handleMessage(raw) {
@@ -1467,6 +1480,8 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1467
1480
  " /diff [--stats] \u2014 Show file modifications in this session",
1468
1481
  " /checkpoint [save|restore|delete] <name> \u2014 Session checkpoints",
1469
1482
  " /fork [checkpoint] \u2014 Fork session from checkpoint or current",
1483
+ " /branch [list|new|switch|delete|rename] \u2014 Manage conversation branches",
1484
+ " /index [status|rebuild|clear] \u2014 Symbol index for find_symbol / get_outline / find_references",
1470
1485
  " /review [--staged] \u2014 AI code review from git diff",
1471
1486
  " /security-review \u2014 Security vulnerability scan on git diff",
1472
1487
  " /rewind [list|<n>] \u2014 Rewind conversation & restore files to checkpoint",
@@ -1860,6 +1875,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1860
1875
  if (ok) {
1861
1876
  await this.sessions.save();
1862
1877
  this.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
1878
+ this.sendStatus();
1863
1879
  } else {
1864
1880
  this.send({ type: "error", message: `Cannot delete "${id}" (not found, active, or last remaining branch).` });
1865
1881
  }
@@ -1876,6 +1892,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1876
1892
  if (ok) {
1877
1893
  await this.sessions.save();
1878
1894
  this.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
1895
+ this.sendStatus();
1879
1896
  } else {
1880
1897
  this.send({ type: "error", message: `Branch "${id}" not found.` });
1881
1898
  }
@@ -1884,6 +1901,43 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
1884
1901
  this.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename.` });
1885
1902
  break;
1886
1903
  }
1904
+ // ── /index ──────────────────────────────────────────────────────
1905
+ case "index": {
1906
+ const sub = (args[0] ?? "status").toLowerCase();
1907
+ const root = process.cwd();
1908
+ const { loadIndex, clearIndex } = await import("./store-S24SPPDZ.js");
1909
+ const { indexProject } = await import("./indexer-C7QYYHSZ.js");
1910
+ if (sub === "status") {
1911
+ const idx = loadIndex(root);
1912
+ if (!idx) {
1913
+ this.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
1914
+ } else {
1915
+ this.send({
1916
+ type: "info",
1917
+ message: [
1918
+ "\u{1F50E} Symbol index:",
1919
+ ` Root: ${idx.root}`,
1920
+ ` Generated: ${idx.generated}`,
1921
+ ` Files: ${idx.fileCount}`,
1922
+ ` Symbols: ${idx.symbolCount}`
1923
+ ].join("\n")
1924
+ });
1925
+ }
1926
+ } else if (sub === "rebuild") {
1927
+ this.send({ type: "info", message: `Indexing ${root}\u2026` });
1928
+ const { stats } = await indexProject(root, { force: true });
1929
+ this.send({
1930
+ type: "info",
1931
+ message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
1932
+ });
1933
+ } else if (sub === "clear") {
1934
+ clearIndex(root);
1935
+ this.send({ type: "info", message: `\u2713 Cleared symbol index for ${root}` });
1936
+ } else {
1937
+ this.send({ type: "error", message: `Unknown subcommand: ${sub}. Use status/rebuild/clear.` });
1938
+ }
1939
+ break;
1940
+ }
1887
1941
  // ── /fork ───────────────────────────────────────────────────────
1888
1942
  case "fork": {
1889
1943
  const session = this.sessions.current;
@@ -2049,7 +2103,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
2049
2103
  case "test": {
2050
2104
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
2051
2105
  try {
2052
- const { executeTests } = await import("./run-tests-HBLD2R6B.js");
2106
+ const { executeTests } = await import("./run-tests-MKKCDUUV.js");
2053
2107
  const argStr = args.join(" ").trim();
2054
2108
  let testArgs = {};
2055
2109
  if (argStr) {
@@ -4,13 +4,15 @@ import {
4
4
  getDangerLevel,
5
5
  googleSearchContext,
6
6
  truncateOutput
7
- } from "./chunk-D5ZDVEJJ.js";
7
+ } from "./chunk-KR4FTJWB.js";
8
8
  import "./chunk-4BKXL7SM.js";
9
- import "./chunk-FKVJRBPO.js";
9
+ import "./chunk-NHNWUBXB.js";
10
+ import "./chunk-6VRJGH25.js";
11
+ import "./chunk-K3JJX2Z5.js";
10
12
  import "./chunk-2ZD3YTVM.js";
11
13
  import {
12
14
  SUBAGENT_ALLOWED_TOOLS
13
- } from "./chunk-BT2TCINO.js";
15
+ } from "./chunk-2Q77FT3F.js";
14
16
 
15
17
  // src/hub/task-orchestrator.ts
16
18
  import { createInterface } from "readline";
Binary file
Binary file
@@ -519,6 +519,17 @@ function handleStatus(msg) {
519
519
  }
520
520
  if (msg.tokenUsage) targetTab.tokenUsage = msg.tokenUsage;
521
521
 
522
+ // B2: stash branch data on the tab so tab-switch keeps the picker in sync.
523
+ if (Array.isArray(msg.branches)) {
524
+ targetTab.branches = msg.branches;
525
+ targetTab.activeBranchId = msg.activeBranchId || 'main';
526
+ }
527
+
528
+ if (isActiveTarget) {
529
+ // Re-render the branches panel whenever the active tab gets a status.
530
+ renderBranchPanel(msg.branches || [], msg.activeBranchId || 'main', msg.sessionId || '');
531
+ }
532
+
522
533
  if (isActiveTarget) {
523
534
  // Active tab: full UI reflection
524
535
  btnThink.classList.toggle('btn-active-toggle', msg.thinkingMode);
@@ -1179,6 +1190,137 @@ function renderReplayStep(m, idx) {
1179
1190
  </div>`;
1180
1191
  }
1181
1192
 
1193
+ // ── B2: Branch sidebar panel ─────────────────────────────
1194
+ let _cachedBranches = [];
1195
+ let _cachedActiveBranchId = 'main';
1196
+
1197
+ function renderBranchPanel(branches, activeId, sessionId) {
1198
+ _cachedBranches = branches;
1199
+ _cachedActiveBranchId = activeId;
1200
+ const listEl = document.getElementById('branch-list');
1201
+ const headerEl = document.getElementById('branches-header');
1202
+ if (!listEl) return;
1203
+
1204
+ if (!sessionId) {
1205
+ if (headerEl) headerEl.textContent = 'No session';
1206
+ listEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">Load a session to see branches</div>';
1207
+ return;
1208
+ }
1209
+ if (headerEl) {
1210
+ headerEl.textContent = `Session ${sessionId.slice(0, 8)} · ${branches.length} branch${branches.length === 1 ? '' : 'es'}`;
1211
+ }
1212
+ if (branches.length === 0) {
1213
+ listEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">No branches</div>';
1214
+ return;
1215
+ }
1216
+
1217
+ // Build tree: compute depth by walking parent chain.
1218
+ const byId = Object.fromEntries(branches.map(b => [b.id, b]));
1219
+ function depthOf(b) {
1220
+ let d = 0, cur = b;
1221
+ while (cur && cur.parentBranchId && byId[cur.parentBranchId]) {
1222
+ d++;
1223
+ cur = byId[cur.parentBranchId];
1224
+ if (d > 100) break; // safety
1225
+ }
1226
+ return d;
1227
+ }
1228
+ // Depth-first order: roots first, then children in order.
1229
+ const ordered = [];
1230
+ const visited = new Set();
1231
+ function visit(b) {
1232
+ if (visited.has(b.id)) return;
1233
+ visited.add(b.id);
1234
+ ordered.push(b);
1235
+ for (const c of branches) {
1236
+ if (c.parentBranchId === b.id) visit(c);
1237
+ }
1238
+ }
1239
+ for (const b of branches) {
1240
+ if (!b.parentBranchId || !byId[b.parentBranchId]) visit(b);
1241
+ }
1242
+ // Any remaining (orphaned) — append at end.
1243
+ for (const b of branches) if (!visited.has(b.id)) visit(b);
1244
+
1245
+ listEl.innerHTML = ordered.map(b => {
1246
+ const depth = depthOf(b);
1247
+ const indent = depth === 0 ? '' : '│ '.repeat(depth - 1) + '└─ ';
1248
+ const isActive = b.id === activeId;
1249
+ const marker = isActive ? '●' : '○';
1250
+ const parentTag = b.parentBranchId
1251
+ ? `<span class="branch-count">← ${escapeHtml(b.parentBranchId)}@${b.parentMessageIndex}</span>`
1252
+ : '';
1253
+ return `
1254
+ <div class="branch-item${isActive ? ' active' : ''}" data-branch-id="${escapeHtml(b.id)}" data-branch-active="${isActive ? '1' : '0'}">
1255
+ <span class="branch-indent">${indent}</span>
1256
+ <span class="branch-marker">${marker}</span>
1257
+ <span class="branch-title" title="${escapeHtml(b.title)}">${escapeHtml(b.title)}</span>
1258
+ <span class="branch-id">${escapeHtml(b.id)}</span>
1259
+ <span class="branch-count">${b.messageCount}m</span>
1260
+ ${parentTag}
1261
+ <span class="branch-actions">
1262
+ <button data-branch-action="rename" title="Rename">✎</button>
1263
+ <button data-branch-action="delete" title="Delete">×</button>
1264
+ </span>
1265
+ </div>`;
1266
+ }).join('');
1267
+
1268
+ // Wire click handlers.
1269
+ listEl.querySelectorAll('.branch-item').forEach(el => {
1270
+ el.addEventListener('click', (e) => {
1271
+ const actionBtn = e.target.closest('button[data-branch-action]');
1272
+ const id = el.dataset.branchId;
1273
+ if (!id) return;
1274
+ if (actionBtn) {
1275
+ e.stopPropagation();
1276
+ const action = actionBtn.dataset.branchAction;
1277
+ if (action === 'rename') {
1278
+ const cur = byId[id];
1279
+ const title = prompt('New branch title:', cur?.title || '');
1280
+ if (title && title.trim() && title.trim() !== cur?.title) {
1281
+ send({ type: 'command', name: 'branch', args: ['rename', id, title.trim()] });
1282
+ }
1283
+ } else if (action === 'delete') {
1284
+ if (id === activeId) {
1285
+ alert('Cannot delete the active branch. Switch to another branch first.');
1286
+ return;
1287
+ }
1288
+ if (confirm(`Delete branch "${id}"? Its messages will be lost.`)) {
1289
+ send({ type: 'command', name: 'branch', args: ['delete', id] });
1290
+ }
1291
+ }
1292
+ return;
1293
+ }
1294
+ // Plain click → switch branch.
1295
+ if (el.dataset.branchActive !== '1') {
1296
+ send({ type: 'command', name: 'branch', args: ['switch', id] });
1297
+ }
1298
+ });
1299
+ });
1300
+ }
1301
+
1302
+ // "+ Fork" button — fork the active branch at its current tip.
1303
+ document.getElementById('btn-branch-new')?.addEventListener('click', () => {
1304
+ const activeTab = sessionTabs[activeTabIdx];
1305
+ if (!activeTab || !activeTab.sessionId) {
1306
+ alert('Load a session first.');
1307
+ return;
1308
+ }
1309
+ const activeBranch = _cachedBranches.find(b => b.id === _cachedActiveBranchId);
1310
+ const tip = activeBranch?.messageCount ?? 0;
1311
+ const raw = prompt(`Fork from message # (0–${tip}, default ${tip}):`, String(tip));
1312
+ if (raw === null) return;
1313
+ const idx = parseInt(raw.trim() || String(tip), 10);
1314
+ if (isNaN(idx) || idx < 0 || idx > tip) {
1315
+ alert(`Invalid index. Range: 0–${tip}`);
1316
+ return;
1317
+ }
1318
+ const title = prompt('New branch title (optional):', '');
1319
+ if (title === null) return;
1320
+ const args = ['new', String(idx), ...(title.trim() ? [title.trim()] : [])];
1321
+ send({ type: 'command', name: 'branch', args });
1322
+ });
1323
+
1182
1324
  function startSessionRename(itemEl, titleEl) {
1183
1325
  const sessionId = itemEl.dataset.sessionId;
1184
1326
  const currentTitle = titleEl.textContent.trim();
@@ -105,6 +105,7 @@
105
105
  <!-- Sidebar tabs -->
106
106
  <div class="flex border-b border-base-content/10 flex-shrink-0">
107
107
  <button class="sidebar-tab active flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="sessions">📋 Sessions</button>
108
+ <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="branches" title="Conversation branches (B2)">🌿 Branches</button>
108
109
  <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="tools">🔧 Tools</button>
109
110
  <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="files">📁 Files</button>
110
111
  </div>
@@ -127,6 +128,17 @@
127
128
  <div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
128
129
  </div>
129
130
  </div>
131
+ <!-- Branches tab (B2) -->
132
+ <div id="tab-branches" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden hidden">
133
+ <div class="p-2 border-b border-base-content/10 flex items-center gap-1">
134
+ <span class="text-xs opacity-60 flex-1 truncate" id="branches-header">No session</span>
135
+ <button id="btn-branch-new" class="btn btn-xs btn-primary btn-outline flex-shrink-0 whitespace-nowrap" title="Fork at current tip">+ Fork</button>
136
+ </div>
137
+ <div id="branch-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
138
+ <div class="text-xs opacity-40 text-center py-4">Load a session to see branches</div>
139
+ </div>
140
+ </div>
141
+
130
142
  <!-- Tools tab -->
131
143
  <div id="tab-tools" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden hidden">
132
144
  <div class="p-2 border-b border-base-content/10">
@@ -903,3 +903,74 @@ button, a, .session-item, .file-tree-row, .template-item, .tool-item, .mcp-serve
903
903
  max-height: 12rem;
904
904
  overflow-y: auto;
905
905
  }
906
+
907
+ /* ── B2 Branch picker (sidebar) ─────────────────────────── */
908
+ .branch-item {
909
+ display: flex;
910
+ align-items: center;
911
+ gap: 0.35rem;
912
+ padding: 0.35rem 0.5rem;
913
+ border-radius: 0.35rem;
914
+ cursor: pointer;
915
+ border: 1px solid transparent;
916
+ transition: background 0.1s, border-color 0.1s;
917
+ font-size: 0.78rem;
918
+ line-height: 1.25;
919
+ position: relative;
920
+ }
921
+ .branch-item:hover {
922
+ background: rgba(128, 128, 128, 0.12);
923
+ }
924
+ .branch-item.active {
925
+ background: rgba(34, 197, 94, 0.12);
926
+ border-color: rgba(34, 197, 94, 0.45);
927
+ }
928
+ .branch-item .branch-marker {
929
+ flex-shrink: 0;
930
+ width: 0.8rem;
931
+ color: rgb(34, 197, 94);
932
+ }
933
+ .branch-item .branch-title {
934
+ flex: 1;
935
+ min-width: 0;
936
+ overflow: hidden;
937
+ text-overflow: ellipsis;
938
+ white-space: nowrap;
939
+ }
940
+ .branch-item .branch-id {
941
+ flex-shrink: 0;
942
+ opacity: 0.5;
943
+ font-family: ui-monospace, SFMono-Regular, monospace;
944
+ font-size: 0.7rem;
945
+ }
946
+ .branch-item .branch-count {
947
+ flex-shrink: 0;
948
+ opacity: 0.55;
949
+ font-size: 0.7rem;
950
+ }
951
+ .branch-item .branch-actions {
952
+ display: none;
953
+ gap: 0.15rem;
954
+ flex-shrink: 0;
955
+ }
956
+ .branch-item:hover .branch-actions {
957
+ display: flex;
958
+ }
959
+ .branch-item .branch-actions button {
960
+ background: transparent;
961
+ border: none;
962
+ padding: 0 0.2rem;
963
+ font-size: 0.72rem;
964
+ cursor: pointer;
965
+ opacity: 0.7;
966
+ }
967
+ .branch-item .branch-actions button:hover {
968
+ opacity: 1;
969
+ }
970
+ .branch-item .branch-indent {
971
+ flex-shrink: 0;
972
+ color: rgba(128, 128, 128, 0.5);
973
+ font-family: ui-monospace, SFMono-Regular, monospace;
974
+ font-size: 0.72rem;
975
+ white-space: pre;
976
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.4.74",
3
+ "version": "0.4.76",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,9 @@
38
38
  },
39
39
  "pkg": {
40
40
  "scripts": "dist-cjs/index.cjs",
41
- "assets": [],
41
+ "assets": [
42
+ "dist/wasm/**/*"
43
+ ],
42
44
  "targets": [
43
45
  "node22-win-x64",
44
46
  "node22-macos-arm64",
@@ -61,6 +63,7 @@
61
63
  "dist/agent-client-*.js",
62
64
  "dist/task-orchestrator-*.js",
63
65
  "dist/web/",
66
+ "dist/wasm/",
64
67
  "README.md"
65
68
  ],
66
69
  "keywords": [
@@ -90,8 +93,12 @@
90
93
  "commander": "^13.0.0",
91
94
  "express": "^5.2.1",
92
95
  "openai": "^4.77.0",
96
+ "tree-sitter-javascript": "^0.25.0",
97
+ "tree-sitter-python": "^0.25.0",
98
+ "tree-sitter-typescript": "^0.23.2",
93
99
  "undici": "^7.22.0",
94
100
  "uuid": "^11.0.5",
101
+ "web-tree-sitter": "^0.26.8",
95
102
  "ws": "^8.19.0",
96
103
  "zod": "^3.24.1"
97
104
  },