jinzd-ai-cli 0.4.73 → 0.4.75
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/README.md +28 -3
- package/README.zh-CN.md +28 -3
- package/dist/{chunk-HAOCJWW2.js → chunk-3BHGEPIT.js} +1 -1
- package/dist/{chunk-T2OUKQOX.js → chunk-5P4QTZBI.js} +1 -1
- package/dist/{chunk-LR7IV4SK.js → chunk-ASNDBI5R.js} +1 -1
- package/dist/{chunk-TIGB5ADX.js → chunk-C4MGON2N.js} +197 -16
- package/dist/{chunk-76KBSQHA.js → chunk-E6RP5DBU.js} +1 -1
- package/dist/{chunk-H65WPFDO.js → chunk-MPIUYP6Q.js} +2 -2
- package/dist/{hub-AFXKRJ5D.js → hub-W3BF22UV.js} +1 -1
- package/dist/index.js +135 -9
- package/dist/{run-tests-5BUD2CL4.js → run-tests-LEYTZHPU.js} +1 -1
- package/dist/{run-tests-6JUSVL4W.js → run-tests-V2JJADIU.js} +2 -2
- package/dist/{server-QQGBLS42.js → server-2XO72FRP.js} +117 -7
- package/dist/{task-orchestrator-M7Y32LNH.js → task-orchestrator-277NWVSE.js} +3 -3
- package/dist/web/client/app.js +174 -0
- package/dist/web/client/index.html +12 -0
- package/dist/web/client/style.css +71 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/jinzd-ai-cli)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
|
-
[]()
|
|
11
11
|
[](https://github.com/jinzhengdong/ai-cli/releases)
|
|
12
12
|
[](https://github.com/jinzhengdong/ai-cli/actions/workflows/ci.yml)
|
|
13
13
|
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
- **8 Built-in Providers** — Claude, Gemini, DeepSeek, OpenAI, Zhipu GLM, Kimi, OpenRouter (300+ models), **Ollama** (local models, no API key needed)
|
|
25
25
|
- **3 Interfaces** — Terminal CLI, browser Web UI (`aicli web`), Electron desktop app
|
|
26
26
|
- **Agentic Tool Calling** — AI autonomously runs shell commands, reads/writes files, searches code, fetches web, runs tests (default 200 rounds, configurable up to 10000 via `config.maxToolRounds` or `--max-tool-rounds`)
|
|
27
|
+
- **Prompt Caching** *(v0.4.70+)* — System prompt split into stable/volatile halves so Claude caches the stable part with `cache_control: ephemeral`; cached tokens bill at ~10% of the input price
|
|
28
|
+
- **Unified-Diff Patch Edits** *(v0.4.72+)* — `edit_file` accepts standard `@@ -a,b +c,d @@` hunks for the most compact way to apply many scattered small changes to a large file (±200-line drift tolerance + whitespace fallback)
|
|
29
|
+
- **Anthropic Batches API** *(v0.4.73+)* — `aicli batch submit/list/status/results/cancel` for 50%-off, 24-hour async processing — ideal for offline analysis and bulk evals
|
|
30
|
+
- **Web UI Session Replay** *(v0.4.71+)* — 🎬 button on every saved session opens a timeline replay: every message, tool call, reasoning, and cache-aware token usage at a glance
|
|
31
|
+
- **Conversation Branching** *(v0.4.74+)* — `/branch list/new/switch/delete/rename` inside the REPL, plus a 🌿 "fork here" button on every replay step — explore alternate directions without losing the original thread
|
|
27
32
|
- **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
|
|
28
33
|
- **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
|
|
29
34
|
- **Extended Thinking** — Claude deep reasoning mode with `/think` toggle
|
|
@@ -34,7 +39,7 @@
|
|
|
34
39
|
- **PWA Support** — Install Web UI as a desktop/mobile app, accessible over LAN
|
|
35
40
|
- **Hierarchical Context** — 3-layer context files (global / project / subdirectory) auto-injected
|
|
36
41
|
- **Headless Mode** — `ai-cli -p "prompt"` for CI/CD pipelines and scripting
|
|
37
|
-
- **40 REPL Commands** — Session management, checkpointing, code review, security review, rewind, scaffolding, and more
|
|
42
|
+
- **40 REPL Commands** — Session management, checkpointing, code review, security review, rewind, scaffolding, cross-session history search, and more
|
|
38
43
|
- **GitHub Actions CI/CD** — Automated testing on Node 20/22 + npm publish on release tags
|
|
39
44
|
- **Cross-Platform** — Windows, macOS, Linux
|
|
40
45
|
|
|
@@ -188,7 +193,8 @@ AI autonomously invokes these 24 tools during conversations:
|
|
|
188
193
|
| `/compact` | Compress conversation history |
|
|
189
194
|
| `/session` | Session management (new / list / load) |
|
|
190
195
|
| `/checkpoint` | Save/restore conversation checkpoints |
|
|
191
|
-
| `/fork` |
|
|
196
|
+
| `/fork` | Fork the current session into a new session file |
|
|
197
|
+
| `/branch` | Create/switch/delete branches *within* the current session (B2) |
|
|
192
198
|
| `/search <keyword>` | Full-text search across all sessions |
|
|
193
199
|
| `/skill` | Manage agent skill packs |
|
|
194
200
|
| `/mcp` | View MCP server status and tools |
|
|
@@ -227,8 +233,27 @@ Subcommands:
|
|
|
227
233
|
aicli providers List all providers and status
|
|
228
234
|
aicli sessions List recent sessions
|
|
229
235
|
aicli user <action> Manage Web UI users
|
|
236
|
+
aicli batch <action> Anthropic Batches API (submit | list | status | results | cancel)
|
|
230
237
|
```
|
|
231
238
|
|
|
239
|
+
### Batch Mode (Anthropic Message Batches)
|
|
240
|
+
|
|
241
|
+
For offline analysis, bulk evals, or any workload where latency is flexible, use the Batches API for **50% off** tokens with a 24-hour processing window.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# 1. Prepare a JSONL file (one request per line):
|
|
245
|
+
# {"customId":"req-1","messages":[{"role":"user","content":"..."}],"maxTokens":1024}
|
|
246
|
+
aicli batch submit prompts.jsonl # validate + submit + track locally
|
|
247
|
+
aicli batch submit --dry-run prompts.jsonl # parse only, no network
|
|
248
|
+
|
|
249
|
+
aicli batch list # live status of recent batches
|
|
250
|
+
aicli batch status <id> # detailed status + request counts
|
|
251
|
+
aicli batch results <id> out.jsonl # download results (stdout if no path)
|
|
252
|
+
aicli batch cancel <id> # cancel an in-progress batch
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Local tracking file: `~/.aicli/batches.json` (last 200 submissions). Requires `AICLI_API_KEY_CLAUDE` or a Claude API key configured via `aicli config`.
|
|
256
|
+
|
|
232
257
|
### Headless Mode
|
|
233
258
|
|
|
234
259
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/jinzd-ai-cli)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
|
-
[]()
|
|
11
11
|
[](https://github.com/jinzhengdong/ai-cli/releases)
|
|
12
12
|
[](https://github.com/jinzhengdong/ai-cli/actions/workflows/ci.yml)
|
|
13
13
|
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
- **8 大内置 Provider** — Claude、Gemini、DeepSeek、OpenAI、智谱 GLM、Kimi、OpenRouter(300+ 模型)、**Ollama**(本地模型,无需 API Key)
|
|
17
17
|
- **三种使用方式** — 终端 CLI、浏览器 Web UI(`aicli web`)、Electron 桌面应用
|
|
18
18
|
- **Agentic 工具调用** — AI 自主执行 bash 命令、读写文件、搜索代码、抓取网页、运行测试(默认 200 轮,可通过 `config.maxToolRounds` 或 `--max-tool-rounds` 调整,上限 10000)
|
|
19
|
+
- **Prompt Caching**(v0.4.70+)— system prompt 拆分稳定/易变两段,Claude 对稳定段启用 `cache_control: ephemeral`,命中时按 10% 计费
|
|
20
|
+
- **Unified Diff Patch 编辑**(v0.4.72+)— `edit_file` 支持标准 `@@ -a,b +c,d @@` hunk,大文件多处小改最省 token;容忍 ±200 行漂移与空白差异
|
|
21
|
+
- **Anthropic Batches API**(v0.4.73+)— `aicli batch submit/list/status/results/cancel` 包 Message Batches(50% 折扣 + 24 小时窗口),适合离线分析和批量 eval
|
|
22
|
+
- **Web UI 会话回放**(v0.4.71+)— 会话列表每项 🎬 按钮打开时间轴回放:消息、工具调用、推理内容、cache-aware token 用量一目了然
|
|
23
|
+
- **对话分支**(v0.4.74+)— REPL 内 `/branch list/new/switch/delete/rename`,Web UI 回放面板每条消息旁 🌿 "fork here" 按钮,任意位置开辟新分支探索不同方向,原对话保持不变
|
|
19
24
|
- **流式工具调用** — 实时流式展示 AI 推理过程和工具调用
|
|
20
25
|
- **子代理系统** — 将复杂子任务委派给独立子代理执行
|
|
21
26
|
- **深度推理** — Claude Extended Thinking,`/think` 一键切换
|
|
@@ -180,7 +185,8 @@ AI 在对话中可自主调用 24 个工具:
|
|
|
180
185
|
| `/compact` | 压缩对话历史 |
|
|
181
186
|
| `/session` | 会话管理(new / list / load) |
|
|
182
187
|
| `/checkpoint` | 保存/恢复会话检查点 |
|
|
183
|
-
| `/fork` |
|
|
188
|
+
| `/fork` | 复制整个会话为新的 session 文件 |
|
|
189
|
+
| `/branch` | 在当前 session **内部**创建/切换/删除分支(B2)|
|
|
184
190
|
| `/search <关键词>` | 跨会话全文搜索 |
|
|
185
191
|
| `/skill` | 管理 Agent 技能包 |
|
|
186
192
|
| `/mcp` | 查看 MCP 服务器状态 |
|
|
@@ -219,8 +225,27 @@ aicli [选项]
|
|
|
219
225
|
aicli providers 列出所有 Provider
|
|
220
226
|
aicli sessions 列出最近会话
|
|
221
227
|
aicli user <操作> 管理 Web UI 用户
|
|
228
|
+
aicli batch <操作> Anthropic Batches API(submit | list | status | results | cancel)
|
|
222
229
|
```
|
|
223
230
|
|
|
231
|
+
### 批处理模式(Anthropic Message Batches)
|
|
232
|
+
|
|
233
|
+
离线分析、批量 eval 等对延迟不敏感的场景,使用 Batches API 可享 **50% 折扣** + 24 小时处理窗口。
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# 1. 准备 JSONL 输入(每行一个请求):
|
|
237
|
+
# {"customId":"req-1","messages":[{"role":"user","content":"..."}],"maxTokens":1024}
|
|
238
|
+
aicli batch submit prompts.jsonl # 校验 + 提交 + 本地追踪
|
|
239
|
+
aicli batch submit --dry-run prompts.jsonl # 只解析不提交
|
|
240
|
+
|
|
241
|
+
aicli batch list # 实时查看已追踪批次状态
|
|
242
|
+
aicli batch status <id> # 单批详情 + 请求计数分解
|
|
243
|
+
aicli batch results <id> out.jsonl # 下载结果(省略文件名则输出到 stdout)
|
|
244
|
+
aicli batch cancel <id> # 取消进行中的批次
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
本地追踪文件:`~/.aicli/batches.json`(保留最近 200 次提交)。需配置 `AICLI_API_KEY_CLAUDE` 或通过 `aicli config` 设置 Claude API Key。
|
|
248
|
+
|
|
224
249
|
### 无头模式
|
|
225
250
|
|
|
226
251
|
```bash
|
|
@@ -391,7 +416,7 @@ Web UI(`aicli web`)提供功能完备的浏览器界面:
|
|
|
391
416
|
## 测试
|
|
392
417
|
|
|
393
418
|
```bash
|
|
394
|
-
npm test # 运行全部
|
|
419
|
+
npm test # 运行全部 515 个测试(30 suites)
|
|
395
420
|
npm run test:watch # 监听模式
|
|
396
421
|
```
|
|
397
422
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
schemaToJsonSchema,
|
|
4
4
|
truncateForPersist
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-MPIUYP6Q.js";
|
|
6
6
|
import {
|
|
7
7
|
AuthError,
|
|
8
8
|
ProviderError,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
MCP_PROTOCOL_VERSION,
|
|
19
19
|
MCP_TOOL_PREFIX,
|
|
20
20
|
VERSION
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-5P4QTZBI.js";
|
|
22
22
|
|
|
23
23
|
// src/providers/claude.ts
|
|
24
24
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -2132,6 +2132,9 @@ function getContentText(content) {
|
|
|
2132
2132
|
}
|
|
2133
2133
|
|
|
2134
2134
|
// src/session/session.ts
|
|
2135
|
+
function makeBranchId() {
|
|
2136
|
+
return Math.random().toString(16).slice(2, 8);
|
|
2137
|
+
}
|
|
2135
2138
|
var Session = class _Session {
|
|
2136
2139
|
id;
|
|
2137
2140
|
provider;
|
|
@@ -2147,12 +2150,32 @@ var Session = class _Session {
|
|
|
2147
2150
|
cacheReadTokens: 0
|
|
2148
2151
|
};
|
|
2149
2152
|
checkpoints = [];
|
|
2153
|
+
// ── B2 Branches (v0.4.74+) ──────────────────────────────────────
|
|
2154
|
+
/**
|
|
2155
|
+
* All branches in this session. The 'main' branch is auto-created and
|
|
2156
|
+
* represents the linear conversation for pre-B2 sessions.
|
|
2157
|
+
*/
|
|
2158
|
+
branches = [];
|
|
2159
|
+
/** Currently active branch — its messages live in `this.messages`. */
|
|
2160
|
+
activeBranchId = "main";
|
|
2161
|
+
/**
|
|
2162
|
+
* Stashed message arrays for INACTIVE branches. The active branch's
|
|
2163
|
+
* messages are always in `this.messages`, never duplicated here.
|
|
2164
|
+
*/
|
|
2165
|
+
_inactiveBranchMessages = /* @__PURE__ */ new Map();
|
|
2150
2166
|
constructor(id, provider, model) {
|
|
2151
2167
|
this.id = id;
|
|
2152
2168
|
this.provider = provider;
|
|
2153
2169
|
this.model = model;
|
|
2154
2170
|
this.created = /* @__PURE__ */ new Date();
|
|
2155
2171
|
this.updated = /* @__PURE__ */ new Date();
|
|
2172
|
+
this.branches.push({
|
|
2173
|
+
id: "main",
|
|
2174
|
+
title: "main",
|
|
2175
|
+
parentBranchId: null,
|
|
2176
|
+
parentMessageIndex: 0,
|
|
2177
|
+
created: this.created
|
|
2178
|
+
});
|
|
2156
2179
|
}
|
|
2157
2180
|
/**
|
|
2158
2181
|
* 更新 session 关联的 provider 和 model(在 /provider 或 /model 切换时调用)。
|
|
@@ -2239,6 +2262,120 @@ var Session = class _Session {
|
|
|
2239
2262
|
this.checkpoints = this.checkpoints.filter((c) => c.name !== name);
|
|
2240
2263
|
return this.checkpoints.length < len;
|
|
2241
2264
|
}
|
|
2265
|
+
// ── B2 Branch operations ────────────────────────────────────────
|
|
2266
|
+
/** Deep-clone a messages array (matches `Session.fork` semantics). */
|
|
2267
|
+
static cloneMessages(msgs) {
|
|
2268
|
+
return msgs.map((m) => {
|
|
2269
|
+
const cloned = { ...m };
|
|
2270
|
+
if (Array.isArray(cloned.content)) {
|
|
2271
|
+
cloned.content = cloned.content.map(
|
|
2272
|
+
(part) => typeof part === "object" && part !== null ? { ...part } : part
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
if (cloned.toolCalls) {
|
|
2276
|
+
cloned.toolCalls = cloned.toolCalls.map((tc) => ({ ...tc }));
|
|
2277
|
+
}
|
|
2278
|
+
return cloned;
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
/** List all branches (metadata only). */
|
|
2282
|
+
listBranches() {
|
|
2283
|
+
return this.branches.map((b) => ({ ...b }));
|
|
2284
|
+
}
|
|
2285
|
+
/** Current active branch metadata. */
|
|
2286
|
+
getActiveBranch() {
|
|
2287
|
+
const b = this.branches.find((b2) => b2.id === this.activeBranchId);
|
|
2288
|
+
if (!b) {
|
|
2289
|
+
this.activeBranchId = this.branches[0]?.id ?? "main";
|
|
2290
|
+
return this.branches[0] ?? { id: "main", title: "main", parentBranchId: null, parentMessageIndex: 0, created: /* @__PURE__ */ new Date() };
|
|
2291
|
+
}
|
|
2292
|
+
return b;
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2295
|
+
* Create a new branch by forking the active branch at message index
|
|
2296
|
+
* `fromIndex`. Copies `messages[0..fromIndex]` into the new branch
|
|
2297
|
+
* and switches to it. The original active branch is preserved intact
|
|
2298
|
+
* in the stash.
|
|
2299
|
+
*
|
|
2300
|
+
* @returns new branch id
|
|
2301
|
+
* @throws if fromIndex is out of range
|
|
2302
|
+
*/
|
|
2303
|
+
createBranch(fromIndex, title) {
|
|
2304
|
+
if (fromIndex < 0 || fromIndex > this.messages.length) {
|
|
2305
|
+
throw new Error(
|
|
2306
|
+
`createBranch: fromIndex ${fromIndex} out of range [0, ${this.messages.length}]`
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2309
|
+
this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
|
|
2310
|
+
const id = makeBranchId();
|
|
2311
|
+
const meta = {
|
|
2312
|
+
id,
|
|
2313
|
+
title: title || `branch-${this.branches.length + 1}`,
|
|
2314
|
+
parentBranchId: this.activeBranchId,
|
|
2315
|
+
parentMessageIndex: fromIndex,
|
|
2316
|
+
created: /* @__PURE__ */ new Date()
|
|
2317
|
+
};
|
|
2318
|
+
this.branches.push(meta);
|
|
2319
|
+
this.messages = _Session.cloneMessages(this.messages.slice(0, fromIndex));
|
|
2320
|
+
this.activeBranchId = id;
|
|
2321
|
+
this.updated = /* @__PURE__ */ new Date();
|
|
2322
|
+
return id;
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2325
|
+
* Switch the active branch. Stashes current messages under the old
|
|
2326
|
+
* active id and loads the target branch's messages into `this.messages`.
|
|
2327
|
+
*
|
|
2328
|
+
* @returns true if switched, false if id not found or already active
|
|
2329
|
+
*/
|
|
2330
|
+
switchBranch(id) {
|
|
2331
|
+
if (id === this.activeBranchId) return false;
|
|
2332
|
+
if (!this.branches.some((b) => b.id === id)) return false;
|
|
2333
|
+
this._inactiveBranchMessages.set(this.activeBranchId, this.messages);
|
|
2334
|
+
const target = this._inactiveBranchMessages.get(id) ?? [];
|
|
2335
|
+
this._inactiveBranchMessages.delete(id);
|
|
2336
|
+
this.messages = target;
|
|
2337
|
+
this.activeBranchId = id;
|
|
2338
|
+
this.updated = /* @__PURE__ */ new Date();
|
|
2339
|
+
return true;
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Delete a branch by id. Cannot delete the active branch or the last
|
|
2343
|
+
* remaining branch. If other branches list this one as parent, their
|
|
2344
|
+
* parent pointer is retargeted to this branch's parent (transparent
|
|
2345
|
+
* to callers — branches still form a valid forest).
|
|
2346
|
+
*
|
|
2347
|
+
* @returns true if deleted
|
|
2348
|
+
*/
|
|
2349
|
+
deleteBranch(id) {
|
|
2350
|
+
if (id === this.activeBranchId) return false;
|
|
2351
|
+
if (this.branches.length <= 1) return false;
|
|
2352
|
+
const idx = this.branches.findIndex((b) => b.id === id);
|
|
2353
|
+
if (idx === -1) return false;
|
|
2354
|
+
const deleted = this.branches[idx];
|
|
2355
|
+
for (const b of this.branches) {
|
|
2356
|
+
if (b.parentBranchId === id) {
|
|
2357
|
+
b.parentBranchId = deleted.parentBranchId;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
this.branches.splice(idx, 1);
|
|
2361
|
+
this._inactiveBranchMessages.delete(id);
|
|
2362
|
+
this.updated = /* @__PURE__ */ new Date();
|
|
2363
|
+
return true;
|
|
2364
|
+
}
|
|
2365
|
+
/** Rename a branch (affects only display title). */
|
|
2366
|
+
renameBranch(id, newTitle) {
|
|
2367
|
+
const b = this.branches.find((b2) => b2.id === id);
|
|
2368
|
+
if (!b) return false;
|
|
2369
|
+
b.title = newTitle;
|
|
2370
|
+
this.updated = /* @__PURE__ */ new Date();
|
|
2371
|
+
return true;
|
|
2372
|
+
}
|
|
2373
|
+
/** Messages of any branch (active or inactive) — read-only copy. */
|
|
2374
|
+
getBranchMessages(id) {
|
|
2375
|
+
if (id === this.activeBranchId) return this.messages.slice();
|
|
2376
|
+
const m = this._inactiveBranchMessages.get(id);
|
|
2377
|
+
return m ? m.slice() : null;
|
|
2378
|
+
}
|
|
2242
2379
|
getMeta() {
|
|
2243
2380
|
return {
|
|
2244
2381
|
id: this.id,
|
|
@@ -2251,6 +2388,23 @@ var Session = class _Session {
|
|
|
2251
2388
|
};
|
|
2252
2389
|
}
|
|
2253
2390
|
toJSON() {
|
|
2391
|
+
const serializeMessages = (msgs) => msgs.map((m) => {
|
|
2392
|
+
const out = {
|
|
2393
|
+
role: m.role,
|
|
2394
|
+
content: m.content,
|
|
2395
|
+
timestamp: m.timestamp.toISOString()
|
|
2396
|
+
};
|
|
2397
|
+
if (m.toolCalls) out.toolCalls = m.toolCalls;
|
|
2398
|
+
if (m.reasoningContent !== void 0) out.reasoningContent = m.reasoningContent;
|
|
2399
|
+
if (m.toolCallId) out.toolCallId = m.toolCallId;
|
|
2400
|
+
if (m.toolName) out.toolName = m.toolName;
|
|
2401
|
+
if (m.isError !== void 0) out.isError = m.isError;
|
|
2402
|
+
return out;
|
|
2403
|
+
});
|
|
2404
|
+
const branchMessages = {};
|
|
2405
|
+
for (const [id, msgs] of this._inactiveBranchMessages.entries()) {
|
|
2406
|
+
branchMessages[id] = serializeMessages(msgs);
|
|
2407
|
+
}
|
|
2254
2408
|
return {
|
|
2255
2409
|
id: this.id,
|
|
2256
2410
|
provider: this.provider,
|
|
@@ -2264,19 +2418,21 @@ var Session = class _Session {
|
|
|
2264
2418
|
messageIndex: c.messageIndex,
|
|
2265
2419
|
timestamp: c.timestamp.toISOString()
|
|
2266
2420
|
})),
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2421
|
+
// B2 Branches (v0.4.74+). Omitted for sessions with only the default
|
|
2422
|
+
// 'main' branch and no stashed messages (keeps file size identical
|
|
2423
|
+
// to pre-B2 for the common case).
|
|
2424
|
+
...this.branches.length > 1 || this._inactiveBranchMessages.size > 0 ? {
|
|
2425
|
+
activeBranchId: this.activeBranchId,
|
|
2426
|
+
branches: this.branches.map((b) => ({
|
|
2427
|
+
id: b.id,
|
|
2428
|
+
title: b.title,
|
|
2429
|
+
parentBranchId: b.parentBranchId,
|
|
2430
|
+
parentMessageIndex: b.parentMessageIndex,
|
|
2431
|
+
created: b.created.toISOString()
|
|
2432
|
+
})),
|
|
2433
|
+
branchMessages
|
|
2434
|
+
} : {},
|
|
2435
|
+
messages: serializeMessages(this.messages)
|
|
2280
2436
|
};
|
|
2281
2437
|
}
|
|
2282
2438
|
/**
|
|
@@ -2344,7 +2500,7 @@ var Session = class _Session {
|
|
|
2344
2500
|
timestamp: new Date(c.timestamp)
|
|
2345
2501
|
}));
|
|
2346
2502
|
}
|
|
2347
|
-
|
|
2503
|
+
const deserializeMessages = (arr) => arr.map((m) => {
|
|
2348
2504
|
const ts = new Date(m.timestamp);
|
|
2349
2505
|
const msg = {
|
|
2350
2506
|
role: m.role ?? "user",
|
|
@@ -2358,6 +2514,31 @@ var Session = class _Session {
|
|
|
2358
2514
|
if (typeof m.isError === "boolean") msg.isError = m.isError;
|
|
2359
2515
|
return msg;
|
|
2360
2516
|
});
|
|
2517
|
+
session.messages = deserializeMessages(d.messages);
|
|
2518
|
+
if (Array.isArray(d.branches) && d.branches.length > 0) {
|
|
2519
|
+
session.branches = d.branches.map((b) => {
|
|
2520
|
+
const ts = new Date(b.created);
|
|
2521
|
+
return {
|
|
2522
|
+
id: String(b.id ?? "main"),
|
|
2523
|
+
title: String(b.title ?? b.id ?? "main"),
|
|
2524
|
+
parentBranchId: typeof b.parentBranchId === "string" ? b.parentBranchId : null,
|
|
2525
|
+
parentMessageIndex: typeof b.parentMessageIndex === "number" ? b.parentMessageIndex : 0,
|
|
2526
|
+
created: isNaN(ts.getTime()) ? /* @__PURE__ */ new Date() : ts
|
|
2527
|
+
};
|
|
2528
|
+
});
|
|
2529
|
+
session.activeBranchId = typeof d.activeBranchId === "string" ? d.activeBranchId : session.branches[0]?.id ?? "main";
|
|
2530
|
+
const bm = d.branchMessages;
|
|
2531
|
+
if (bm && typeof bm === "object") {
|
|
2532
|
+
for (const [id, arr] of Object.entries(bm)) {
|
|
2533
|
+
if (id === session.activeBranchId) continue;
|
|
2534
|
+
if (!Array.isArray(arr)) continue;
|
|
2535
|
+
session._inactiveBranchMessages.set(
|
|
2536
|
+
id,
|
|
2537
|
+
deserializeMessages(arr)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2361
2542
|
return session;
|
|
2362
2543
|
}
|
|
2363
2544
|
};
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-4BKXL7SM.js";
|
|
5
5
|
import {
|
|
6
6
|
runTestsTool
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3BHGEPIT.js";
|
|
8
8
|
import {
|
|
9
9
|
EnvLoader,
|
|
10
10
|
NetworkError,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
SUBAGENT_ALLOWED_TOOLS,
|
|
18
18
|
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
19
19
|
SUBAGENT_MAX_ROUNDS_LIMIT
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-5P4QTZBI.js";
|
|
21
21
|
|
|
22
22
|
// src/tools/builtin/bash.ts
|
|
23
23
|
import { execSync } from "child_process";
|
|
@@ -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-
|
|
388
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-277NWVSE.js");
|
|
389
389
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
390
390
|
let interrupted = false;
|
|
391
391
|
const onSigint = () => {
|
package/dist/index.js
CHANGED
|
@@ -30,10 +30,10 @@ import {
|
|
|
30
30
|
saveDevState,
|
|
31
31
|
sessionHasMeaningfulContent,
|
|
32
32
|
setupProxy
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-C4MGON2N.js";
|
|
34
34
|
import {
|
|
35
35
|
ConfigManager
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-ASNDBI5R.js";
|
|
37
37
|
import {
|
|
38
38
|
ToolExecutor,
|
|
39
39
|
ToolRegistry,
|
|
@@ -49,11 +49,11 @@ import {
|
|
|
49
49
|
spawnAgentContext,
|
|
50
50
|
theme,
|
|
51
51
|
undoStack
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-MPIUYP6Q.js";
|
|
53
53
|
import {
|
|
54
54
|
fileCheckpoints
|
|
55
55
|
} from "./chunk-4BKXL7SM.js";
|
|
56
|
-
import "./chunk-
|
|
56
|
+
import "./chunk-3BHGEPIT.js";
|
|
57
57
|
import "./chunk-2ZD3YTVM.js";
|
|
58
58
|
import {
|
|
59
59
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -76,7 +76,7 @@ import {
|
|
|
76
76
|
SKILLS_DIR_NAME,
|
|
77
77
|
VERSION,
|
|
78
78
|
buildUserIdentityPrompt
|
|
79
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-5P4QTZBI.js";
|
|
80
80
|
|
|
81
81
|
// src/index.ts
|
|
82
82
|
import { program } from "commander";
|
|
@@ -316,6 +316,11 @@ var Renderer = class {
|
|
|
316
316
|
console.log(feat("Session size control: auto-trim old tool output when session exceeds 2MB, keep recent rounds intact"));
|
|
317
317
|
console.log(feat("Crash recovery: detect incomplete agentic loops on /resume, warn and offer continuation"));
|
|
318
318
|
console.log(feat("Cost dashboard: /cost history shows cross-session daily/weekly/monthly spend with budget progress bar"));
|
|
319
|
+
console.log(feat("Prompt caching (A1, v0.4.70+): system prompt split into stable/volatile \u2014 Claude caches stable half, 10% cost on hits"));
|
|
320
|
+
console.log(feat("edit_file patch mode (A2, v0.4.72+): accepts unified diff (@@ hunks) \u2014 most compact for scattered small changes in large files"));
|
|
321
|
+
console.log(feat("Anthropic Batches API (A3, v0.4.73+): aicli batch submit/list/status/results/cancel \u2014 50% off, 24h window"));
|
|
322
|
+
console.log(feat("Session Replay (B1, v0.4.71+): Web UI \u{1F3AC} button \u2014 timeline view of every message, tool call, reasoning, and cache-aware token usage"));
|
|
323
|
+
console.log(feat("Conversation Branching (B2, v0.4.74+): /branch list/new/switch/delete/rename \u2014 fork the conversation at any message; Web UI replay \u{1F33F} fork-here button"));
|
|
319
324
|
console.log();
|
|
320
325
|
}
|
|
321
326
|
printPrompt(provider, _model) {
|
|
@@ -2235,6 +2240,122 @@ ${hint}` : "")
|
|
|
2235
2240
|
console.log();
|
|
2236
2241
|
}
|
|
2237
2242
|
},
|
|
2243
|
+
// ── /branch ───────────────────────────────────────────────────
|
|
2244
|
+
{
|
|
2245
|
+
name: "branch",
|
|
2246
|
+
description: "Manage conversation branches (fork/switch/list/delete/rename)",
|
|
2247
|
+
usage: "/branch [list | new <msgIndex> [title] | switch <id> | delete <id> | rename <id> <title>]",
|
|
2248
|
+
async execute(args, ctx) {
|
|
2249
|
+
const session = ctx.sessions.current;
|
|
2250
|
+
if (!session) {
|
|
2251
|
+
ctx.renderer.renderError("No active session.");
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const sub = args[0]?.toLowerCase();
|
|
2255
|
+
if (!sub || sub === "list") {
|
|
2256
|
+
const branches = session.listBranches();
|
|
2257
|
+
console.log(theme.heading(`
|
|
2258
|
+
Branches (${branches.length}):
|
|
2259
|
+
`));
|
|
2260
|
+
for (const b of branches) {
|
|
2261
|
+
const active = b.id === session.activeBranchId ? theme.success("\u25CF ") : " ";
|
|
2262
|
+
const parent = b.parentBranchId ? theme.dim(` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}`) : "";
|
|
2263
|
+
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
2264
|
+
console.log(
|
|
2265
|
+
` ${active}${theme.accent(b.id.padEnd(10))} ${b.title.padEnd(20)} ` + theme.dim(`(${count} msgs)`) + parent
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
console.log();
|
|
2269
|
+
console.log(theme.dim(" /branch new <msgIndex> [title] \u2014 fork active branch at message N"));
|
|
2270
|
+
console.log(theme.dim(" /branch switch <id> \u2014 switch active branch"));
|
|
2271
|
+
console.log(theme.dim(" /branch delete <id> \u2014 delete inactive branch"));
|
|
2272
|
+
console.log(theme.dim(" /branch rename <id> <title> \u2014 rename a branch"));
|
|
2273
|
+
console.log();
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (sub === "new") {
|
|
2277
|
+
const idxArg = args[1];
|
|
2278
|
+
if (!idxArg) {
|
|
2279
|
+
ctx.renderer.renderError("Usage: /branch new <msgIndex> [title]");
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const fromIndex = parseInt(idxArg, 10);
|
|
2283
|
+
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
2284
|
+
ctx.renderer.renderError(`Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}`);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const title = args.slice(2).join(" ").trim() || void 0;
|
|
2288
|
+
try {
|
|
2289
|
+
const newId = session.createBranch(fromIndex, title);
|
|
2290
|
+
await ctx.sessions.save();
|
|
2291
|
+
console.log(theme.success(`
|
|
2292
|
+
\u2713 Created branch "${newId}" from message #${fromIndex}`));
|
|
2293
|
+
console.log(theme.dim(` Now on branch "${newId}" (${session.messages.length} messages)
|
|
2294
|
+
`));
|
|
2295
|
+
} catch (err) {
|
|
2296
|
+
ctx.renderer.renderError(err.message);
|
|
2297
|
+
}
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (sub === "switch") {
|
|
2301
|
+
const id = args[1];
|
|
2302
|
+
if (!id) {
|
|
2303
|
+
ctx.renderer.renderError("Usage: /branch switch <id>");
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
const ok = session.switchBranch(id);
|
|
2307
|
+
if (ok) {
|
|
2308
|
+
await ctx.sessions.save();
|
|
2309
|
+
const b = session.getActiveBranch();
|
|
2310
|
+
console.log(theme.success(`
|
|
2311
|
+
\u2713 Switched to branch "${b.id}" \u2014 ${b.title}`));
|
|
2312
|
+
console.log(theme.dim(` ${session.messages.length} messages on this branch
|
|
2313
|
+
`));
|
|
2314
|
+
} else {
|
|
2315
|
+
ctx.renderer.renderError(`Cannot switch to "${id}" (not found or already active).`);
|
|
2316
|
+
}
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
if (sub === "delete") {
|
|
2320
|
+
const id = args[1];
|
|
2321
|
+
if (!id) {
|
|
2322
|
+
ctx.renderer.renderError("Usage: /branch delete <id>");
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const ok = session.deleteBranch(id);
|
|
2326
|
+
if (ok) {
|
|
2327
|
+
await ctx.sessions.save();
|
|
2328
|
+
console.log(theme.success(`
|
|
2329
|
+
\u2713 Deleted branch "${id}"
|
|
2330
|
+
`));
|
|
2331
|
+
} else {
|
|
2332
|
+
ctx.renderer.renderError(
|
|
2333
|
+
`Cannot delete "${id}" (not found, active, or last remaining branch).`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
if (sub === "rename") {
|
|
2339
|
+
const id = args[1];
|
|
2340
|
+
const title = args.slice(2).join(" ").trim();
|
|
2341
|
+
if (!id || !title) {
|
|
2342
|
+
ctx.renderer.renderError("Usage: /branch rename <id> <new title>");
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const ok = session.renameBranch(id, title);
|
|
2346
|
+
if (ok) {
|
|
2347
|
+
await ctx.sessions.save();
|
|
2348
|
+
console.log(theme.success(`
|
|
2349
|
+
\u2713 Renamed branch "${id}" \u2192 "${title}"
|
|
2350
|
+
`));
|
|
2351
|
+
} else {
|
|
2352
|
+
ctx.renderer.renderError(`Branch "${id}" not found.`);
|
|
2353
|
+
}
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
ctx.renderer.renderError(`Unknown subcommand: ${sub}. Use list/new/switch/delete/rename.`);
|
|
2357
|
+
}
|
|
2358
|
+
},
|
|
2238
2359
|
// ── /commands ─────────────────────────────────────────────────
|
|
2239
2360
|
{
|
|
2240
2361
|
name: "commands",
|
|
@@ -2271,7 +2392,7 @@ ${hint}` : "")
|
|
|
2271
2392
|
usage: "/test [command|filter]",
|
|
2272
2393
|
async execute(args, ctx) {
|
|
2273
2394
|
try {
|
|
2274
|
-
const { executeTests } = await import("./run-tests-
|
|
2395
|
+
const { executeTests } = await import("./run-tests-V2JJADIU.js");
|
|
2275
2396
|
const argStr = args.join(" ").trim();
|
|
2276
2397
|
let testArgs = {};
|
|
2277
2398
|
if (argStr) {
|
|
@@ -6143,7 +6264,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
6143
6264
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
6144
6265
|
process.exit(1);
|
|
6145
6266
|
}
|
|
6146
|
-
const { startWebServer } = await import("./server-
|
|
6267
|
+
const { startWebServer } = await import("./server-2XO72FRP.js");
|
|
6147
6268
|
await startWebServer({ port, host: options.host });
|
|
6148
6269
|
});
|
|
6149
6270
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -6266,7 +6387,7 @@ program.command("sessions").description("List recent conversation sessions").act
|
|
|
6266
6387
|
});
|
|
6267
6388
|
program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
|
|
6268
6389
|
try {
|
|
6269
|
-
const batch = await import("./batch-
|
|
6390
|
+
const batch = await import("./batch-2RTTAHBL.js");
|
|
6270
6391
|
switch (action) {
|
|
6271
6392
|
case "submit":
|
|
6272
6393
|
if (!arg) {
|
|
@@ -6353,6 +6474,7 @@ program.command("help").description("Show a comprehensive guide to all aicli fea
|
|
|
6353
6474
|
`${B}${C} \u25A0 WEB UI OPTIONS${R}`,
|
|
6354
6475
|
` ${Y}aicli web --port 8080${R} Custom port (default: 3000)`,
|
|
6355
6476
|
` ${Y}aicli web --host 0.0.0.0${R} LAN access (mobile/tablet)`,
|
|
6477
|
+
` ${D} Features: \u{1F3AC} Session Replay + \u{1F33F} Conversation Branching (fork from any message)${R}`,
|
|
6356
6478
|
"",
|
|
6357
6479
|
`${B}${C} \u25A0 MULTI-AGENT HUB${R}`,
|
|
6358
6480
|
` ${Y}--preset <name>${R} Role preset (tech-review/brainstorm/code-review/debate)`,
|
|
@@ -6375,6 +6497,7 @@ program.command("help").description("Show a comprehensive guide to all aicli fea
|
|
|
6375
6497
|
` ${M}/search${R} Search across all session histories`,
|
|
6376
6498
|
` ${M}/compact${R} Compress conversation to save context`,
|
|
6377
6499
|
` ${M}/checkpoint${R} ${M}/fork${R} Save/restore/fork session state`,
|
|
6500
|
+
` ${M}/branch${R} List/create/switch/delete conversation branches (B2)`,
|
|
6378
6501
|
` ${M}/undo${R} Undo file operations`,
|
|
6379
6502
|
` ${M}/yolo${R} Skip all tool confirmations`,
|
|
6380
6503
|
` ${M}/mcp${R} Manage MCP server connections`,
|
|
@@ -6386,6 +6509,7 @@ program.command("help").description("Show a comprehensive guide to all aicli fea
|
|
|
6386
6509
|
` ${D}bash read_file write_file edit_file list_dir grep_files glob_files${R}`,
|
|
6387
6510
|
` ${D}web_fetch google_search run_interactive run_tests spawn_agent${R}`,
|
|
6388
6511
|
` ${D}ask_user write_todos save_memory save_last_response + MCP tools${R}`,
|
|
6512
|
+
` ${D}edit_file has 5 modes: string replace, line insert/delete, batch edits, unified-diff patch${R}`,
|
|
6389
6513
|
"",
|
|
6390
6514
|
`${B}${C} \u25A0 CONFIGURATION${R}`,
|
|
6391
6515
|
` ${D}Config: ~/.aicli/config.json${R}`,
|
|
@@ -6403,6 +6527,8 @@ program.command("help").description("Show a comprehensive guide to all aicli fea
|
|
|
6403
6527
|
` ${G}aicli hub --preset brainstorm "idea"${R} ${D}# multi-agent brainstorm${R}`,
|
|
6404
6528
|
` ${G}aicli hub --task --roles team.json "build app"${R} ${D}# task mode: agents write code${R}`,
|
|
6405
6529
|
` ${G}aicli hub -c spec.md -c api.md "build it"${R} ${D}# hub + docs context${R}`,
|
|
6530
|
+
` ${G}aicli batch submit prompts.jsonl${R} ${D}# Anthropic batch: 50% off, 24h window${R}`,
|
|
6531
|
+
` ${G}aicli batch status <id>${R} ${G}/ results <id> out.jsonl${R} ${D}# poll / download${R}`,
|
|
6406
6532
|
"",
|
|
6407
6533
|
`${D} Docs: https://github.com/jinzhengdong/ai-cli${R}`,
|
|
6408
6534
|
`${D} Issues: https://github.com/jinzhengdong/ai-cli/issues${R}`,
|
|
@@ -6421,7 +6547,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
6421
6547
|
}),
|
|
6422
6548
|
config.get("customProviders")
|
|
6423
6549
|
);
|
|
6424
|
-
const { startHub } = await import("./hub-
|
|
6550
|
+
const { startHub } = await import("./hub-W3BF22UV.js");
|
|
6425
6551
|
await startHub(
|
|
6426
6552
|
{
|
|
6427
6553
|
topic: topic ?? "",
|
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
persistToolRound,
|
|
21
21
|
rebuildExtraMessages,
|
|
22
22
|
setupProxy
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-C4MGON2N.js";
|
|
24
24
|
import {
|
|
25
25
|
AuthManager
|
|
26
26
|
} from "./chunk-BYNY5JPB.js";
|
|
27
27
|
import {
|
|
28
28
|
ConfigManager
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-ASNDBI5R.js";
|
|
30
30
|
import {
|
|
31
31
|
ToolExecutor,
|
|
32
32
|
ToolRegistry,
|
|
@@ -44,9 +44,9 @@ import {
|
|
|
44
44
|
spawnAgentContext,
|
|
45
45
|
truncateOutput,
|
|
46
46
|
undoStack
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-MPIUYP6Q.js";
|
|
48
48
|
import "./chunk-4BKXL7SM.js";
|
|
49
|
-
import "./chunk-
|
|
49
|
+
import "./chunk-3BHGEPIT.js";
|
|
50
50
|
import "./chunk-2ZD3YTVM.js";
|
|
51
51
|
import {
|
|
52
52
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
SKILLS_DIR_NAME,
|
|
67
67
|
VERSION,
|
|
68
68
|
buildUserIdentityPrompt
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-5P4QTZBI.js";
|
|
70
70
|
|
|
71
71
|
// src/web/server.ts
|
|
72
72
|
import express from "express";
|
|
@@ -563,6 +563,15 @@ var SessionHandler = class _SessionHandler {
|
|
|
563
563
|
models: p.info.models.map((m) => ({ id: m.id, name: m.displayName ?? m.id }))
|
|
564
564
|
}));
|
|
565
565
|
const costUsd = computeCost(this.currentProvider, this.currentModel, this.sessionTokenUsage);
|
|
566
|
+
const sess = this.sessions.current;
|
|
567
|
+
const branches = sess ? sess.listBranches().map((b) => ({
|
|
568
|
+
id: b.id,
|
|
569
|
+
title: b.title,
|
|
570
|
+
parentBranchId: b.parentBranchId,
|
|
571
|
+
parentMessageIndex: b.parentMessageIndex,
|
|
572
|
+
created: b.created.toISOString(),
|
|
573
|
+
messageCount: b.id === sess.activeBranchId ? sess.messages.length : sess.getBranchMessages(b.id)?.length ?? 0
|
|
574
|
+
})) : [];
|
|
566
575
|
this.send({
|
|
567
576
|
type: "status",
|
|
568
577
|
provider: this.currentProvider,
|
|
@@ -574,7 +583,9 @@ var SessionHandler = class _SessionHandler {
|
|
|
574
583
|
thinkingMode: this.runtimeThinking ?? false,
|
|
575
584
|
tokenUsage: { ...this.sessionTokenUsage },
|
|
576
585
|
costUsd,
|
|
577
|
-
providers: providerList
|
|
586
|
+
providers: providerList,
|
|
587
|
+
branches,
|
|
588
|
+
activeBranchId: sess?.activeBranchId ?? "main"
|
|
578
589
|
});
|
|
579
590
|
}
|
|
580
591
|
async handleMessage(raw) {
|
|
@@ -1787,6 +1798,105 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1787
1798
|
}
|
|
1788
1799
|
break;
|
|
1789
1800
|
}
|
|
1801
|
+
// ── /branch ─────────────────────────────────────────────────────
|
|
1802
|
+
case "branch": {
|
|
1803
|
+
const session = this.sessions.current;
|
|
1804
|
+
if (!session) {
|
|
1805
|
+
this.send({ type: "error", message: "No active session." });
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
const sub = args[0]?.toLowerCase();
|
|
1809
|
+
if (!sub || sub === "list") {
|
|
1810
|
+
const branches = session.listBranches();
|
|
1811
|
+
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
1812
|
+
for (const b of branches) {
|
|
1813
|
+
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
1814
|
+
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
1815
|
+
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
1816
|
+
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
1817
|
+
}
|
|
1818
|
+
lines.push("");
|
|
1819
|
+
lines.push("Usage: /branch new <msgIndex> [title] | switch <id> | delete <id> | rename <id> <title>");
|
|
1820
|
+
this.send({ type: "info", message: lines.join("\n") });
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
if (sub === "new") {
|
|
1824
|
+
const idxArg = args[1];
|
|
1825
|
+
if (!idxArg) {
|
|
1826
|
+
this.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
|
|
1827
|
+
break;
|
|
1828
|
+
}
|
|
1829
|
+
const fromIndex = parseInt(idxArg, 10);
|
|
1830
|
+
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
1831
|
+
this.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
const title = args.slice(2).join(" ").trim() || void 0;
|
|
1835
|
+
try {
|
|
1836
|
+
const newId = session.createBranch(fromIndex, title);
|
|
1837
|
+
await this.sessions.save();
|
|
1838
|
+
this.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
1839
|
+
this.sendSessionMessages();
|
|
1840
|
+
this.sendStatus();
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
this.send({ type: "error", message: err.message });
|
|
1843
|
+
}
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
if (sub === "switch") {
|
|
1847
|
+
const id = args[1];
|
|
1848
|
+
if (!id) {
|
|
1849
|
+
this.send({ type: "error", message: "Usage: /branch switch <id>" });
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
const ok = session.switchBranch(id);
|
|
1853
|
+
if (ok) {
|
|
1854
|
+
await this.sessions.save();
|
|
1855
|
+
const b = session.getActiveBranch();
|
|
1856
|
+
this.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
1857
|
+
this.sendSessionMessages();
|
|
1858
|
+
this.sendStatus();
|
|
1859
|
+
} else {
|
|
1860
|
+
this.send({ type: "error", message: `Cannot switch to "${id}" (not found or already active).` });
|
|
1861
|
+
}
|
|
1862
|
+
break;
|
|
1863
|
+
}
|
|
1864
|
+
if (sub === "delete") {
|
|
1865
|
+
const id = args[1];
|
|
1866
|
+
if (!id) {
|
|
1867
|
+
this.send({ type: "error", message: "Usage: /branch delete <id>" });
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
const ok = session.deleteBranch(id);
|
|
1871
|
+
if (ok) {
|
|
1872
|
+
await this.sessions.save();
|
|
1873
|
+
this.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
1874
|
+
this.sendStatus();
|
|
1875
|
+
} else {
|
|
1876
|
+
this.send({ type: "error", message: `Cannot delete "${id}" (not found, active, or last remaining branch).` });
|
|
1877
|
+
}
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
if (sub === "rename") {
|
|
1881
|
+
const id = args[1];
|
|
1882
|
+
const title = args.slice(2).join(" ").trim();
|
|
1883
|
+
if (!id || !title) {
|
|
1884
|
+
this.send({ type: "error", message: "Usage: /branch rename <id> <new title>" });
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
const ok = session.renameBranch(id, title);
|
|
1888
|
+
if (ok) {
|
|
1889
|
+
await this.sessions.save();
|
|
1890
|
+
this.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
1891
|
+
this.sendStatus();
|
|
1892
|
+
} else {
|
|
1893
|
+
this.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
1894
|
+
}
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
this.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename.` });
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1790
1900
|
// ── /fork ───────────────────────────────────────────────────────
|
|
1791
1901
|
case "fork": {
|
|
1792
1902
|
const session = this.sessions.current;
|
|
@@ -1952,7 +2062,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1952
2062
|
case "test": {
|
|
1953
2063
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1954
2064
|
try {
|
|
1955
|
-
const { executeTests } = await import("./run-tests-
|
|
2065
|
+
const { executeTests } = await import("./run-tests-V2JJADIU.js");
|
|
1956
2066
|
const argStr = args.join(" ").trim();
|
|
1957
2067
|
let testArgs = {};
|
|
1958
2068
|
if (argStr) {
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
getDangerLevel,
|
|
5
5
|
googleSearchContext,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MPIUYP6Q.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-3BHGEPIT.js";
|
|
10
10
|
import "./chunk-2ZD3YTVM.js";
|
|
11
11
|
import {
|
|
12
12
|
SUBAGENT_ALLOWED_TOOLS
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-5P4QTZBI.js";
|
|
14
14
|
|
|
15
15
|
// src/hub/task-orchestrator.ts
|
|
16
16
|
import { createInterface } from "readline";
|
package/dist/web/client/app.js
CHANGED
|
@@ -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);
|
|
@@ -1044,7 +1055,10 @@ function renderFilteredSessions(filter) {
|
|
|
1044
1055
|
}
|
|
1045
1056
|
|
|
1046
1057
|
// ── Session Replay (B1) ─────────────────────────────────
|
|
1058
|
+
let _replaySessionId = null; // session shown in the replay modal
|
|
1059
|
+
|
|
1047
1060
|
async function openReplay(sessionId) {
|
|
1061
|
+
_replaySessionId = sessionId;
|
|
1048
1062
|
const modal = document.getElementById('replay-modal');
|
|
1049
1063
|
const metaEl = document.getElementById('replay-meta');
|
|
1050
1064
|
const usageEl = document.getElementById('replay-usage');
|
|
@@ -1092,6 +1106,34 @@ function renderReplay(session, metaEl, usageEl, timelineEl) {
|
|
|
1092
1106
|
|
|
1093
1107
|
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
1094
1108
|
timelineEl.innerHTML = messages.map((m, i) => renderReplayStep(m, i)).join('');
|
|
1109
|
+
|
|
1110
|
+
// Wire 🌿 fork-from-here buttons.
|
|
1111
|
+
timelineEl.querySelectorAll('.replay-fork-btn').forEach((btn) => {
|
|
1112
|
+
btn.addEventListener('click', (e) => {
|
|
1113
|
+
e.stopPropagation();
|
|
1114
|
+
const idx = parseInt(btn.dataset.msgIndex || '0', 10);
|
|
1115
|
+
forkFromReplay(idx);
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function forkFromReplay(msgIndex) {
|
|
1121
|
+
if (!_replaySessionId) return;
|
|
1122
|
+
const activeSid = sessionTabs[activeTabIdx]?.sessionId;
|
|
1123
|
+
const title = prompt(`New branch title (forking at message #${msgIndex}):`, '');
|
|
1124
|
+
if (title === null) return; // cancelled
|
|
1125
|
+
const args = ['new', String(msgIndex), ...(title.trim() ? [title.trim()] : [])];
|
|
1126
|
+
if (activeSid && activeSid === _replaySessionId) {
|
|
1127
|
+
// Replayed session is already active — just branch.
|
|
1128
|
+
send({ type: 'command', name: 'branch', args });
|
|
1129
|
+
} else {
|
|
1130
|
+
// Load the replayed session first, then branch.
|
|
1131
|
+
send({ type: 'command', name: 'session', args: ['load', _replaySessionId] });
|
|
1132
|
+
// Small delay to let the load settle before branching.
|
|
1133
|
+
setTimeout(() => send({ type: 'command', name: 'branch', args }), 400);
|
|
1134
|
+
}
|
|
1135
|
+
const modal = document.getElementById('replay-modal');
|
|
1136
|
+
if (modal && typeof modal.close === 'function') modal.close();
|
|
1095
1137
|
}
|
|
1096
1138
|
|
|
1097
1139
|
function renderReplayStep(m, idx) {
|
|
@@ -1142,11 +1184,143 @@ function renderReplayStep(m, idx) {
|
|
|
1142
1184
|
<span class="role-tag">${escapeHtml(roleTag)}</span>
|
|
1143
1185
|
<span class="opacity-50">${ts}</span>
|
|
1144
1186
|
${m.toolCallId ? `<span class="opacity-40">↳ ${escapeHtml(m.toolCallId)}</span>` : ''}
|
|
1187
|
+
<button class="replay-fork-btn opacity-60 hover:opacity-100 text-xs ml-auto" data-msg-index="${idx + 1}" title="Fork a new branch from this point">🌿 fork here</button>
|
|
1145
1188
|
</div>
|
|
1146
1189
|
${body || '<div class="opacity-40 text-xs">(empty)</div>'}
|
|
1147
1190
|
</div>`;
|
|
1148
1191
|
}
|
|
1149
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
|
+
|
|
1150
1324
|
function startSessionRename(itemEl, titleEl) {
|
|
1151
1325
|
const sessionId = itemEl.dataset.sessionId;
|
|
1152
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
|
+
}
|