jinzd-ai-cli 0.1.83 → 0.1.85

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/CLAUDE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 项目简介
4
4
 
5
- 一个跨平台的 REPL 风格 AI 对话工具,支持多个主流 AI 提供商(OpenAI、Claude、Gemini、DeepSeek、智谱清言、Kimi),
5
+ 一个跨平台的 REPL 风格 AI 对话工具,支持多个主流 AI 提供商(OpenAI、Claude、Gemini、DeepSeek、智谱清言、Kimi、OpenRouter),
6
6
  带有 **AI 工具调用(Agentic)** 能力,支持执行 bash 命令、读写文件、运行交互式程序、流式生成大文档。
7
7
  代理支持(`proxy` 配置字段 + 环境变量),Gemini 完整支持(2.5 Pro/Flash,array items schema 修复)。
8
8
  **MCP 协议支持**:可接入外部 MCP 服务器,自动发现并注册工具,无缝融入 agentic 循环。
@@ -32,6 +32,7 @@ src/
32
32
  │ ├── claude.ts # Anthropic SDK provider
33
33
  │ ├── gemini.ts # Google Generative AI provider(role: assistant → model)
34
34
  │ ├── openai.ts # OpenAI provider(GPT-5.4/5/4.1/4o/o3/o4-mini)
35
+ │ ├── openrouter.ts # OpenRouter provider(多模型聚合,16 个热门模型)
35
36
  │ ├── deepseek.ts / zhipu.ts / kimi.ts # 继承 OpenAICompatibleProvider;deepseek/kimi 覆写 chatWithTools 实现虚假声明检测
36
37
  ├── config/
37
38
  │ ├── schema.ts # Zod schema(含 timeouts / customBaseUrls / defaultModels)
@@ -165,6 +166,7 @@ AICLI_API_KEY_GEMINI Gemini API Key
165
166
  AICLI_API_KEY_DEEPSEEK DeepSeek API Key
166
167
  AICLI_API_KEY_ZHIPU 智谱 API Key
167
168
  AICLI_API_KEY_KIMI Kimi API Key
169
+ AICLI_API_KEY_OPENROUTER OpenRouter API Key
168
170
  AICLI_API_KEY_GOOGLESEARCH Google Custom Search API Key
169
171
  AICLI_GOOGLE_CX Google Search Engine ID (cx)
170
172
  AICLI_PROVIDER 默认 Provider ID
@@ -350,6 +352,88 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
350
352
  - [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
351
353
  - [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
352
354
 
355
+ ## 本轮开发完成记录(2026-03-16,v0.1.84 → v0.1.85)
356
+
357
+ ### Web UI P1 增强:侧边栏 Tools Tab + Diff 语法高亮
358
+
359
+ **P1-1:侧边栏增强 — Sessions/Tools 双 Tab**
360
+
361
+ 将原有单一 session 列表侧边栏升级为双 Tab 布局:
362
+
363
+ | 文件 | 变更类型 | 说明 |
364
+ |------|---------|------|
365
+ | `src/web/protocol.ts` | 修改 | 新增 `S2C_ToolsList` 消息类型(builtinTools/mcpServers/skills 三段数据) |
366
+ | `src/web/session-handler.ts` | 修改 | 新增 `/tools` 命令 + `sendToolsList()` 方法(遍历 ToolRegistry + MCP + Skills) |
367
+ | `src/web/client/index.html` | 修改 | 侧边栏重构为 Tabs(📋 Sessions / 🔧 Tools)+ 搜索框 |
368
+ | `src/web/client/style.css` | 修改 | Tab 激活样式 + 工具列表样式(section 标题/工具项/彩色圆点/MCP 折叠) |
369
+ | `src/web/client/app.js` | 修改 | Tab 切换逻辑 + `renderToolsList()` + `renderFilteredTools()` 搜索过滤 |
370
+
371
+ - **Built-in Tools**:显示所有内置工具,带危险级别彩色圆点(蓝=safe, 黄=write, 红=destructive)
372
+ - **MCP Servers**:显示连接状态(绿=connected)+ 工具数量 badge,可折叠展开查看工具列表
373
+ - **Skills**:显示所有可用技能,绿色=active,灰色=inactive
374
+ - **搜索过滤**:实时过滤工具名和描述,空 section 自动隐藏
375
+ - **Session 搜索**:Sessions tab 新增搜索框,按标题过滤
376
+
377
+ **P1-2:Diff 渲染增强 — 语法高亮**
378
+
379
+ 工具确认对话框中的 diff 预览从纯文本升级为语法高亮:
380
+
381
+ | 文件 | 变更类型 | 说明 |
382
+ |------|---------|------|
383
+ | `src/web/client/style.css` | 修改 | 新增 `.diff-add`/`.diff-del`/`.diff-hunk`/`.diff-ctx` 四种行样式 |
384
+ | `src/web/client/app.js` | 修改 | 新增 `renderDiffHtml()` 函数,按行首字符(+/-/@@)分类着色 |
385
+
386
+ - `+` 增加行:绿色文字 + 绿色半透明背景
387
+ - `-` 删除行:红色文字 + 红色半透明背景
388
+ - `@@` Hunk header:主题色(紫色),加粗
389
+ - 其他上下文行:降低不透明度
390
+
391
+ ### 版本与收尾
392
+ - `src/core/constants.ts`:VERSION `0.1.84` → `0.1.85`
393
+ - `package.json`:version 同步
394
+ - 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
395
+ - Web UI 预览测试:侧边栏 Tab 切换、工具列表渲染、搜索过滤、MCP/Skills 显示 全部通过
396
+
397
+ ---
398
+
399
+ ## 本轮开发完成记录(2026-03-16,v0.1.82 → v0.1.83)
400
+
401
+ ### 新增功能:OpenRouter 内置 Provider
402
+
403
+ **背景**:OpenRouter 是多模型聚合 API 平台,提供统一的 OpenAI 兼容接口访问数百个模型。用户可通过单个 API Key 使用来自 Anthropic、OpenAI、Google、Meta、DeepSeek、Qwen、Mistral 等厂商的模型。
404
+
405
+ **实现**:继承 `OpenAICompatibleProvider`,无需覆写任何方法(API 完全兼容)。
406
+
407
+ | 文件 | 变更类型 | 说明 |
408
+ |------|---------|------|
409
+ | `src/providers/openrouter.ts` | **新增** | OpenRouterProvider 类,16 个热门模型 |
410
+ | `src/providers/registry.ts` | 修改 | BUILT_IN_PROVIDERS 新增 OpenRouterProvider |
411
+ | `src/config/env-loader.ts` | 修改 | ENV_KEY_MAP 新增 `openrouter → AICLI_API_KEY_OPENROUTER` |
412
+ | `src/repl/setup-wizard.ts` | 修改 | PROVIDERS 列表新增 OpenRouter |
413
+ | `src/core/constants.ts` | 修改 | VERSION 0.1.82 → 0.1.83 |
414
+ | `package.json` | 修改 | version 0.1.82 → 0.1.83 |
415
+
416
+ **内置模型列表(16 个)**:
417
+ - Anthropic:Claude Opus 4 / Sonnet 4 / Haiku 4
418
+ - OpenAI:GPT-5.4 / GPT-4o / GPT-4o Mini
419
+ - Google:Gemini 2.5 Pro / Flash
420
+ - Meta:Llama 4 Maverick / Scout
421
+ - DeepSeek:V3 0324 / R1
422
+ - Qwen:Qwen3 Coder / Qwen 2.5 72B
423
+ - Mistral:Large / Small 3.1 24B
424
+
425
+ **Web UI 无需修改**:Web UI 采用动态注册架构,provider 列表通过 WebSocket S2C_Status 从后端 ProviderRegistry 动态获取,自动支持新 Provider。
426
+
427
+ **同步更新 CLAUDE.md**:项目简介、项目结构、环境变量、Web UI 路线图(P0 全部标记已完成)。
428
+
429
+ ### 版本与收尾
430
+ - `src/core/constants.ts`:VERSION `0.1.82` → `0.1.83`
431
+ - `package.json`:version 同步
432
+ - 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
433
+ - 发布:`npm publish` → `jinzd-ai-cli@0.1.83`
434
+
435
+ ---
436
+
353
437
  ## 本轮开发完成记录(2026-03-10,v0.1.57 → v0.1.58)
354
438
 
355
439
  ### 代码质量修复:19 个低危问题修复 + 代码重构
@@ -1860,7 +1944,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
1860
1944
 
1861
1945
  > 目标:将 `aicli web` 从"可用的聊天界面"升级为功能完备、体验出色的 AI 代码助手 Web 端。
1862
1946
 
1863
- ### 当前 Web UI 已实现功能(v0.1.76 基线)
1947
+ ### 当前 Web UI 已实现功能(v0.1.82 基线)
1864
1948
 
1865
1949
  - 实时流式聊天(WebSocket)
1866
1950
  - 完整 Agentic 循环(25 轮工具调用 + 流式工具调用)
@@ -1871,27 +1955,30 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
1871
1955
  - Plan Mode 只读规划
1872
1956
  - Markdown 渲染 + 代码高亮(marked.js + highlight.js)
1873
1957
  - 代码块复制按钮
1874
- - 图片上传协议支持(C2S `images` 字段)
1875
1958
  - 用户纠偏(interjection)— 处理中可发送纠正指令
1876
- - /clear /compact /think /plan /status /provider /model 命令
1959
+ - Session 持久化与管理(侧边栏列表、新建/切换/恢复/删除会话)
1960
+ - `@文件` 引用(`@` 触发文件选择下拉、目录遍历、服务端文件读取 API)
1961
+ - 图片拖拽/粘贴上传(D&D + Ctrl+V + 预览缩略图 + 10MB 限制)
1962
+ - 工具卡片折叠(safe 默认折叠、write/destructive 展开、可手动 toggle)
1963
+ - /clear /compact /think /plan /status /provider /model /cost /session /help 命令
1877
1964
 
1878
- ### P0 — 最痛问题(当前就会遇到)
1965
+ ### P0 — 最痛问题(全部已完成 ✅)
1879
1966
 
1880
1967
  | # | 功能 | 状态 | 说明 |
1881
1968
  |---|------|------|------|
1882
- | P0-1 | **Session 持久化与管理** | [ ] | 刷新页面不丢对话;会话列表侧边栏;新建/切换/恢复/删除会话;历史搜索 |
1883
- | P0-2 | **`@文件` 引用** | [ ] | CLI 支持 `@file.ts` 读文件注入上下文,Web 需要:`@` 触发文件选择 + 服务端文件读取 API |
1884
- | P0-3 | **图片拖拽/粘贴上传** | [ ] | 协议层已有 `images` 字段,缺少客户端 UI:拖拽到输入区 + `Ctrl+V` 粘贴截图 + 预览缩略图 |
1885
- | P0-4 | **工具卡片折叠** | [ ] | 多轮工具调用后聊天区被淹没;safe 级别默认折叠、write/destructive 展开;可手动 toggle |
1886
- | P0-5 | **`/help` 命令** | [ ] | 显示所有支持的 Web 命令及用法说明;输入不存在的命令给出提示 |
1969
+ | P0-1 | **Session 持久化与管理** | [x] | 侧边栏会话列表 + 新建/切换/恢复/删除 + JSON 持久化 |
1970
+ | P0-2 | **`@文件` 引用** | [x] | `@` 触发文件选择下拉 + `/api/file-content` 读取 + 路径穿越安全检查 |
1971
+ | P0-3 | **图片拖拽/粘贴上传** | [x] | D&D + Ctrl+V 粘贴 + 预览缩略图 + base64 传输 + 10MB 限制 |
1972
+ | P0-4 | **工具卡片折叠** | [x] | safe 默认折叠、write/destructive 展开、手动 toggle、结果 badge |
1973
+ | P0-5 | **`/help` 命令** | [x] | 显示所有支持的 Web 命令及用法说明 |
1887
1974
 
1888
1975
  ### P1 — 明显提升体验
1889
1976
 
1890
1977
  | # | 功能 | 状态 | 说明 |
1891
1978
  |---|------|------|------|
1892
- | P1-1 | **侧边栏布局** | [ ] | 左侧:Session 列表 + Skills + MCP 工具列表 + 搜索;右侧:聊天区;响应式 mobile 折叠 |
1893
- | P1-2 | **Diff 渲染增强** | [ ] | confirm 对话框中的 diff 从纯文本升级为语法高亮的 diff 视图(绿/红增删行) |
1894
- | P1-3 | **更多命令支持** | [ ] | `/cost`(token 用量统计)、`/session list\|load`、`/undo`、`/export [md\|json]`、`/tools`(工具列表) |
1979
+ | P1-1 | **侧边栏增强** | [x] | Sessions/Tools Tab + Built-in Tools(危险级别圆点)+ MCP Servers(连接状态+工具数)+ Skills 列表 + 搜索过滤 |
1980
+ | P1-2 | **Diff 渲染增强** | [x] | confirm 对话框 diff 语法高亮(绿色增行、红色删行、紫色 hunk header、灰色上下文) |
1981
+ | P1-3 | **更多命令支持** | [~] | `/cost`、`/session list\|load\|delete` 已实现;待增加 `/undo`、`/export [md\|json]`、`/tools`、`/memory` |
1895
1982
  | P1-4 | **键盘快捷键** | [ ] | `Ctrl+L` 清屏、`Ctrl+K` 清空输入、`Esc` 停止生成、`↑` 历史消息回溯 |
1896
1983
  | P1-5 | **Markdown 导出** | [ ] | 一键导出当前对话为 `.md` 文件下载 |
1897
1984
  | P1-6 | **断线重连恢复** | [ ] | WebSocket 重连后恢复 session 状态(当前重连=全丢);心跳 ping/pong |
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-VLVLY5YV.js";
19
+ } from "./chunk-VR7VECPG.js";
20
20
 
21
21
  // src/config/config-manager.ts
22
22
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -14,7 +14,7 @@ import { platform } from "os";
14
14
  import chalk from "chalk";
15
15
 
16
16
  // src/core/constants.ts
17
- var VERSION = "0.1.83";
17
+ var VERSION = "0.1.85";
18
18
  var APP_NAME = "ai-cli";
19
19
  var CONFIG_DIR_NAME = ".aicli";
20
20
  var CONFIG_FILE_NAME = "config.json";
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import {
35
35
  theme,
36
36
  truncateOutput,
37
37
  undoStack
38
- } from "./chunk-F24DAC6L.js";
38
+ } from "./chunk-QHZGVP5X.js";
39
39
  import {
40
40
  AGENTIC_BEHAVIOR_GUIDELINE,
41
41
  AUTHOR,
@@ -55,7 +55,7 @@ import {
55
55
  REPO_URL,
56
56
  SKILLS_DIR_NAME,
57
57
  VERSION
58
- } from "./chunk-VLVLY5YV.js";
58
+ } from "./chunk-VR7VECPG.js";
59
59
 
60
60
  // src/index.ts
61
61
  import { program } from "commander";
@@ -1904,7 +1904,7 @@ ${hint}` : "")
1904
1904
  description: "Run project tests and show structured report",
1905
1905
  usage: "/test [command|filter]",
1906
1906
  async execute(args, _ctx) {
1907
- const { executeTests } = await import("./run-tests-QYSZ27IZ.js");
1907
+ const { executeTests } = await import("./run-tests-IFXCV4UW.js");
1908
1908
  const argStr = args.join(" ").trim();
1909
1909
  let testArgs = {};
1910
1910
  if (argStr) {
@@ -5292,7 +5292,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5292
5292
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5293
5293
  process.exit(1);
5294
5294
  }
5295
- const { startWebServer } = await import("./server-ZFMNC6PH.js");
5295
+ const { startWebServer } = await import("./server-2LECXHO6.js");
5296
5296
  await startWebServer({ port, host: options.host });
5297
5297
  });
5298
5298
  program.command("sessions").description("List recent conversation sessions").action(async () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-VLVLY5YV.js";
5
+ } from "./chunk-VR7VECPG.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
@@ -23,7 +23,7 @@ import {
23
23
  setupProxy,
24
24
  spawnAgentContext,
25
25
  truncateOutput
26
- } from "./chunk-F24DAC6L.js";
26
+ } from "./chunk-QHZGVP5X.js";
27
27
  import {
28
28
  AGENTIC_BEHAVIOR_GUIDELINE,
29
29
  CONTEXT_FILE_CANDIDATES,
@@ -36,7 +36,7 @@ import {
36
36
  SKILLS_DIR_NAME,
37
37
  VERSION,
38
38
  __require
39
- } from "./chunk-VLVLY5YV.js";
39
+ } from "./chunk-VR7VECPG.js";
40
40
 
41
41
  // src/web/server.ts
42
42
  import express from "express";
@@ -961,6 +961,7 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
961
961
  " /session delete <id> \u2014 Delete a session",
962
962
  " /status \u2014 Show session info & token usage",
963
963
  " /cost \u2014 Show cumulative token usage",
964
+ " /tools \u2014 Show tools, MCP servers & skills in sidebar",
964
965
  " /help \u2014 Show this help message",
965
966
  "",
966
967
  "\u{1F4A1} Tips:",
@@ -985,6 +986,9 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
985
986
  });
986
987
  break;
987
988
  }
989
+ case "tools":
990
+ this.sendToolsList();
991
+ break;
988
992
  default:
989
993
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
990
994
  }
@@ -1036,6 +1040,38 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1036
1040
  }))
1037
1041
  });
1038
1042
  }
1043
+ sendToolsList() {
1044
+ const allDefs = this.toolRegistry.getDefinitions();
1045
+ const builtinTools = allDefs.filter((d) => !d.name.startsWith("mcp__")).map((d) => ({
1046
+ name: d.name,
1047
+ description: d.description,
1048
+ dangerLevel: getDangerLevel(d.name, {})
1049
+ }));
1050
+ const mcpServers = [];
1051
+ if (this.mcpManager) {
1052
+ for (const status of this.mcpManager.getStatus()) {
1053
+ const serverTools = allDefs.filter((d) => d.name.startsWith(`mcp__${status.serverId}__`)).map((d) => d.name.replace(`mcp__${status.serverId}__`, ""));
1054
+ mcpServers.push({
1055
+ serverId: status.serverId,
1056
+ serverName: status.serverName,
1057
+ toolCount: status.toolCount,
1058
+ connected: status.connected,
1059
+ tools: serverTools
1060
+ });
1061
+ }
1062
+ }
1063
+ const skills = (this.skillManager?.listSkills() ?? []).map((s) => ({
1064
+ name: s.meta.name,
1065
+ description: s.meta.description,
1066
+ isActive: this.skillManager?.getActive()?.meta.name === s.meta.name
1067
+ }));
1068
+ this.send({
1069
+ type: "tools_list",
1070
+ builtinTools,
1071
+ mcpServers,
1072
+ skills
1073
+ });
1074
+ }
1039
1075
  sendSessionMessages() {
1040
1076
  const session = this.sessions.current;
1041
1077
  if (!session) return;
@@ -1322,7 +1358,7 @@ async function startWebServer(options = {}) {
1322
1358
  process.on("SIGINT", () => {
1323
1359
  console.log("\n Shutting down...");
1324
1360
  if (activeHandler) activeHandler.onDisconnect();
1325
- if (mcpManager) mcpManager.disconnectAll();
1361
+ if (mcpManager) mcpManager.closeAll();
1326
1362
  wss.close();
1327
1363
  server.close();
1328
1364
  process.exit(0);
@@ -34,6 +34,11 @@ const connectionStatus = document.getElementById('connection-status');
34
34
  const sidebar = document.getElementById('sidebar');
35
35
  const sessionListEl = document.getElementById('session-list');
36
36
  const btnNewSession = document.getElementById('btn-new-session');
37
+ const toolsListEl = document.getElementById('tools-list');
38
+ const sessionSearchInput = document.getElementById('session-search');
39
+ const toolsSearchInput = document.getElementById('tools-search');
40
+ let cachedSessions = [];
41
+ let cachedToolsData = null;
37
42
 
38
43
  // ── Configure marked.js ────────────────────────────────────────────
39
44
 
@@ -107,6 +112,7 @@ function handleServerMessage(msg) {
107
112
  case 'status': handleStatus(msg); break;
108
113
  case 'session_list': renderSessionList(msg.sessions); break;
109
114
  case 'session_messages':renderSessionMessages(msg.messages); break;
115
+ case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
110
116
  case 'info': addInfoMessage(msg.message); break;
111
117
  case 'error': addErrorMessage(msg.message); setProcessing(false); break;
112
118
  case 'round_progress': break;
@@ -205,7 +211,7 @@ function handleConfirmRequest(msg) {
205
211
  <span class="badge ${isDestructive ? 'badge-error' : 'badge-warning'} badge-sm">${isDestructive ? '⚠ DESTRUCTIVE' : '✎ Write'}</span>
206
212
  <span class="text-sm font-semibold">${escapeHtml(msg.toolName)}</span>
207
213
  </div>
208
- ${msg.diff ? `<div class="confirm-diff w-full">${escapeHtml(msg.diff)}</div>` : ''}
214
+ ${msg.diff ? `<div class="confirm-diff w-full">${renderDiffHtml(msg.diff)}</div>` : ''}
209
215
  <div class="flex gap-2 mt-2">
210
216
  <button class="btn btn-success btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', true)">✓ Approve</button>
211
217
  <button class="btn btn-error btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', false)">✗ Deny</button>
@@ -591,8 +597,16 @@ modelSelect.addEventListener('change', () => {
591
597
  // ── Session management ──────────────────────────────────────────────
592
598
 
593
599
  function renderSessionList(sessions) {
600
+ cachedSessions = sessions || [];
601
+ renderFilteredSessions(sessionSearchInput?.value || '');
602
+ }
603
+
604
+ function renderFilteredSessions(filter) {
605
+ const sessions = filter
606
+ ? cachedSessions.filter(s => (s.title || '').toLowerCase().includes(filter.toLowerCase()))
607
+ : cachedSessions;
594
608
  if (!sessions || sessions.length === 0) {
595
- sessionListEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">No sessions yet</div>';
609
+ sessionListEl.innerHTML = `<div class="text-xs opacity-40 text-center py-4">${filter ? 'No matches' : 'No sessions yet'}</div>`;
596
610
  return;
597
611
  }
598
612
  sessionListEl.innerHTML = sessions.map(s => {
@@ -656,6 +670,122 @@ btnNewSession.addEventListener('click', () => {
656
670
  // Request session list on connect
657
671
  function requestSessionList() {
658
672
  send({ type: 'command', name: 'session', args: ['list'] });
673
+ // Also request tools list for sidebar
674
+ send({ type: 'command', name: 'tools', args: [] });
675
+ }
676
+
677
+ // Session search filter
678
+ if (sessionSearchInput) {
679
+ sessionSearchInput.addEventListener('input', () => {
680
+ renderFilteredSessions(sessionSearchInput.value);
681
+ });
682
+ }
683
+
684
+ // ── Sidebar tabs ──────────────────────────────────────────────────────
685
+
686
+ function switchSidebarTab(tabName) {
687
+ document.querySelectorAll('.sidebar-tab').forEach(btn => {
688
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
689
+ });
690
+ document.querySelectorAll('.sidebar-tab-content').forEach(el => {
691
+ el.classList.toggle('hidden', el.id !== `tab-${tabName}`);
692
+ });
693
+ }
694
+
695
+ document.querySelectorAll('.sidebar-tab').forEach(btn => {
696
+ btn.addEventListener('click', () => switchSidebarTab(btn.dataset.tab));
697
+ });
698
+
699
+ // ── Tools list rendering ──────────────────────────────────────────────
700
+
701
+ function renderToolsList(data) {
702
+ cachedToolsData = data;
703
+ renderFilteredTools(toolsSearchInput?.value || '');
704
+ }
705
+
706
+ function renderFilteredTools(filter) {
707
+ if (!cachedToolsData) {
708
+ toolsListEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">Type /tools to load</div>';
709
+ return;
710
+ }
711
+ const fl = filter.toLowerCase();
712
+ let html = '';
713
+
714
+ // Built-in tools
715
+ const tools = fl
716
+ ? cachedToolsData.builtinTools.filter(t => t.name.includes(fl) || t.description.toLowerCase().includes(fl))
717
+ : cachedToolsData.builtinTools;
718
+ if (tools.length > 0) {
719
+ html += '<div class="tools-section-title">Built-in Tools</div>';
720
+ html += tools.map(t =>
721
+ `<div class="tool-item" title="${escapeHtml(t.description)}">
722
+ <span class="tool-dot tool-dot-${t.dangerLevel}"></span>
723
+ <span class="flex-1 truncate">${escapeHtml(t.name)}</span>
724
+ </div>`
725
+ ).join('');
726
+ }
727
+
728
+ // MCP servers
729
+ const servers = fl
730
+ ? cachedToolsData.mcpServers.filter(s =>
731
+ s.serverId.includes(fl) || s.serverName.toLowerCase().includes(fl) ||
732
+ s.tools.some(t => t.includes(fl)))
733
+ : cachedToolsData.mcpServers;
734
+ if (servers.length > 0) {
735
+ html += '<div class="tools-section-title mt-2">MCP Servers</div>';
736
+ for (const s of servers) {
737
+ const dotClass = s.connected ? 'tool-dot-connected' : 'tool-dot-disconnected';
738
+ html += `<details class="mcp-server-item">
739
+ <summary class="flex items-center gap-2 cursor-pointer select-none">
740
+ <span class="tool-dot ${dotClass}"></span>
741
+ <span class="flex-1 truncate">${escapeHtml(s.serverId)}</span>
742
+ <span class="text-xs opacity-40">${s.toolCount}</span>
743
+ </summary>
744
+ <div class="mcp-server-tools">
745
+ ${s.tools.map(t => `<div class="py-0.5">${escapeHtml(t)}</div>`).join('')}
746
+ </div>
747
+ </details>`;
748
+ }
749
+ }
750
+
751
+ // Skills
752
+ const skills = fl
753
+ ? cachedToolsData.skills.filter(s => s.name.includes(fl) || s.description.toLowerCase().includes(fl))
754
+ : cachedToolsData.skills;
755
+ if (skills.length > 0) {
756
+ html += '<div class="tools-section-title mt-2">Skills</div>';
757
+ html += skills.map(s =>
758
+ `<div class="tool-item" title="${escapeHtml(s.description)}">
759
+ <span class="tool-dot ${s.isActive ? 'tool-dot-active' : 'tool-dot-inactive'}"></span>
760
+ <span class="flex-1 truncate">${escapeHtml(s.name)}</span>
761
+ ${s.isActive ? '<span class="badge badge-success badge-xs">active</span>' : ''}
762
+ </div>`
763
+ ).join('');
764
+ }
765
+
766
+ if (!html) {
767
+ html = '<div class="text-xs opacity-40 text-center py-4">No matches</div>';
768
+ }
769
+ toolsListEl.innerHTML = html;
770
+ }
771
+
772
+ // Tools search filter
773
+ if (toolsSearchInput) {
774
+ toolsSearchInput.addEventListener('input', () => {
775
+ renderFilteredTools(toolsSearchInput.value);
776
+ });
777
+ }
778
+
779
+ // ── Diff syntax highlighting ──────────────────────────────────────────
780
+
781
+ function renderDiffHtml(diffText) {
782
+ return diffText.split('\n').map(line => {
783
+ const escaped = escapeHtml(line);
784
+ if (line.startsWith('@@')) return `<div class="diff-hunk">${escaped}</div>`;
785
+ if (line.startsWith('+')) return `<div class="diff-add">${escaped}</div>`;
786
+ if (line.startsWith('-')) return `<div class="diff-del">${escaped}</div>`;
787
+ return `<div class="diff-ctx">${escaped}</div>`;
788
+ }).join('');
659
789
  }
660
790
 
661
791
  // ── @ File reference autocomplete ───────────────────────────────────
@@ -50,12 +50,29 @@
50
50
 
51
51
  <!-- Sidebar -->
52
52
  <aside id="sidebar" class="sidebar bg-base-200 border-r border-base-content/10 flex flex-col w-64 flex-shrink-0 overflow-hidden transition-all duration-200">
53
- <div class="p-3 border-b border-base-content/10 flex items-center justify-between h-12">
54
- <span class="font-semibold text-sm leading-none">Sessions</span>
55
- <button id="btn-new-session" class="btn btn-xs btn-primary btn-outline leading-none" title="New session">+ New</button>
53
+ <!-- Sidebar tabs -->
54
+ <div class="flex border-b border-base-content/10 flex-shrink-0">
55
+ <button class="sidebar-tab active flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="sessions">📋 Sessions</button>
56
+ <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="tools">🔧 Tools</button>
56
57
  </div>
57
- <div id="session-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
58
- <div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
58
+ <!-- Sessions tab -->
59
+ <div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
60
+ <div class="p-2 border-b border-base-content/10 flex items-center justify-between">
61
+ <input id="session-search" type="text" class="input input-xs input-bordered flex-1 mr-2" placeholder="Search sessions...">
62
+ <button id="btn-new-session" class="btn btn-xs btn-primary btn-outline flex-shrink-0" title="New session">+ New</button>
63
+ </div>
64
+ <div id="session-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
65
+ <div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
66
+ </div>
67
+ </div>
68
+ <!-- Tools tab -->
69
+ <div id="tab-tools" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden hidden">
70
+ <div class="p-2 border-b border-base-content/10">
71
+ <input id="tools-search" type="text" class="input input-xs input-bordered w-full" placeholder="Search tools...">
72
+ </div>
73
+ <div id="tools-list" class="flex-1 overflow-y-auto p-2 text-sm">
74
+ <div class="text-xs opacity-40 text-center py-4">Type /tools to load</div>
75
+ </div>
59
76
  </div>
60
77
  </aside>
61
78
 
@@ -239,6 +239,20 @@
239
239
  .status-connected { color: oklch(var(--su)); }
240
240
  .status-disconnected { color: oklch(var(--er)); }
241
241
 
242
+ /* ── Sidebar tabs ──────────────────────────────────── */
243
+ .sidebar-tab {
244
+ cursor: pointer;
245
+ opacity: 0.5;
246
+ transition: all 0.15s;
247
+ border-bottom: 2px solid transparent;
248
+ }
249
+ .sidebar-tab:hover { opacity: 0.8; }
250
+ .sidebar-tab.active {
251
+ opacity: 1;
252
+ border-bottom-color: oklch(var(--p));
253
+ color: oklch(var(--p));
254
+ }
255
+
242
256
  /* ── Sidebar ───────────────────────────────────────── */
243
257
  .sidebar .session-item {
244
258
  padding: 0.5rem 0.6rem;
@@ -280,6 +294,73 @@
280
294
  display: inline-block;
281
295
  }
282
296
 
297
+ /* ── Diff syntax highlighting ─────────────────────── */
298
+ .confirm-diff .diff-add {
299
+ color: oklch(var(--su));
300
+ background: oklch(var(--su) / 0.1);
301
+ }
302
+ .confirm-diff .diff-del {
303
+ color: oklch(var(--er));
304
+ background: oklch(var(--er) / 0.1);
305
+ }
306
+ .confirm-diff .diff-hunk {
307
+ color: oklch(var(--p));
308
+ opacity: 0.8;
309
+ font-weight: 600;
310
+ }
311
+ .confirm-diff .diff-ctx {
312
+ opacity: 0.6;
313
+ }
314
+
315
+ /* ── Tools sidebar list ──────────────────────────── */
316
+ .tools-section-title {
317
+ font-size: 0.7rem;
318
+ font-weight: 700;
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.05em;
321
+ opacity: 0.4;
322
+ padding: 0.5rem 0.25rem 0.25rem;
323
+ }
324
+ .tool-item {
325
+ padding: 0.3rem 0.5rem;
326
+ border-radius: 0.25rem;
327
+ font-size: 0.8rem;
328
+ display: flex;
329
+ align-items: center;
330
+ gap: 0.35rem;
331
+ }
332
+ .tool-item:hover {
333
+ background: oklch(var(--b3));
334
+ }
335
+ .tool-item .tool-dot {
336
+ width: 6px;
337
+ height: 6px;
338
+ border-radius: 50%;
339
+ flex-shrink: 0;
340
+ }
341
+ .tool-dot-safe { background: oklch(var(--in)); }
342
+ .tool-dot-write { background: oklch(var(--wa)); }
343
+ .tool-dot-destructive { background: oklch(var(--er)); }
344
+ .tool-dot-connected { background: oklch(var(--su)); }
345
+ .tool-dot-disconnected { background: oklch(var(--er)); }
346
+ .tool-dot-active { background: oklch(var(--su)); }
347
+ .tool-dot-inactive { background: oklch(var(--bc) / 0.3); }
348
+
349
+ .mcp-server-item {
350
+ padding: 0.3rem 0.5rem;
351
+ border-radius: 0.25rem;
352
+ cursor: pointer;
353
+ font-size: 0.8rem;
354
+ }
355
+ .mcp-server-item:hover {
356
+ background: oklch(var(--b3));
357
+ }
358
+ .mcp-server-tools {
359
+ padding-left: 1.25rem;
360
+ font-size: 0.75rem;
361
+ opacity: 0.6;
362
+ }
363
+
283
364
  /* ── Responsive ─────────────────────────────────────── */
284
365
  @media (max-width: 768px) {
285
366
  .sidebar { width: 0; padding: 0; border: none; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",