jinzd-ai-cli 0.1.96 → 0.1.98
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 +72 -4
- package/dist/{chunk-LSM6QYBE.js → chunk-E6NUTTCD.js} +1 -8
- package/dist/{chunk-MKCOC3S6.js → chunk-K4AB4ZUH.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{run-tests-BX2T6JTU.js → run-tests-CNDALP24.js} +1 -1
- package/dist/{server-KV423SND.js → server-K5CTT3WU.js} +15 -15
- package/dist/web/client/app.js +109 -0
- package/dist/web/client/index.html +11 -6
- package/dist/web/client/style.css +116 -6
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -352,6 +352,74 @@ 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-18,v0.1.91 → v0.1.96)
|
|
356
|
+
|
|
357
|
+
### Web UI P2 收尾 + P3-2 PWA + Bug 修复
|
|
358
|
+
|
|
359
|
+
**P2-3:工具执行可视化**(v0.1.91)
|
|
360
|
+
|
|
361
|
+
| 文件 | 变更类型 | 说明 |
|
|
362
|
+
|------|---------|------|
|
|
363
|
+
| `src/web/protocol.ts` | 修改 | `S2C_ToolCall` 新增 `startTime` 字段 |
|
|
364
|
+
| `src/web/tool-executor-web.ts` | 修改 | 工具执行计时 + 状态追踪 |
|
|
365
|
+
| `src/web/client/app.js` | 修改 | 工具卡片显示耗时、进度指示、状态可视化 |
|
|
366
|
+
|
|
367
|
+
**P2-4:Prompt 模板库**(v0.1.91)
|
|
368
|
+
|
|
369
|
+
| 文件 | 变更类型 | 说明 |
|
|
370
|
+
|------|---------|------|
|
|
371
|
+
| `src/web/client/index.html` | 修改 | 📝 按钮 + `<dialog>` 模板管理弹窗(表单/搜索/列表) |
|
|
372
|
+
| `src/web/client/style.css` | 修改 | `.template-item` 系列样式(hover、tags、actions、empty) |
|
|
373
|
+
| `src/web/client/app.js` | 修改 | 完整 CRUD(loadTemplates/saveTemplates/renderTemplateList/useTemplate/editTemplate/deleteTemplate/exportTemplates/importTemplates) |
|
|
374
|
+
|
|
375
|
+
- localStorage 持久化,key: `aicli-templates`
|
|
376
|
+
- 搜索过滤(名称/标签/内容)
|
|
377
|
+
- 标签系统(逗号分隔,彩色 badge)
|
|
378
|
+
- 导入/导出 JSON(去重合并)
|
|
379
|
+
|
|
380
|
+
**P2-5:代码主题联动**(v0.1.91)
|
|
381
|
+
- highlight.js 主题随 DaisyUI 主题自动切换(暗色 → github-dark,亮色 → github-light)
|
|
382
|
+
|
|
383
|
+
**Bug 修复:EADDRINUSE 端口占用崩溃**(v0.1.92)
|
|
384
|
+
|
|
385
|
+
| 文件 | 变更类型 | 说明 |
|
|
386
|
+
|------|---------|------|
|
|
387
|
+
| `src/web/server.ts` | 修改 | 端口占用时自动递增(最多尝试 10 次),不再 Fatal crash |
|
|
388
|
+
|
|
389
|
+
**Bug 修复:空 Untitled 会话 + 侧边栏宽度 + /skill 命令**(v0.1.94)
|
|
390
|
+
|
|
391
|
+
| 文件 | 变更类型 | 说明 |
|
|
392
|
+
|------|---------|------|
|
|
393
|
+
| `src/web/session-handler.ts` | 修改 | 新增 `saveIfNeeded()` 仅在 session 有消息时持久化;chat 完成后自动保存;`/clear` 先保存旧 session;新增 `/skill` 命令(list/activate/off/reload);`/help` 更新 |
|
|
394
|
+
| `src/web/client/index.html` | 修改 | 搜索框 `min-w-0` + 按钮 `whitespace-nowrap`,防止 "+" New 被挤压 |
|
|
395
|
+
|
|
396
|
+
**P3-2:PWA 支持 — 可安装为桌面/移动应用**(v0.1.95)
|
|
397
|
+
|
|
398
|
+
| 文件 | 变更类型 | 说明 |
|
|
399
|
+
|------|---------|------|
|
|
400
|
+
| `src/web/client/manifest.json` | **新增** | PWA 清单(name/theme_color/display:standalone/icons) |
|
|
401
|
+
| `src/web/client/icon.svg` | **新增** | 矢量图标(机器人头像 + "ai-cli" 文字) |
|
|
402
|
+
| `src/web/client/icon-192.png` | **新增** | 192x192 PNG 图标(纯 Node.js 生成) |
|
|
403
|
+
| `src/web/client/icon-512.png` | **新增** | 512x512 PNG 图标 |
|
|
404
|
+
| `src/web/client/sw.js` | **新增** | Service Worker(网络优先 app shell + 缓存优先 CDN,不缓存 API/WS) |
|
|
405
|
+
| `src/web/client/index.html` | 修改 | manifest link + theme-color + apple-mobile-web-app + favicon + SW 注册 |
|
|
406
|
+
| `scripts/generate-icons.cjs` | **新增** | 纯 Node.js PNG 图标生成器(CRC32 + zlib,无外部依赖) |
|
|
407
|
+
|
|
408
|
+
**LAN 访问支持**(v0.1.96)
|
|
409
|
+
|
|
410
|
+
| 文件 | 变更类型 | 说明 |
|
|
411
|
+
|------|---------|------|
|
|
412
|
+
| `src/web/server.ts` | 修改 | `--host 0.0.0.0` 时显示 📱 LAN IP 地址,方便手机/平板访问 |
|
|
413
|
+
|
|
414
|
+
### 版本与收尾
|
|
415
|
+
- `src/core/constants.ts`:VERSION `0.1.91` → `0.1.96`
|
|
416
|
+
- `package.json`:version 同步
|
|
417
|
+
- 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
|
|
418
|
+
- 发布:`npm publish` → `jinzd-ai-cli@0.1.96`
|
|
419
|
+
- Web UI P2 全部完成(P2-1 ~ P2-5),P3-2 完成
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
355
423
|
## 本轮开发完成记录(2026-03-16,v0.1.85 → v0.1.86)
|
|
356
424
|
|
|
357
425
|
### Web UI P1 增强(续):命令扩展 + 键盘快捷键 + 导出 + 断线重连
|
|
@@ -2042,15 +2110,15 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
2042
2110
|
|---|------|------|------|
|
|
2043
2111
|
| P2-1 | **多 Tab 会话** | [x] | 浏览器内多 Tab 并行对话(CLI 做不到),类似 ChatGPT |
|
|
2044
2112
|
| P2-2 | **文件树面板** | [x] | 浏览项目目录结构,点击文件查看/插入上下文,可视化 `@` 引用 |
|
|
2045
|
-
| P2-3 | **工具执行可视化** | [
|
|
2046
|
-
| P2-4 | **Prompt 模板库** | [
|
|
2047
|
-
| P2-5 | **代码主题联动** | [
|
|
2113
|
+
| P2-3 | **工具执行可视化** | [x] | 工具调用时间线 + 耗时显示 + 进度指示 + 状态可视化 |
|
|
2114
|
+
| P2-4 | **Prompt 模板库** | [x] | CRUD + 搜索 + 标签 + 导入导出(localStorage 持久化) |
|
|
2115
|
+
| P2-5 | **代码主题联动** | [x] | highlight.js 主题随 DaisyUI 主题切换(亮色 → github-light,暗色 → github-dark) |
|
|
2048
2116
|
|
|
2049
2117
|
### P3 — 长远方向
|
|
2050
2118
|
|
|
2051
2119
|
| # | 功能 | 状态 | 说明 |
|
|
2052
2120
|
|---|------|------|------|
|
|
2053
2121
|
| P3-1 | **多用户支持** | [ ] | 认证 + 多 session handler(当前 `activeHandler` 单连接限制) |
|
|
2054
|
-
| P3-2 | **PWA 支持** | [
|
|
2122
|
+
| P3-2 | **PWA 支持** | [x] | manifest.json + Service Worker + 图标 + `--host 0.0.0.0` LAN 访问 |
|
|
2055
2123
|
| P3-3 | **移动端适配** | [ ] | 响应式布局 + 触摸手势 |
|
|
2056
2124
|
| P3-4 | **Electron 打包** | [ ] | 复用 Web UI 代码打包为桌面应用 |
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/tools/builtin/run-tests.ts
|
|
10
4
|
import { execSync } from "child_process";
|
|
@@ -14,7 +8,7 @@ import { platform } from "os";
|
|
|
14
8
|
import chalk from "chalk";
|
|
15
9
|
|
|
16
10
|
// src/core/constants.ts
|
|
17
|
-
var VERSION = "0.1.
|
|
11
|
+
var VERSION = "0.1.98";
|
|
18
12
|
var APP_NAME = "ai-cli";
|
|
19
13
|
var CONFIG_DIR_NAME = ".aicli";
|
|
20
14
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -447,7 +441,6 @@ var runTestsTool = {
|
|
|
447
441
|
};
|
|
448
442
|
|
|
449
443
|
export {
|
|
450
|
-
__require,
|
|
451
444
|
VERSION,
|
|
452
445
|
APP_NAME,
|
|
453
446
|
CONFIG_DIR_NAME,
|
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-K4AB4ZUH.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-E6NUTTCD.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-CNDALP24.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-K5CTT3WU.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-K4AB4ZUH.js";
|
|
27
27
|
import {
|
|
28
28
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
29
29
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -34,9 +34,8 @@ import {
|
|
|
34
34
|
PLAN_MODE_READONLY_TOOLS,
|
|
35
35
|
PLAN_MODE_SYSTEM_ADDON,
|
|
36
36
|
SKILLS_DIR_NAME,
|
|
37
|
-
VERSION
|
|
38
|
-
|
|
39
|
-
} from "./chunk-LSM6QYBE.js";
|
|
37
|
+
VERSION
|
|
38
|
+
} from "./chunk-E6NUTTCD.js";
|
|
40
39
|
|
|
41
40
|
// src/web/server.ts
|
|
42
41
|
import express from "express";
|
|
@@ -44,6 +43,7 @@ import { createServer } from "http";
|
|
|
44
43
|
import { WebSocketServer } from "ws";
|
|
45
44
|
import { join as join3, dirname, resolve as resolve2, relative } from "path";
|
|
46
45
|
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
46
|
+
import { networkInterfaces } from "os";
|
|
47
47
|
|
|
48
48
|
// src/web/tool-executor-web.ts
|
|
49
49
|
import { randomUUID } from "crypto";
|
|
@@ -1560,6 +1560,16 @@ async function startWebServer(options = {}) {
|
|
|
1560
1560
|
const MAX_PORT_ATTEMPTS = 10;
|
|
1561
1561
|
let actualPort = port;
|
|
1562
1562
|
const tryListen = (attempt) => {
|
|
1563
|
+
server.once("error", (err) => {
|
|
1564
|
+
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
1565
|
+
actualPort++;
|
|
1566
|
+
console.log(` \u26A0 Port ${actualPort - 1} in use, trying ${actualPort}...`);
|
|
1567
|
+
server.close(() => tryListen(attempt + 1));
|
|
1568
|
+
} else {
|
|
1569
|
+
console.error(` \u274C Failed to start server: ${err.message}`);
|
|
1570
|
+
process.exit(1);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1563
1573
|
server.listen(actualPort, host, () => {
|
|
1564
1574
|
const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${actualPort}`;
|
|
1565
1575
|
console.log(`
|
|
@@ -1567,7 +1577,7 @@ async function startWebServer(options = {}) {
|
|
|
1567
1577
|
if (actualPort !== port) console.log(` (port ${port} was in use, using ${actualPort})`);
|
|
1568
1578
|
if (host === "0.0.0.0" || host === "::") {
|
|
1569
1579
|
try {
|
|
1570
|
-
const nets =
|
|
1580
|
+
const nets = networkInterfaces();
|
|
1571
1581
|
for (const name of Object.keys(nets)) {
|
|
1572
1582
|
for (const net of nets[name] ?? []) {
|
|
1573
1583
|
if (net.family === "IPv4" && !net.internal) {
|
|
@@ -1582,16 +1592,6 @@ async function startWebServer(options = {}) {
|
|
|
1582
1592
|
`);
|
|
1583
1593
|
openBrowser(url);
|
|
1584
1594
|
});
|
|
1585
|
-
server.once("error", (err) => {
|
|
1586
|
-
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
1587
|
-
actualPort++;
|
|
1588
|
-
console.log(` \u26A0 Port ${actualPort - 1} in use, trying ${actualPort}...`);
|
|
1589
|
-
tryListen(attempt + 1);
|
|
1590
|
-
} else {
|
|
1591
|
-
console.error(` \u274C Failed to start server: ${err.message}`);
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
});
|
|
1595
1595
|
};
|
|
1596
1596
|
tryListen(1);
|
|
1597
1597
|
process.on("SIGINT", () => {
|
package/dist/web/client/app.js
CHANGED
|
@@ -1682,3 +1682,112 @@ if (savedTheme) {
|
|
|
1682
1682
|
|
|
1683
1683
|
connect();
|
|
1684
1684
|
userInput.focus();
|
|
1685
|
+
|
|
1686
|
+
// ── Mobile: Sidebar toggle ──────────────────────────────────────────
|
|
1687
|
+
|
|
1688
|
+
const btnSidebarToggle = document.getElementById('btn-sidebar-toggle');
|
|
1689
|
+
const sidebarBackdrop = document.getElementById('sidebar-backdrop');
|
|
1690
|
+
|
|
1691
|
+
function openSidebar() {
|
|
1692
|
+
sidebar.classList.add('sidebar-open');
|
|
1693
|
+
sidebarBackdrop?.classList.remove('hidden');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function closeSidebar() {
|
|
1697
|
+
sidebar.classList.remove('sidebar-open');
|
|
1698
|
+
sidebarBackdrop?.classList.add('hidden');
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function toggleSidebar() {
|
|
1702
|
+
if (sidebar.classList.contains('sidebar-open')) {
|
|
1703
|
+
closeSidebar();
|
|
1704
|
+
} else {
|
|
1705
|
+
openSidebar();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (btnSidebarToggle) {
|
|
1710
|
+
btnSidebarToggle.addEventListener('click', toggleSidebar);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Close sidebar when selecting a session on mobile
|
|
1714
|
+
const origSwitchSession = window.switchSession;
|
|
1715
|
+
if (typeof origSwitchSession === 'function') {
|
|
1716
|
+
// Wrap — after switching, close sidebar if mobile
|
|
1717
|
+
window._origSwitchSession = origSwitchSession;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// ── Mobile: Swipe gesture to open/close sidebar ─────────────────────
|
|
1721
|
+
|
|
1722
|
+
let touchStartX = 0;
|
|
1723
|
+
let touchStartY = 0;
|
|
1724
|
+
let touchStartTime = 0;
|
|
1725
|
+
const SWIPE_THRESHOLD = 60;
|
|
1726
|
+
const SWIPE_MAX_Y = 80; // Ignore diagonal swipes
|
|
1727
|
+
const EDGE_ZONE = 30; // px from left edge to trigger open
|
|
1728
|
+
|
|
1729
|
+
document.addEventListener('touchstart', (e) => {
|
|
1730
|
+
touchStartX = e.touches[0].clientX;
|
|
1731
|
+
touchStartY = e.touches[0].clientY;
|
|
1732
|
+
touchStartTime = Date.now();
|
|
1733
|
+
}, { passive: true });
|
|
1734
|
+
|
|
1735
|
+
document.addEventListener('touchend', (e) => {
|
|
1736
|
+
const dx = e.changedTouches[0].clientX - touchStartX;
|
|
1737
|
+
const dy = Math.abs(e.changedTouches[0].clientY - touchStartY);
|
|
1738
|
+
const dt = Date.now() - touchStartTime;
|
|
1739
|
+
|
|
1740
|
+
// Only quick swipes (< 400ms), mostly horizontal
|
|
1741
|
+
if (dt > 400 || dy > SWIPE_MAX_Y) return;
|
|
1742
|
+
|
|
1743
|
+
// Swipe right from left edge → open sidebar
|
|
1744
|
+
if (dx > SWIPE_THRESHOLD && touchStartX < EDGE_ZONE && !sidebar.classList.contains('sidebar-open')) {
|
|
1745
|
+
openSidebar();
|
|
1746
|
+
}
|
|
1747
|
+
// Swipe left → close sidebar (if open)
|
|
1748
|
+
if (dx < -SWIPE_THRESHOLD && sidebar.classList.contains('sidebar-open')) {
|
|
1749
|
+
closeSidebar();
|
|
1750
|
+
}
|
|
1751
|
+
}, { passive: true });
|
|
1752
|
+
|
|
1753
|
+
// ── Mobile: Handle virtual keyboard / visual viewport ───────────────
|
|
1754
|
+
|
|
1755
|
+
if (window.visualViewport) {
|
|
1756
|
+
let initialHeight = window.visualViewport.height;
|
|
1757
|
+
|
|
1758
|
+
window.visualViewport.addEventListener('resize', () => {
|
|
1759
|
+
const vh = window.visualViewport.height;
|
|
1760
|
+
const offset = window.visualViewport.offsetTop;
|
|
1761
|
+
// When keyboard opens, viewport height shrinks significantly
|
|
1762
|
+
const keyboardOpen = vh < initialHeight * 0.75;
|
|
1763
|
+
|
|
1764
|
+
// Adjust the app container to fit the visible viewport
|
|
1765
|
+
const app = document.getElementById('app');
|
|
1766
|
+
if (app) {
|
|
1767
|
+
app.style.height = `${vh}px`;
|
|
1768
|
+
app.style.transform = `translateY(${offset}px)`;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Scroll to bottom when keyboard opens (so user sees latest messages)
|
|
1772
|
+
if (keyboardOpen) {
|
|
1773
|
+
setTimeout(scrollToBottom, 100);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
// Update initial height on orientation change
|
|
1778
|
+
window.addEventListener('orientationchange', () => {
|
|
1779
|
+
setTimeout(() => { initialHeight = window.visualViewport.height; }, 500);
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// ── Mobile: Close sidebar when clicking session item ────────────────
|
|
1784
|
+
|
|
1785
|
+
// Observe session-list clicks to auto-close sidebar on mobile
|
|
1786
|
+
if (sessionListEl) {
|
|
1787
|
+
sessionListEl.addEventListener('click', (e) => {
|
|
1788
|
+
const item = e.target.closest('.session-item');
|
|
1789
|
+
if (item && window.innerWidth <= 768) {
|
|
1790
|
+
setTimeout(closeSidebar, 150);
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en" data-theme="dark">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
6
6
|
<title>ai-cli Web UI</title>
|
|
7
7
|
<!-- PWA -->
|
|
8
8
|
<link rel="manifest" href="manifest.json">
|
|
@@ -27,9 +27,12 @@
|
|
|
27
27
|
<!-- ── Navbar ─────────────────────────────────────── -->
|
|
28
28
|
<div class="navbar bg-base-200 border-b border-base-content/10 px-4 min-h-[3.5rem] flex-shrink-0">
|
|
29
29
|
<div class="navbar-start gap-2">
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
<button id="btn-sidebar-toggle" class="btn btn-sm btn-ghost sidebar-toggle-btn" title="Toggle sidebar" aria-label="Toggle sidebar">
|
|
31
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
32
|
+
</button>
|
|
33
|
+
<span class="text-lg font-bold text-primary brand-text">🤖 ai-cli</span>
|
|
34
|
+
<select id="provider-select" class="select select-sm select-bordered navbar-select" title="Provider"></select>
|
|
35
|
+
<select id="model-select" class="select select-sm select-bordered navbar-select" title="Model"></select>
|
|
33
36
|
</div>
|
|
34
37
|
<div class="navbar-end gap-1">
|
|
35
38
|
<button id="btn-think" class="btn btn-sm btn-ghost" title="Toggle thinking mode">💭 Think</button>
|
|
@@ -57,7 +60,7 @@
|
|
|
57
60
|
<div class="flex flex-1 overflow-hidden">
|
|
58
61
|
|
|
59
62
|
<!-- Sidebar -->
|
|
60
|
-
<aside id="sidebar" class="sidebar bg-base-200 border-r border-base-content/10 flex flex-col
|
|
63
|
+
<aside id="sidebar" class="sidebar bg-base-200 border-r border-base-content/10 flex flex-col flex-shrink-0 overflow-hidden">
|
|
61
64
|
<!-- Sidebar tabs -->
|
|
62
65
|
<div class="flex border-b border-base-content/10 flex-shrink-0">
|
|
63
66
|
<button class="sidebar-tab active flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="sessions">📋 Sessions</button>
|
|
@@ -103,6 +106,8 @@
|
|
|
103
106
|
</div>
|
|
104
107
|
</div>
|
|
105
108
|
</aside>
|
|
109
|
+
<!-- Sidebar backdrop (mobile overlay) -->
|
|
110
|
+
<div id="sidebar-backdrop" class="sidebar-backdrop hidden" onclick="closeSidebar()"></div>
|
|
106
111
|
|
|
107
112
|
<!-- Chat Area -->
|
|
108
113
|
<main id="chat-area" class="flex-1 overflow-y-auto px-4 py-4 relative">
|
|
@@ -127,7 +132,7 @@
|
|
|
127
132
|
</div>
|
|
128
133
|
|
|
129
134
|
<!-- ── Input Area ─────────────────────────────────── -->
|
|
130
|
-
<footer class="bg-base-200 border-t border-base-content/10 px-4 py-3 flex-shrink-0">
|
|
135
|
+
<footer class="bg-base-200 border-t border-base-content/10 px-4 py-3 flex-shrink-0 input-footer">
|
|
131
136
|
<div class="max-w-4xl mx-auto flex gap-2 items-end">
|
|
132
137
|
<button id="btn-templates" class="btn btn-ghost btn-square btn-sm self-center" title="Prompt templates">
|
|
133
138
|
<span class="text-lg">📝</span>
|
|
@@ -254,6 +254,10 @@
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/* ── Sidebar ───────────────────────────────────────── */
|
|
257
|
+
.sidebar {
|
|
258
|
+
width: 18rem;
|
|
259
|
+
transition: width 0.2s ease;
|
|
260
|
+
}
|
|
257
261
|
.sidebar .session-item {
|
|
258
262
|
padding: 0.5rem 0.6rem;
|
|
259
263
|
border-radius: 0.375rem;
|
|
@@ -559,12 +563,118 @@
|
|
|
559
563
|
font-size: 0.85rem;
|
|
560
564
|
}
|
|
561
565
|
|
|
562
|
-
/* ──
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
+
/* ── Sidebar toggle button (hidden on desktop) ──────── */
|
|
567
|
+
.sidebar-toggle-btn { display: none; }
|
|
568
|
+
|
|
569
|
+
/* ── Sidebar backdrop (mobile overlay) ───────────────── */
|
|
570
|
+
.sidebar-backdrop {
|
|
571
|
+
position: fixed;
|
|
572
|
+
inset: 0;
|
|
573
|
+
top: 3.5rem;
|
|
574
|
+
background: rgba(0, 0, 0, 0.5);
|
|
575
|
+
z-index: 19;
|
|
576
|
+
-webkit-backdrop-filter: blur(2px);
|
|
577
|
+
backdrop-filter: blur(2px);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* ── Touch optimization ──────────────────────────────── */
|
|
581
|
+
button, a, .session-item, .file-tree-row, .template-item, .tool-item, .mcp-server-item {
|
|
582
|
+
-webkit-tap-highlight-color: transparent;
|
|
583
|
+
touch-action: manipulation;
|
|
566
584
|
}
|
|
585
|
+
|
|
586
|
+
/* ── Input footer safe area (iPhone notch) ───────────── */
|
|
587
|
+
.input-footer {
|
|
588
|
+
padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ── Responsive: Tablet ────────────────────────────── */
|
|
592
|
+
@media (max-width: 768px) {
|
|
593
|
+
.sidebar-toggle-btn { display: flex; }
|
|
594
|
+
.sidebar {
|
|
595
|
+
width: 0;
|
|
596
|
+
padding: 0;
|
|
597
|
+
border: none;
|
|
598
|
+
position: fixed;
|
|
599
|
+
z-index: 20;
|
|
600
|
+
top: 3.5rem;
|
|
601
|
+
left: 0;
|
|
602
|
+
height: calc(100vh - 3.5rem);
|
|
603
|
+
height: calc(100dvh - 3.5rem);
|
|
604
|
+
transition: width 0.2s ease, padding 0.2s ease;
|
|
605
|
+
}
|
|
606
|
+
.sidebar.sidebar-open {
|
|
607
|
+
width: 18rem;
|
|
608
|
+
border-right: 1px solid oklch(var(--bc) / 0.1);
|
|
609
|
+
overflow: hidden;
|
|
610
|
+
}
|
|
611
|
+
/* Provider/model selects: narrower on tablet */
|
|
612
|
+
.navbar-select { width: 7rem; font-size: 0.8rem; }
|
|
613
|
+
/* Navbar end buttons: hide text labels */
|
|
614
|
+
#btn-think, #btn-plan { font-size: 0.8rem; padding: 0 0.4rem; }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* ── Responsive: Phone ──────────────────────────────── */
|
|
567
618
|
@media (max-width: 640px) {
|
|
568
|
-
|
|
569
|
-
.
|
|
619
|
+
/* Navbar: compact layout */
|
|
620
|
+
.navbar { padding: 0 0.5rem; min-height: 3rem; }
|
|
621
|
+
.navbar-start { gap: 0.25rem; }
|
|
622
|
+
.navbar-end { gap: 0; }
|
|
623
|
+
.brand-text { display: none; }
|
|
624
|
+
.navbar-select { width: 5.5rem; font-size: 0.72rem; height: 2rem; min-height: 2rem; }
|
|
625
|
+
/* Navbar end: icon-only for action buttons */
|
|
626
|
+
#btn-think, #btn-plan { font-size: 0.75rem; padding: 0 0.3rem; min-height: 2rem; height: 2rem; }
|
|
627
|
+
.divider-horizontal { display: none; }
|
|
628
|
+
#btn-clear, #btn-compact { min-height: 2rem; height: 2rem; padding: 0 0.3rem; }
|
|
629
|
+
|
|
630
|
+
/* Sidebar: full width on small phones */
|
|
631
|
+
.sidebar.sidebar-open { width: min(85vw, 20rem); }
|
|
632
|
+
|
|
633
|
+
/* Chat area */
|
|
634
|
+
#chat-area { padding: 0.75rem 0.5rem; }
|
|
635
|
+
.msg-assistant { padding: 0.75rem; font-size: 0.92rem; }
|
|
636
|
+
.msg-assistant pre code { font-size: 0.78rem; padding: 0.75rem; }
|
|
637
|
+
.msg-assistant table { font-size: 0.8rem; }
|
|
638
|
+
.msg-assistant th, .msg-assistant td { padding: 0.3rem 0.5rem; }
|
|
639
|
+
|
|
640
|
+
/* Input area */
|
|
641
|
+
.input-footer { padding: 0.5rem; padding-bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px)); }
|
|
642
|
+
.input-footer .max-w-4xl { gap: 0.35rem; }
|
|
643
|
+
#user-input { font-size: 16px; /* Prevents iOS zoom on focus */ min-height: 2.5rem; }
|
|
644
|
+
#btn-send, #btn-stop { width: 2.5rem; height: 2.5rem; min-height: 2.5rem; }
|
|
645
|
+
#btn-templates { width: 2rem; height: 2rem; min-height: 2rem; }
|
|
646
|
+
#btn-templates .text-lg { font-size: 1rem; }
|
|
647
|
+
|
|
648
|
+
/* Status bar: hide or compact */
|
|
649
|
+
.input-footer .text-xs { font-size: 0.65rem; gap: 0.25rem; }
|
|
650
|
+
|
|
651
|
+
/* Tool cards */
|
|
652
|
+
.tool-card { padding: 0.4rem 0.65rem; font-size: 0.8rem; }
|
|
653
|
+
.confirm-diff { font-size: 0.72rem; max-height: 150px; }
|
|
654
|
+
|
|
655
|
+
/* Copy button always visible on mobile (no hover) */
|
|
656
|
+
.copy-code-btn { opacity: 0.6; }
|
|
657
|
+
|
|
658
|
+
/* Modals: near full-width on phone */
|
|
659
|
+
.modal-box { max-width: 95vw; margin: 0.5rem; }
|
|
660
|
+
#templates-modal .modal-box { max-height: 85vh; }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── Responsive: Very small phones (320px) ───────────── */
|
|
664
|
+
@media (max-width: 380px) {
|
|
665
|
+
.navbar-select { width: 4.5rem; font-size: 0.68rem; }
|
|
666
|
+
.sidebar.sidebar-open { width: 90vw; }
|
|
667
|
+
.msg-assistant { padding: 0.5rem 0.6rem; font-size: 0.88rem; }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* ── Landscape phone: reduce vertical space ──────────── */
|
|
671
|
+
@media (max-height: 500px) and (max-width: 900px) {
|
|
672
|
+
.navbar { min-height: 2.5rem; }
|
|
673
|
+
.input-footer { padding-top: 0.35rem; padding-bottom: calc(0.35rem + env(safe-area-inset-bottom, 0px)); }
|
|
674
|
+
#chat-area { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/* ── PWA standalone mode: extra top padding for status bar ── */
|
|
678
|
+
@media (display-mode: standalone) {
|
|
679
|
+
.navbar { padding-top: env(safe-area-inset-top, 0px); }
|
|
570
680
|
}
|