jinzd-ai-cli 0.1.85 → 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 CHANGED
@@ -352,6 +352,59 @@ 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
+
355
408
  ## 本轮开发完成记录(2026-03-16,v0.1.84 → v0.1.85)
356
409
 
357
410
  ### Web UI P1 增强:侧边栏 Tools Tab + Diff 语法高亮
@@ -1978,10 +2031,10 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
1978
2031
  |---|------|------|------|
1979
2032
  | P1-1 | **侧边栏增强** | [x] | Sessions/Tools 双 Tab + Built-in Tools(危险级别圆点)+ MCP Servers(连接状态+工具数)+ Skills 列表 + 搜索过滤 |
1980
2033
  | P1-2 | **Diff 渲染增强** | [x] | confirm 对话框 diff 语法高亮(绿色增行、红色删行、紫色 hunk header、灰色上下文) |
1981
- | P1-3 | **更多命令支持** | [~] | `/cost`、`/session list\|load\|delete` 已实现;待增加 `/undo`、`/export [md\|json]`、`/tools`、`/memory` |
1982
- | P1-4 | **键盘快捷键** | [ ] | `Ctrl+L` 清屏、`Ctrl+K` 清空输入、`Esc` 停止生成、`↑` 历史消息回溯 |
1983
- | P1-5 | **Markdown 导出** | [ ] | 一键导出当前对话为 `.md` 文件下载 |
1984
- | P1-6 | **断线重连恢复** | [ ] | WebSocket 重连后恢复 session 状态(当前重连=全丢);心跳 ping/pong |
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)、重连后状态恢复提示 |
1985
2038
 
1986
2039
  ### P2 — 差异化功能(Web 独有优势)
1987
2040
 
@@ -16,7 +16,7 @@ import {
16
16
  SUBAGENT_MAX_ROUNDS_LIMIT,
17
17
  VERSION,
18
18
  runTestsTool
19
- } from "./chunk-VR7VECPG.js";
19
+ } from "./chunk-SMLN357K.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.85";
17
+ var VERSION = "0.1.86";
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-QHZGVP5X.js";
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-VR7VECPG.js";
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-IFXCV4UW.js");
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-2LECXHO6.js");
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 () => {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-VR7VECPG.js";
5
+ } from "./chunk-SMLN357K.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-QHZGVP5X.js";
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-VR7VECPG.js";
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"]);
@@ -962,6 +962,10 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
962
962
  " /status \u2014 Show session info & token usage",
963
963
  " /cost \u2014 Show cumulative token usage",
964
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",
965
969
  " /help \u2014 Show this help message",
966
970
  "",
967
971
  "\u{1F4A1} Tips:",
@@ -969,7 +973,9 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
969
973
  " \u2022 Drag & drop or Ctrl+V to paste images",
970
974
  " \u2022 Type @ to reference files from your project",
971
975
  " \u2022 Use Shift+Enter for multi-line input",
972
- " \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"
973
979
  ].join("\n")
974
980
  });
975
981
  break;
@@ -989,6 +995,22 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
989
995
  case "tools":
990
996
  this.sendToolsList();
991
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
+ }
992
1014
  default:
993
1015
  this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
994
1016
  }
@@ -1072,6 +1094,90 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
1072
1094
  skills
1073
1095
  });
1074
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
+ }
1075
1181
  sendSessionMessages() {
1076
1182
  const session = this.sessions.current;
1077
1183
  if (!session) return;
@@ -1329,7 +1435,15 @@ async function startWebServer(options = {}) {
1329
1435
  activeHandler = handler;
1330
1436
  ws.on("message", async (data) => {
1331
1437
  try {
1332
- await handler.handleMessage(data.toString());
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);
1333
1447
  } catch (err) {
1334
1448
  const message = err instanceof Error ? err.message : String(err);
1335
1449
  console.error(` Error: ${message}`);
@@ -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
 
@@ -56,22 +59,41 @@ marked.setOptions({
56
59
 
57
60
  // ── WebSocket ──────────────────────────────────────────────────────
58
61
 
62
+ let heartbeatTimer = null;
63
+ let reconnectDelay = 1000; // Start at 1s, exponential backoff
64
+
59
65
  function connect() {
60
66
  const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
61
67
  ws = new WebSocket(`${protocol}//${location.host}`);
62
68
 
63
69
  ws.onopen = () => {
64
70
  connected = true;
71
+ reconnectDelay = 1000; // Reset backoff on success
65
72
  connectionStatus.textContent = '🟢 Connected';
66
73
  connectionStatus.className = 'status-connected';
67
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
+ }
68
87
  };
69
88
 
70
89
  ws.onclose = () => {
71
90
  connected = false;
72
- connectionStatus.textContent = '🔴 Disconnected';
91
+ clearInterval(heartbeatTimer);
92
+ connectionStatus.textContent = '🔴 Disconnected — reconnecting...';
73
93
  connectionStatus.className = 'status-disconnected';
74
- setTimeout(connect, 3000);
94
+ // Exponential backoff: 1s, 2s, 4s, max 10s
95
+ setTimeout(connect, reconnectDelay);
96
+ reconnectDelay = Math.min(reconnectDelay * 2, 10000);
75
97
  };
76
98
 
77
99
  ws.onerror = () => {
@@ -81,7 +103,9 @@ function connect() {
81
103
 
82
104
  ws.onmessage = (event) => {
83
105
  try {
84
- handleServerMessage(JSON.parse(event.data));
106
+ const msg = JSON.parse(event.data);
107
+ if (msg.type === 'pong') return; // Heartbeat response, ignore
108
+ handleServerMessage(msg);
85
109
  } catch (e) {
86
110
  console.error('Failed to parse message:', e);
87
111
  }
@@ -113,6 +137,8 @@ function handleServerMessage(msg) {
113
137
  case 'session_list': renderSessionList(msg.sessions); break;
114
138
  case 'session_messages':renderSessionMessages(msg.messages); break;
115
139
  case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
140
+ case 'export_data': handleExportData(msg); break;
141
+ case 'memory_content': handleMemoryContent(msg); break;
116
142
  case 'info': addInfoMessage(msg.message); break;
117
143
  case 'error': addErrorMessage(msg.message); setProcessing(false); break;
118
144
  case 'round_progress': break;
@@ -519,6 +545,12 @@ async function sendMessage() {
519
545
  const args = parts.slice(1);
520
546
  send({ type: 'command', name, args });
521
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;
522
554
  userInput.value = '';
523
555
  userInput.style.height = 'auto';
524
556
  return;
@@ -545,6 +577,14 @@ async function sendMessage() {
545
577
  }
546
578
  }
547
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
+
548
588
  addUserMessage(text, pendingImages);
549
589
  const msg = { type: 'chat', content: resolvedText };
550
590
  if (pendingImages.length > 0) {
@@ -936,6 +976,72 @@ userInput.addEventListener('keydown', (e) => {
936
976
  }
937
977
  }
938
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
939
1045
  if (e.key === 'Enter' && !e.shiftKey) {
940
1046
  e.preventDefault();
941
1047
  sendMessage();
@@ -1015,6 +1121,34 @@ userInput.addEventListener('paste', (e) => {
1015
1121
  }
1016
1122
  });
1017
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
+
1018
1152
  // ── Initialize ─────────────────────────────────────────────────────
1019
1153
 
1020
1154
  // Restore theme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",