jinzd-ai-cli 0.1.84 → 0.1.86
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 +103 -6
- package/dist/{chunk-L2PQET5S.js → chunk-PCT4OP6Q.js} +1 -1
- package/dist/{chunk-YHB3S2KS.js → chunk-SMLN357K.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{run-tests-V2VE7ST5.js → run-tests-Y2PN5FYJ.js} +1 -1
- package/dist/{server-XWSMOXVH.js → server-BFNZIL3O.js} +155 -5
- package/dist/web/client/app.js +269 -5
- package/dist/web/client/index.html +22 -5
- package/dist/web/client/style.css +81 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -352,6 +352,103 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
352
352
|
- [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
|
|
353
353
|
- [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
|
|
354
354
|
|
|
355
|
+
## 本轮开发完成记录(2026-03-16,v0.1.85 → v0.1.86)
|
|
356
|
+
|
|
357
|
+
### Web UI P1 增强(续):命令扩展 + 键盘快捷键 + 导出 + 断线重连
|
|
358
|
+
|
|
359
|
+
**P1-3:更多命令支持**
|
|
360
|
+
|
|
361
|
+
| 文件 | 变更类型 | 说明 |
|
|
362
|
+
|------|---------|------|
|
|
363
|
+
| `src/web/protocol.ts` | 修改 | 新增 `S2C_ExportData`(format/filename/content)+ `S2C_MemoryContent` 消息类型 |
|
|
364
|
+
| `src/web/session-handler.ts` | 修改 | 新增 `/export [md\|json]` + `/memory [show\|add\|clear]` 命令 + 3 个实现方法 |
|
|
365
|
+
| `src/web/client/app.js` | 修改 | `handleExportData()` 浏览器 Blob 下载 + `handleMemoryContent()` 卡片渲染 |
|
|
366
|
+
|
|
367
|
+
- `/export md`:格式化 session 消息为 Markdown(标题/元数据/User+Assistant 分段),触发浏览器下载
|
|
368
|
+
- `/export json`:导出结构化 JSON(title/provider/model/messages),触发浏览器下载
|
|
369
|
+
- `/memory`:读取 `~/.aicli/memory.md` 显示为带样式卡片
|
|
370
|
+
- `/memory add <text>`:追加带时间戳的条目
|
|
371
|
+
- `/memory clear`:清空记忆文件
|
|
372
|
+
|
|
373
|
+
**P1-4:键盘快捷键**
|
|
374
|
+
|
|
375
|
+
| 快捷键 | 功能 |
|
|
376
|
+
|--------|------|
|
|
377
|
+
| `Esc` | 停止 AI 生成(发送 abort) |
|
|
378
|
+
| `Ctrl+L` | 清屏(保留欢迎消息) |
|
|
379
|
+
| `Ctrl+K` | 清空输入框 |
|
|
380
|
+
| `↑` / `↓` | 历史消息回溯(最多 50 条,支持命令和聊天输入) |
|
|
381
|
+
|
|
382
|
+
- 输入历史在 `sendMessage()` 中记录,去重 + 50 条上限
|
|
383
|
+
- ↑ 键首次按下保存当前草稿,↓ 键到底恢复草稿
|
|
384
|
+
- 单行输入时 ↑ 触发历史,多行输入时保持原有光标移动
|
|
385
|
+
|
|
386
|
+
**P1-5:Markdown 导出**(合并到 P1-3 的 `/export md` 命令实现)
|
|
387
|
+
|
|
388
|
+
**P1-6:断线重连恢复**
|
|
389
|
+
|
|
390
|
+
| 文件 | 变更类型 | 说明 |
|
|
391
|
+
|------|---------|------|
|
|
392
|
+
| `src/web/client/app.js` | 修改 | 30s 心跳 ping + 指数退避重连(1s→2s→4s→10s)+ 重连后 UI 状态恢复 |
|
|
393
|
+
| `src/web/server.ts` | 修改 | WebSocket message handler 中识别 `ping` 并回复 `pong` |
|
|
394
|
+
|
|
395
|
+
- 心跳:客户端每 30s 发送 `{ type: 'ping' }`,服务器回复 `{ type: 'pong' }`
|
|
396
|
+
- 断线检测:连接关闭后显示 "Disconnected — reconnecting..."
|
|
397
|
+
- 指数退避:重连间隔 1s→2s→4s→8s→10s(上限),连接成功后重置
|
|
398
|
+
- 状态恢复:重连成功后自动请求 session list + tools list,processing 中断时显示提示
|
|
399
|
+
|
|
400
|
+
### 版本与收尾
|
|
401
|
+
- `src/core/constants.ts`:VERSION `0.1.85` → `0.1.86`
|
|
402
|
+
- `package.json`:version 同步
|
|
403
|
+
- 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
|
|
404
|
+
- Web UI 预览测试:/help 新命令显示、/memory 卡片渲染、/export 空 session 提示、心跳状态 全部通过
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 本轮开发完成记录(2026-03-16,v0.1.84 → v0.1.85)
|
|
409
|
+
|
|
410
|
+
### Web UI P1 增强:侧边栏 Tools Tab + Diff 语法高亮
|
|
411
|
+
|
|
412
|
+
**P1-1:侧边栏增强 — Sessions/Tools 双 Tab**
|
|
413
|
+
|
|
414
|
+
将原有单一 session 列表侧边栏升级为双 Tab 布局:
|
|
415
|
+
|
|
416
|
+
| 文件 | 变更类型 | 说明 |
|
|
417
|
+
|------|---------|------|
|
|
418
|
+
| `src/web/protocol.ts` | 修改 | 新增 `S2C_ToolsList` 消息类型(builtinTools/mcpServers/skills 三段数据) |
|
|
419
|
+
| `src/web/session-handler.ts` | 修改 | 新增 `/tools` 命令 + `sendToolsList()` 方法(遍历 ToolRegistry + MCP + Skills) |
|
|
420
|
+
| `src/web/client/index.html` | 修改 | 侧边栏重构为 Tabs(📋 Sessions / 🔧 Tools)+ 搜索框 |
|
|
421
|
+
| `src/web/client/style.css` | 修改 | Tab 激活样式 + 工具列表样式(section 标题/工具项/彩色圆点/MCP 折叠) |
|
|
422
|
+
| `src/web/client/app.js` | 修改 | Tab 切换逻辑 + `renderToolsList()` + `renderFilteredTools()` 搜索过滤 |
|
|
423
|
+
|
|
424
|
+
- **Built-in Tools**:显示所有内置工具,带危险级别彩色圆点(蓝=safe, 黄=write, 红=destructive)
|
|
425
|
+
- **MCP Servers**:显示连接状态(绿=connected)+ 工具数量 badge,可折叠展开查看工具列表
|
|
426
|
+
- **Skills**:显示所有可用技能,绿色=active,灰色=inactive
|
|
427
|
+
- **搜索过滤**:实时过滤工具名和描述,空 section 自动隐藏
|
|
428
|
+
- **Session 搜索**:Sessions tab 新增搜索框,按标题过滤
|
|
429
|
+
|
|
430
|
+
**P1-2:Diff 渲染增强 — 语法高亮**
|
|
431
|
+
|
|
432
|
+
工具确认对话框中的 diff 预览从纯文本升级为语法高亮:
|
|
433
|
+
|
|
434
|
+
| 文件 | 变更类型 | 说明 |
|
|
435
|
+
|------|---------|------|
|
|
436
|
+
| `src/web/client/style.css` | 修改 | 新增 `.diff-add`/`.diff-del`/`.diff-hunk`/`.diff-ctx` 四种行样式 |
|
|
437
|
+
| `src/web/client/app.js` | 修改 | 新增 `renderDiffHtml()` 函数,按行首字符(+/-/@@)分类着色 |
|
|
438
|
+
|
|
439
|
+
- `+` 增加行:绿色文字 + 绿色半透明背景
|
|
440
|
+
- `-` 删除行:红色文字 + 红色半透明背景
|
|
441
|
+
- `@@` Hunk header:主题色(紫色),加粗
|
|
442
|
+
- 其他上下文行:降低不透明度
|
|
443
|
+
|
|
444
|
+
### 版本与收尾
|
|
445
|
+
- `src/core/constants.ts`:VERSION `0.1.84` → `0.1.85`
|
|
446
|
+
- `package.json`:version 同步
|
|
447
|
+
- 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
|
|
448
|
+
- Web UI 预览测试:侧边栏 Tab 切换、工具列表渲染、搜索过滤、MCP/Skills 显示 全部通过
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
355
452
|
## 本轮开发完成记录(2026-03-16,v0.1.82 → v0.1.83)
|
|
356
453
|
|
|
357
454
|
### 新增功能:OpenRouter 内置 Provider
|
|
@@ -1932,12 +2029,12 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
1932
2029
|
|
|
1933
2030
|
| # | 功能 | 状态 | 说明 |
|
|
1934
2031
|
|---|------|------|------|
|
|
1935
|
-
| P1-1 | **侧边栏增强** | [x] |
|
|
1936
|
-
| P1-2 | **Diff 渲染增强** | [
|
|
1937
|
-
| P1-3 | **更多命令支持** | [
|
|
1938
|
-
| P1-4 | **键盘快捷键** | [
|
|
1939
|
-
| P1-5 | **Markdown 导出** | [
|
|
1940
|
-
| P1-6 | **断线重连恢复** | [
|
|
2032
|
+
| P1-1 | **侧边栏增强** | [x] | Sessions/Tools 双 Tab + Built-in Tools(危险级别圆点)+ MCP Servers(连接状态+工具数)+ Skills 列表 + 搜索过滤 |
|
|
2033
|
+
| P1-2 | **Diff 渲染增强** | [x] | confirm 对话框 diff 语法高亮(绿色增行、红色删行、紫色 hunk header、灰色上下文) |
|
|
2034
|
+
| P1-3 | **更多命令支持** | [x] | `/export [md\|json]`(浏览器下载)、`/memory [show\|add\|clear]`、`/tools`、`/cost`、`/session` |
|
|
2035
|
+
| P1-4 | **键盘快捷键** | [x] | `Ctrl+L` 清屏、`Ctrl+K` 清空输入、`Esc` 停止生成、`↑↓` 历史消息回溯(50 条) |
|
|
2036
|
+
| P1-5 | **Markdown 导出** | [x] | `/export md` 一键导出对话为 `.md` 文件浏览器下载、`/export json` 导出 JSON |
|
|
2037
|
+
| P1-6 | **断线重连恢复** | [x] | 30s 心跳 ping/pong、指数退避重连(1s→2s→4s→10s)、重连后状态恢复提示 |
|
|
1941
2038
|
|
|
1942
2039
|
### P2 — 差异化功能(Web 独有优势)
|
|
1943
2040
|
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
theme,
|
|
36
36
|
truncateOutput,
|
|
37
37
|
undoStack
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-PCT4OP6Q.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-
|
|
58
|
+
} from "./chunk-SMLN357K.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-
|
|
1907
|
+
const { executeTests } = await import("./run-tests-Y2PN5FYJ.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-
|
|
5295
|
+
const { startWebServer } = await import("./server-BFNZIL3O.js");
|
|
5296
5296
|
await startWebServer({ port, host: options.host });
|
|
5297
5297
|
});
|
|
5298
5298
|
program.command("sessions").description("List recent conversation sessions").action(async () => {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
setupProxy,
|
|
24
24
|
spawnAgentContext,
|
|
25
25
|
truncateOutput
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-PCT4OP6Q.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-
|
|
39
|
+
} from "./chunk-SMLN357K.js";
|
|
40
40
|
|
|
41
41
|
// src/web/server.ts
|
|
42
42
|
import express from "express";
|
|
@@ -390,7 +390,7 @@ function loadMemoryContent(configDir) {
|
|
|
390
390
|
}
|
|
391
391
|
|
|
392
392
|
// src/web/session-handler.ts
|
|
393
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
393
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, appendFileSync, writeFileSync, mkdirSync } from "fs";
|
|
394
394
|
import { join as join2, resolve } from "path";
|
|
395
395
|
var MAX_TOOL_ROUNDS = 25;
|
|
396
396
|
var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
|
|
@@ -961,6 +961,11 @@ 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",
|
|
965
|
+
" /export [md|json] \u2014 Export conversation as Markdown or JSON",
|
|
966
|
+
" /memory \u2014 Show persistent memory contents",
|
|
967
|
+
" /memory add <text> \u2014 Add entry to persistent memory",
|
|
968
|
+
" /memory clear \u2014 Clear persistent memory",
|
|
964
969
|
" /help \u2014 Show this help message",
|
|
965
970
|
"",
|
|
966
971
|
"\u{1F4A1} Tips:",
|
|
@@ -968,7 +973,9 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
968
973
|
" \u2022 Drag & drop or Ctrl+V to paste images",
|
|
969
974
|
" \u2022 Type @ to reference files from your project",
|
|
970
975
|
" \u2022 Use Shift+Enter for multi-line input",
|
|
971
|
-
" \u2022 During AI processing, type to send corrections"
|
|
976
|
+
" \u2022 During AI processing, type to send corrections",
|
|
977
|
+
" \u2022 Ctrl+L to clear screen, Esc to stop generation",
|
|
978
|
+
" \u2022 \u2191/\u2193 arrow keys to recall previous messages"
|
|
972
979
|
].join("\n")
|
|
973
980
|
});
|
|
974
981
|
break;
|
|
@@ -985,6 +992,25 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
985
992
|
});
|
|
986
993
|
break;
|
|
987
994
|
}
|
|
995
|
+
case "tools":
|
|
996
|
+
this.sendToolsList();
|
|
997
|
+
break;
|
|
998
|
+
case "export": {
|
|
999
|
+
const format = args[0] === "json" ? "json" : "md";
|
|
1000
|
+
this.exportConversation(format);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
case "memory": {
|
|
1004
|
+
const sub = args[0];
|
|
1005
|
+
if (sub === "add" && args.length > 1) {
|
|
1006
|
+
this.memoryAdd(args.slice(1).join(" "));
|
|
1007
|
+
} else if (sub === "clear") {
|
|
1008
|
+
this.memoryClear();
|
|
1009
|
+
} else {
|
|
1010
|
+
this.memoryShow();
|
|
1011
|
+
}
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
988
1014
|
default:
|
|
989
1015
|
this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
|
|
990
1016
|
}
|
|
@@ -1036,6 +1062,122 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1036
1062
|
}))
|
|
1037
1063
|
});
|
|
1038
1064
|
}
|
|
1065
|
+
sendToolsList() {
|
|
1066
|
+
const allDefs = this.toolRegistry.getDefinitions();
|
|
1067
|
+
const builtinTools = allDefs.filter((d) => !d.name.startsWith("mcp__")).map((d) => ({
|
|
1068
|
+
name: d.name,
|
|
1069
|
+
description: d.description,
|
|
1070
|
+
dangerLevel: getDangerLevel(d.name, {})
|
|
1071
|
+
}));
|
|
1072
|
+
const mcpServers = [];
|
|
1073
|
+
if (this.mcpManager) {
|
|
1074
|
+
for (const status of this.mcpManager.getStatus()) {
|
|
1075
|
+
const serverTools = allDefs.filter((d) => d.name.startsWith(`mcp__${status.serverId}__`)).map((d) => d.name.replace(`mcp__${status.serverId}__`, ""));
|
|
1076
|
+
mcpServers.push({
|
|
1077
|
+
serverId: status.serverId,
|
|
1078
|
+
serverName: status.serverName,
|
|
1079
|
+
toolCount: status.toolCount,
|
|
1080
|
+
connected: status.connected,
|
|
1081
|
+
tools: serverTools
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const skills = (this.skillManager?.listSkills() ?? []).map((s) => ({
|
|
1086
|
+
name: s.meta.name,
|
|
1087
|
+
description: s.meta.description,
|
|
1088
|
+
isActive: this.skillManager?.getActive()?.meta.name === s.meta.name
|
|
1089
|
+
}));
|
|
1090
|
+
this.send({
|
|
1091
|
+
type: "tools_list",
|
|
1092
|
+
builtinTools,
|
|
1093
|
+
mcpServers,
|
|
1094
|
+
skills
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
exportConversation(format) {
|
|
1098
|
+
const session = this.sessions.current;
|
|
1099
|
+
if (!session || session.messages.length === 0) {
|
|
1100
|
+
this.send({ type: "info", message: "No messages to export." });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const title = session.title ?? "Untitled";
|
|
1104
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1105
|
+
if (format === "json") {
|
|
1106
|
+
const data = {
|
|
1107
|
+
title,
|
|
1108
|
+
provider: this.currentProvider,
|
|
1109
|
+
model: this.currentModel,
|
|
1110
|
+
exported: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1111
|
+
messages: session.messages.map((m) => ({
|
|
1112
|
+
role: m.role,
|
|
1113
|
+
content: getContentText(m.content),
|
|
1114
|
+
timestamp: m.timestamp?.toISOString()
|
|
1115
|
+
}))
|
|
1116
|
+
};
|
|
1117
|
+
this.send({
|
|
1118
|
+
type: "export_data",
|
|
1119
|
+
format: "json",
|
|
1120
|
+
filename: `aicli-${timestamp}.json`,
|
|
1121
|
+
content: JSON.stringify(data, null, 2)
|
|
1122
|
+
});
|
|
1123
|
+
} else {
|
|
1124
|
+
const lines = [`# ${title}`, "", `> Exported: ${(/* @__PURE__ */ new Date()).toLocaleString()} | Provider: ${this.currentProvider} | Model: ${this.currentModel}`, ""];
|
|
1125
|
+
for (const m of session.messages) {
|
|
1126
|
+
const text = getContentText(m.content);
|
|
1127
|
+
if (m.role === "user") {
|
|
1128
|
+
lines.push(`## \u{1F9D1} User`, "", text, "");
|
|
1129
|
+
} else if (m.role === "assistant") {
|
|
1130
|
+
lines.push(`## \u{1F916} Assistant`, "", text, "");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
this.send({
|
|
1134
|
+
type: "export_data",
|
|
1135
|
+
format: "md",
|
|
1136
|
+
filename: `aicli-${timestamp}.md`,
|
|
1137
|
+
content: lines.join("\n")
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
memoryShow() {
|
|
1142
|
+
const configDir = this.config.getConfigDir();
|
|
1143
|
+
const memPath = join2(configDir, MEMORY_FILE_NAME);
|
|
1144
|
+
let content = "";
|
|
1145
|
+
try {
|
|
1146
|
+
if (existsSync3(memPath)) {
|
|
1147
|
+
content = readFileSync3(memPath, "utf-8");
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
this.send({
|
|
1152
|
+
type: "memory_content",
|
|
1153
|
+
content: content || "(empty \u2014 no persistent memory entries yet)",
|
|
1154
|
+
filePath: memPath
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
memoryAdd(text) {
|
|
1158
|
+
const configDir = this.config.getConfigDir();
|
|
1159
|
+
const memPath = join2(configDir, MEMORY_FILE_NAME);
|
|
1160
|
+
try {
|
|
1161
|
+
mkdirSync(configDir, { recursive: true });
|
|
1162
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
|
|
1163
|
+
appendFileSync(memPath, `
|
|
1164
|
+
- [${timestamp}] ${text}
|
|
1165
|
+
`, "utf-8");
|
|
1166
|
+
this.send({ type: "info", message: `\u{1F4DD} Memory entry added: "${text}"` });
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
this.send({ type: "error", message: `Failed to write memory: ${err.message}` });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
memoryClear() {
|
|
1172
|
+
const configDir = this.config.getConfigDir();
|
|
1173
|
+
const memPath = join2(configDir, MEMORY_FILE_NAME);
|
|
1174
|
+
try {
|
|
1175
|
+
writeFileSync(memPath, "", "utf-8");
|
|
1176
|
+
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
this.send({ type: "error", message: `Failed to clear memory: ${err.message}` });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1039
1181
|
sendSessionMessages() {
|
|
1040
1182
|
const session = this.sessions.current;
|
|
1041
1183
|
if (!session) return;
|
|
@@ -1293,7 +1435,15 @@ async function startWebServer(options = {}) {
|
|
|
1293
1435
|
activeHandler = handler;
|
|
1294
1436
|
ws.on("message", async (data) => {
|
|
1295
1437
|
try {
|
|
1296
|
-
|
|
1438
|
+
const raw = data.toString();
|
|
1439
|
+
const parsed = JSON.parse(raw);
|
|
1440
|
+
if (parsed.type === "ping") {
|
|
1441
|
+
if (ws.readyState === ws.OPEN) {
|
|
1442
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
await handler.handleMessage(raw);
|
|
1297
1447
|
} catch (err) {
|
|
1298
1448
|
const message = err instanceof Error ? err.message : String(err);
|
|
1299
1449
|
console.error(` Error: ${message}`);
|
package/dist/web/client/app.js
CHANGED
|
@@ -14,6 +14,9 @@ let currentThinkingEl = null;
|
|
|
14
14
|
let currentThinkingContent = '';
|
|
15
15
|
let providers = [];
|
|
16
16
|
let pendingImages = []; // { name, data (base64), mime }
|
|
17
|
+
let inputHistory = []; // Previous user inputs for ↑/↓ navigation
|
|
18
|
+
let historyIndex = -1; // -1 = not browsing history
|
|
19
|
+
let savedInputDraft = ''; // Saved current input when entering history mode
|
|
17
20
|
|
|
18
21
|
// ── DOM refs ───────────────────────────────────────────────────────
|
|
19
22
|
|
|
@@ -34,6 +37,11 @@ const connectionStatus = document.getElementById('connection-status');
|
|
|
34
37
|
const sidebar = document.getElementById('sidebar');
|
|
35
38
|
const sessionListEl = document.getElementById('session-list');
|
|
36
39
|
const btnNewSession = document.getElementById('btn-new-session');
|
|
40
|
+
const toolsListEl = document.getElementById('tools-list');
|
|
41
|
+
const sessionSearchInput = document.getElementById('session-search');
|
|
42
|
+
const toolsSearchInput = document.getElementById('tools-search');
|
|
43
|
+
let cachedSessions = [];
|
|
44
|
+
let cachedToolsData = null;
|
|
37
45
|
|
|
38
46
|
// ── Configure marked.js ────────────────────────────────────────────
|
|
39
47
|
|
|
@@ -51,22 +59,41 @@ marked.setOptions({
|
|
|
51
59
|
|
|
52
60
|
// ── WebSocket ──────────────────────────────────────────────────────
|
|
53
61
|
|
|
62
|
+
let heartbeatTimer = null;
|
|
63
|
+
let reconnectDelay = 1000; // Start at 1s, exponential backoff
|
|
64
|
+
|
|
54
65
|
function connect() {
|
|
55
66
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
56
67
|
ws = new WebSocket(`${protocol}//${location.host}`);
|
|
57
68
|
|
|
58
69
|
ws.onopen = () => {
|
|
59
70
|
connected = true;
|
|
71
|
+
reconnectDelay = 1000; // Reset backoff on success
|
|
60
72
|
connectionStatus.textContent = '🟢 Connected';
|
|
61
73
|
connectionStatus.className = 'status-connected';
|
|
62
74
|
requestSessionList();
|
|
75
|
+
// Start heartbeat ping every 30s
|
|
76
|
+
clearInterval(heartbeatTimer);
|
|
77
|
+
heartbeatTimer = setInterval(() => {
|
|
78
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
79
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
80
|
+
}
|
|
81
|
+
}, 30000);
|
|
82
|
+
// If we had a session & were processing, reset UI state
|
|
83
|
+
if (processing) {
|
|
84
|
+
setProcessing(false);
|
|
85
|
+
addInfoMessage('⚡ Reconnected — previous generation may have been interrupted.');
|
|
86
|
+
}
|
|
63
87
|
};
|
|
64
88
|
|
|
65
89
|
ws.onclose = () => {
|
|
66
90
|
connected = false;
|
|
67
|
-
|
|
91
|
+
clearInterval(heartbeatTimer);
|
|
92
|
+
connectionStatus.textContent = '🔴 Disconnected — reconnecting...';
|
|
68
93
|
connectionStatus.className = 'status-disconnected';
|
|
69
|
-
|
|
94
|
+
// Exponential backoff: 1s, 2s, 4s, max 10s
|
|
95
|
+
setTimeout(connect, reconnectDelay);
|
|
96
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 10000);
|
|
70
97
|
};
|
|
71
98
|
|
|
72
99
|
ws.onerror = () => {
|
|
@@ -76,7 +103,9 @@ function connect() {
|
|
|
76
103
|
|
|
77
104
|
ws.onmessage = (event) => {
|
|
78
105
|
try {
|
|
79
|
-
|
|
106
|
+
const msg = JSON.parse(event.data);
|
|
107
|
+
if (msg.type === 'pong') return; // Heartbeat response, ignore
|
|
108
|
+
handleServerMessage(msg);
|
|
80
109
|
} catch (e) {
|
|
81
110
|
console.error('Failed to parse message:', e);
|
|
82
111
|
}
|
|
@@ -107,6 +136,9 @@ function handleServerMessage(msg) {
|
|
|
107
136
|
case 'status': handleStatus(msg); break;
|
|
108
137
|
case 'session_list': renderSessionList(msg.sessions); break;
|
|
109
138
|
case 'session_messages':renderSessionMessages(msg.messages); break;
|
|
139
|
+
case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
|
|
140
|
+
case 'export_data': handleExportData(msg); break;
|
|
141
|
+
case 'memory_content': handleMemoryContent(msg); break;
|
|
110
142
|
case 'info': addInfoMessage(msg.message); break;
|
|
111
143
|
case 'error': addErrorMessage(msg.message); setProcessing(false); break;
|
|
112
144
|
case 'round_progress': break;
|
|
@@ -205,7 +237,7 @@ function handleConfirmRequest(msg) {
|
|
|
205
237
|
<span class="badge ${isDestructive ? 'badge-error' : 'badge-warning'} badge-sm">${isDestructive ? '⚠ DESTRUCTIVE' : '✎ Write'}</span>
|
|
206
238
|
<span class="text-sm font-semibold">${escapeHtml(msg.toolName)}</span>
|
|
207
239
|
</div>
|
|
208
|
-
${msg.diff ? `<div class="confirm-diff w-full">${
|
|
240
|
+
${msg.diff ? `<div class="confirm-diff w-full">${renderDiffHtml(msg.diff)}</div>` : ''}
|
|
209
241
|
<div class="flex gap-2 mt-2">
|
|
210
242
|
<button class="btn btn-success btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', true)">✓ Approve</button>
|
|
211
243
|
<button class="btn btn-error btn-sm btn-outline" onclick="respondConfirm('${msg.requestId}', false)">✗ Deny</button>
|
|
@@ -513,6 +545,12 @@ async function sendMessage() {
|
|
|
513
545
|
const args = parts.slice(1);
|
|
514
546
|
send({ type: 'command', name, args });
|
|
515
547
|
addInfoMessage(`/${text.slice(1)}`);
|
|
548
|
+
// Track command in history
|
|
549
|
+
if (inputHistory[inputHistory.length - 1] !== text) {
|
|
550
|
+
inputHistory.push(text);
|
|
551
|
+
if (inputHistory.length > 50) inputHistory.shift();
|
|
552
|
+
}
|
|
553
|
+
historyIndex = -1;
|
|
516
554
|
userInput.value = '';
|
|
517
555
|
userInput.style.height = 'auto';
|
|
518
556
|
return;
|
|
@@ -539,6 +577,14 @@ async function sendMessage() {
|
|
|
539
577
|
}
|
|
540
578
|
}
|
|
541
579
|
|
|
580
|
+
// Track input history
|
|
581
|
+
if (inputHistory[inputHistory.length - 1] !== text) {
|
|
582
|
+
inputHistory.push(text);
|
|
583
|
+
if (inputHistory.length > 50) inputHistory.shift();
|
|
584
|
+
}
|
|
585
|
+
historyIndex = -1;
|
|
586
|
+
savedInputDraft = '';
|
|
587
|
+
|
|
542
588
|
addUserMessage(text, pendingImages);
|
|
543
589
|
const msg = { type: 'chat', content: resolvedText };
|
|
544
590
|
if (pendingImages.length > 0) {
|
|
@@ -591,8 +637,16 @@ modelSelect.addEventListener('change', () => {
|
|
|
591
637
|
// ── Session management ──────────────────────────────────────────────
|
|
592
638
|
|
|
593
639
|
function renderSessionList(sessions) {
|
|
640
|
+
cachedSessions = sessions || [];
|
|
641
|
+
renderFilteredSessions(sessionSearchInput?.value || '');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function renderFilteredSessions(filter) {
|
|
645
|
+
const sessions = filter
|
|
646
|
+
? cachedSessions.filter(s => (s.title || '').toLowerCase().includes(filter.toLowerCase()))
|
|
647
|
+
: cachedSessions;
|
|
594
648
|
if (!sessions || sessions.length === 0) {
|
|
595
|
-
sessionListEl.innerHTML =
|
|
649
|
+
sessionListEl.innerHTML = `<div class="text-xs opacity-40 text-center py-4">${filter ? 'No matches' : 'No sessions yet'}</div>`;
|
|
596
650
|
return;
|
|
597
651
|
}
|
|
598
652
|
sessionListEl.innerHTML = sessions.map(s => {
|
|
@@ -656,6 +710,122 @@ btnNewSession.addEventListener('click', () => {
|
|
|
656
710
|
// Request session list on connect
|
|
657
711
|
function requestSessionList() {
|
|
658
712
|
send({ type: 'command', name: 'session', args: ['list'] });
|
|
713
|
+
// Also request tools list for sidebar
|
|
714
|
+
send({ type: 'command', name: 'tools', args: [] });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Session search filter
|
|
718
|
+
if (sessionSearchInput) {
|
|
719
|
+
sessionSearchInput.addEventListener('input', () => {
|
|
720
|
+
renderFilteredSessions(sessionSearchInput.value);
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ── Sidebar tabs ──────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
function switchSidebarTab(tabName) {
|
|
727
|
+
document.querySelectorAll('.sidebar-tab').forEach(btn => {
|
|
728
|
+
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
729
|
+
});
|
|
730
|
+
document.querySelectorAll('.sidebar-tab-content').forEach(el => {
|
|
731
|
+
el.classList.toggle('hidden', el.id !== `tab-${tabName}`);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
document.querySelectorAll('.sidebar-tab').forEach(btn => {
|
|
736
|
+
btn.addEventListener('click', () => switchSidebarTab(btn.dataset.tab));
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// ── Tools list rendering ──────────────────────────────────────────────
|
|
740
|
+
|
|
741
|
+
function renderToolsList(data) {
|
|
742
|
+
cachedToolsData = data;
|
|
743
|
+
renderFilteredTools(toolsSearchInput?.value || '');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function renderFilteredTools(filter) {
|
|
747
|
+
if (!cachedToolsData) {
|
|
748
|
+
toolsListEl.innerHTML = '<div class="text-xs opacity-40 text-center py-4">Type /tools to load</div>';
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const fl = filter.toLowerCase();
|
|
752
|
+
let html = '';
|
|
753
|
+
|
|
754
|
+
// Built-in tools
|
|
755
|
+
const tools = fl
|
|
756
|
+
? cachedToolsData.builtinTools.filter(t => t.name.includes(fl) || t.description.toLowerCase().includes(fl))
|
|
757
|
+
: cachedToolsData.builtinTools;
|
|
758
|
+
if (tools.length > 0) {
|
|
759
|
+
html += '<div class="tools-section-title">Built-in Tools</div>';
|
|
760
|
+
html += tools.map(t =>
|
|
761
|
+
`<div class="tool-item" title="${escapeHtml(t.description)}">
|
|
762
|
+
<span class="tool-dot tool-dot-${t.dangerLevel}"></span>
|
|
763
|
+
<span class="flex-1 truncate">${escapeHtml(t.name)}</span>
|
|
764
|
+
</div>`
|
|
765
|
+
).join('');
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// MCP servers
|
|
769
|
+
const servers = fl
|
|
770
|
+
? cachedToolsData.mcpServers.filter(s =>
|
|
771
|
+
s.serverId.includes(fl) || s.serverName.toLowerCase().includes(fl) ||
|
|
772
|
+
s.tools.some(t => t.includes(fl)))
|
|
773
|
+
: cachedToolsData.mcpServers;
|
|
774
|
+
if (servers.length > 0) {
|
|
775
|
+
html += '<div class="tools-section-title mt-2">MCP Servers</div>';
|
|
776
|
+
for (const s of servers) {
|
|
777
|
+
const dotClass = s.connected ? 'tool-dot-connected' : 'tool-dot-disconnected';
|
|
778
|
+
html += `<details class="mcp-server-item">
|
|
779
|
+
<summary class="flex items-center gap-2 cursor-pointer select-none">
|
|
780
|
+
<span class="tool-dot ${dotClass}"></span>
|
|
781
|
+
<span class="flex-1 truncate">${escapeHtml(s.serverId)}</span>
|
|
782
|
+
<span class="text-xs opacity-40">${s.toolCount}</span>
|
|
783
|
+
</summary>
|
|
784
|
+
<div class="mcp-server-tools">
|
|
785
|
+
${s.tools.map(t => `<div class="py-0.5">${escapeHtml(t)}</div>`).join('')}
|
|
786
|
+
</div>
|
|
787
|
+
</details>`;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Skills
|
|
792
|
+
const skills = fl
|
|
793
|
+
? cachedToolsData.skills.filter(s => s.name.includes(fl) || s.description.toLowerCase().includes(fl))
|
|
794
|
+
: cachedToolsData.skills;
|
|
795
|
+
if (skills.length > 0) {
|
|
796
|
+
html += '<div class="tools-section-title mt-2">Skills</div>';
|
|
797
|
+
html += skills.map(s =>
|
|
798
|
+
`<div class="tool-item" title="${escapeHtml(s.description)}">
|
|
799
|
+
<span class="tool-dot ${s.isActive ? 'tool-dot-active' : 'tool-dot-inactive'}"></span>
|
|
800
|
+
<span class="flex-1 truncate">${escapeHtml(s.name)}</span>
|
|
801
|
+
${s.isActive ? '<span class="badge badge-success badge-xs">active</span>' : ''}
|
|
802
|
+
</div>`
|
|
803
|
+
).join('');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (!html) {
|
|
807
|
+
html = '<div class="text-xs opacity-40 text-center py-4">No matches</div>';
|
|
808
|
+
}
|
|
809
|
+
toolsListEl.innerHTML = html;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Tools search filter
|
|
813
|
+
if (toolsSearchInput) {
|
|
814
|
+
toolsSearchInput.addEventListener('input', () => {
|
|
815
|
+
renderFilteredTools(toolsSearchInput.value);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ── Diff syntax highlighting ──────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
function renderDiffHtml(diffText) {
|
|
822
|
+
return diffText.split('\n').map(line => {
|
|
823
|
+
const escaped = escapeHtml(line);
|
|
824
|
+
if (line.startsWith('@@')) return `<div class="diff-hunk">${escaped}</div>`;
|
|
825
|
+
if (line.startsWith('+')) return `<div class="diff-add">${escaped}</div>`;
|
|
826
|
+
if (line.startsWith('-')) return `<div class="diff-del">${escaped}</div>`;
|
|
827
|
+
return `<div class="diff-ctx">${escaped}</div>`;
|
|
828
|
+
}).join('');
|
|
659
829
|
}
|
|
660
830
|
|
|
661
831
|
// ── @ File reference autocomplete ───────────────────────────────────
|
|
@@ -806,6 +976,72 @@ userInput.addEventListener('keydown', (e) => {
|
|
|
806
976
|
}
|
|
807
977
|
}
|
|
808
978
|
|
|
979
|
+
// ── Keyboard shortcuts ───────────────────────────────────────
|
|
980
|
+
|
|
981
|
+
// Escape: stop generation
|
|
982
|
+
if (e.key === 'Escape') {
|
|
983
|
+
if (processing) {
|
|
984
|
+
e.preventDefault();
|
|
985
|
+
send({ type: 'abort' });
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Ctrl+L: clear screen (keep welcome)
|
|
991
|
+
if (e.key === 'l' && (e.ctrlKey || e.metaKey)) {
|
|
992
|
+
e.preventDefault();
|
|
993
|
+
const welcome = messagesEl.querySelector('.chat');
|
|
994
|
+
messagesEl.innerHTML = '';
|
|
995
|
+
if (welcome) messagesEl.appendChild(welcome);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Ctrl+K: clear input
|
|
1000
|
+
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
|
|
1001
|
+
e.preventDefault();
|
|
1002
|
+
userInput.value = '';
|
|
1003
|
+
userInput.style.height = 'auto';
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ↑/↓: input history navigation (only when input is empty or browsing history, single-line)
|
|
1008
|
+
if (e.key === 'ArrowUp' && !e.shiftKey) {
|
|
1009
|
+
const isAtStart = userInput.selectionStart === 0 && userInput.selectionEnd === 0;
|
|
1010
|
+
const isSingleLine = !userInput.value.includes('\n');
|
|
1011
|
+
if ((isSingleLine || userInput.value === '') && (isAtStart || userInput.value === '')) {
|
|
1012
|
+
if (inputHistory.length > 0) {
|
|
1013
|
+
e.preventDefault();
|
|
1014
|
+
if (historyIndex === -1) {
|
|
1015
|
+
savedInputDraft = userInput.value;
|
|
1016
|
+
historyIndex = inputHistory.length - 1;
|
|
1017
|
+
} else if (historyIndex > 0) {
|
|
1018
|
+
historyIndex--;
|
|
1019
|
+
}
|
|
1020
|
+
userInput.value = inputHistory[historyIndex] || '';
|
|
1021
|
+
userInput.style.height = 'auto';
|
|
1022
|
+
userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (e.key === 'ArrowDown' && !e.shiftKey) {
|
|
1029
|
+
if (historyIndex >= 0) {
|
|
1030
|
+
e.preventDefault();
|
|
1031
|
+
if (historyIndex < inputHistory.length - 1) {
|
|
1032
|
+
historyIndex++;
|
|
1033
|
+
userInput.value = inputHistory[historyIndex] || '';
|
|
1034
|
+
} else {
|
|
1035
|
+
historyIndex = -1;
|
|
1036
|
+
userInput.value = savedInputDraft;
|
|
1037
|
+
}
|
|
1038
|
+
userInput.style.height = 'auto';
|
|
1039
|
+
userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Enter: send message
|
|
809
1045
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
810
1046
|
e.preventDefault();
|
|
811
1047
|
sendMessage();
|
|
@@ -885,6 +1121,34 @@ userInput.addEventListener('paste', (e) => {
|
|
|
885
1121
|
}
|
|
886
1122
|
});
|
|
887
1123
|
|
|
1124
|
+
// ── Export & Memory handlers ────────────────────────────────────────
|
|
1125
|
+
|
|
1126
|
+
function handleExportData(msg) {
|
|
1127
|
+
// Trigger browser download
|
|
1128
|
+
const blob = new Blob([msg.content], { type: msg.format === 'json' ? 'application/json' : 'text/markdown' });
|
|
1129
|
+
const url = URL.createObjectURL(blob);
|
|
1130
|
+
const a = document.createElement('a');
|
|
1131
|
+
a.href = url;
|
|
1132
|
+
a.download = msg.filename;
|
|
1133
|
+
document.body.appendChild(a);
|
|
1134
|
+
a.click();
|
|
1135
|
+
document.body.removeChild(a);
|
|
1136
|
+
URL.revokeObjectURL(url);
|
|
1137
|
+
addInfoMessage(`📥 Exported: ${msg.filename}`);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function handleMemoryContent(msg) {
|
|
1141
|
+
const el = document.createElement('div');
|
|
1142
|
+
el.className = 'msg-assistant my-2';
|
|
1143
|
+
el.innerHTML = `
|
|
1144
|
+
<div class="text-xs opacity-50 mb-1">📝 Persistent Memory</div>
|
|
1145
|
+
<pre class="text-sm whitespace-pre-wrap opacity-80">${escapeHtml(msg.content)}</pre>
|
|
1146
|
+
<div class="text-xs opacity-30 mt-1">${escapeHtml(msg.filePath)}</div>
|
|
1147
|
+
`;
|
|
1148
|
+
messagesEl.appendChild(el);
|
|
1149
|
+
scrollToBottom();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
888
1152
|
// ── Initialize ─────────────────────────────────────────────────────
|
|
889
1153
|
|
|
890
1154
|
// Restore theme
|
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
<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
|
-
|
|
58
|
-
|
|
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; }
|