@vincentbanana/banana-gui 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -7
- package/bin/banana-gui-desktop.mjs +77 -0
- package/bin/banana-gui.mjs +71 -22
- package/desktop/main.cjs +53 -4
- package/package.json +18 -12
- package/ui/app.js +18 -1
package/README.md
CHANGED
|
@@ -6,9 +6,26 @@ Banana 的图形界面:本地起一个服务,用浏览器打开。黑白主
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g @vincentbanana/banana-gui
|
|
9
|
-
banana-gui
|
|
10
9
|
```
|
|
11
10
|
|
|
11
|
+
装完有两种启动方式,**同一份安装,不用额外装东西**:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
banana-gui # 浏览器版:启动后自动开浏览器,最轻
|
|
15
|
+
banana-gui --desktop # 桌面版:直接开独立窗口
|
|
16
|
+
banana-gui-desktop # 同上,等价的独立命令
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
electron 是可选依赖(`optionalDependencies`),npm 安装时会自动带上;万一某个平台装不上,`banana-gui` 仍然照常工作,只是 `--desktop` 会提示你用浏览器版。
|
|
20
|
+
|
|
21
|
+
> **国内网络注意**:electron 的二进制默认从 GitHub 下载,国内经常连不上(连不上时 npm 会静默跳过它,浏览器版不受影响)。想要桌面版就用镜像重装一次:
|
|
22
|
+
>
|
|
23
|
+
> ```bash
|
|
24
|
+
> npm install -g electron --electron_mirror=https://npmmirror.com/mirrors/electron/
|
|
25
|
+
> ```
|
|
26
|
+
>
|
|
27
|
+
> 装完 `banana-gui --desktop` 就能开窗口了。
|
|
28
|
+
|
|
12
29
|
首次启动会进配置向导(选 provider、贴 API key、选权限、测连接),配置与 CLI 共用 `~/.banana/config.json`。
|
|
13
30
|
|
|
14
31
|
## 用法
|
|
@@ -35,17 +52,27 @@ banana-gui --provider deepseek --model deepseek-chat
|
|
|
35
52
|
- 三态主题:跟随系统 / 浅色 / 深色,纯黑白无渐变
|
|
36
53
|
- 没配 API Key 也能打开界面,照着引导填完即可用
|
|
37
54
|
|
|
38
|
-
##
|
|
55
|
+
## 两种方式对比
|
|
56
|
+
|
|
57
|
+
| | 启动命令 | 需要什么 | 说明 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| 浏览器版 | `banana-gui` | 任何装了 Node 20+ 的系统 | 启动后自动打开浏览器,几十 MB |
|
|
60
|
+
| 桌面版 | `banana-gui --desktop` | 同一个 npm 包 | 独立窗口,约 200MB(内含 electron) |
|
|
61
|
+
|
|
62
|
+
两者用的是同一套 agent 内核和同一份界面资源,功能完全一致,只是外壳不同。
|
|
63
|
+
|
|
64
|
+
## 打包成安装包(可选)
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
想出 `.exe` / `.dmg` / `AppImage` 的话,在源码仓库里构建:
|
|
41
67
|
|
|
42
68
|
```bash
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
npm
|
|
69
|
+
git clone <这个仓库>
|
|
70
|
+
cd banana-gui
|
|
71
|
+
npm install # 装 electron-builder
|
|
72
|
+
npm run dist:win # 也支持 dist:mac / dist:linux
|
|
46
73
|
```
|
|
47
74
|
|
|
48
|
-
|
|
75
|
+
产物在 `gui/release/`。
|
|
49
76
|
|
|
50
77
|
## 配置
|
|
51
78
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 桌面版启动器:npm 安装后直接开一个独立窗口,不用浏览器。
|
|
3
|
+
//
|
|
4
|
+
// npm install -g @vincentbanana/banana-gui
|
|
5
|
+
// banana-gui-desktop 开窗口
|
|
6
|
+
// banana-gui-desktop --port 8080
|
|
7
|
+
//
|
|
8
|
+
// electron 是 optionalDependencies:装得上就能用,装不上会提示改用浏览器版
|
|
9
|
+
// (banana-gui)。本文件不直接依赖 electron 的 API,只负责把它拉起来。
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const require_ = createRequire(import.meta.url);
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
|
|
19
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
20
|
+
console.log('banana-gui-desktop — Banana 的桌面版(独立窗口)');
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log('用法: banana-gui-desktop [选项]');
|
|
23
|
+
console.log(' --port <端口> 指定端口(默认随机)');
|
|
24
|
+
console.log(' --cwd <目录> 指定工作区');
|
|
25
|
+
console.log(' --provider <名> 指定 provider');
|
|
26
|
+
console.log(' --model <名> 指定模型');
|
|
27
|
+
console.log(' --help 显示帮助');
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log('提示:日常使用建议用 banana-gui(浏览器版,更轻)。');
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** electron 装在依赖里;require('electron') 返回的是可执行文件路径。 */
|
|
34
|
+
function electronBinary() {
|
|
35
|
+
for (const specifier of ['electron', '@vincentbanana/banana-gui/node_modules/electron']) {
|
|
36
|
+
try {
|
|
37
|
+
const resolved = require_.resolve(specifier);
|
|
38
|
+
// electron 主入口导出的是 exe 路径;解析失败时退回到包目录里的 dist
|
|
39
|
+
const binary = require_(resolved);
|
|
40
|
+
if (typeof binary === 'string') return binary;
|
|
41
|
+
return join(dirname(resolved), 'dist', process.platform === 'win32' ? 'electron.exe' : 'electron');
|
|
42
|
+
} catch {
|
|
43
|
+
/* 换下一个候选 */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const binary = electronBinary();
|
|
50
|
+
if (!binary) {
|
|
51
|
+
console.error('没找到 electron,桌面版暂时用不了。');
|
|
52
|
+
console.error('');
|
|
53
|
+
console.error('可以先继续用浏览器版,功能完全一样:');
|
|
54
|
+
console.error(' banana-gui');
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error('想用桌面版,试着补装一次:');
|
|
57
|
+
console.error(' npm install -g electron');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const main = join(here, '..', 'desktop', 'main.cjs');
|
|
62
|
+
const child = spawn(binary, [main, ...args], {
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
env: {
|
|
65
|
+
...process.env,
|
|
66
|
+
BANANA_UI_DIR: join(here, '..', 'ui'),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
child.on('error', (error) => {
|
|
70
|
+
console.error('桌面版启动失败:' + error.message);
|
|
71
|
+
console.error('可以先用浏览器版:banana-gui');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
});
|
|
74
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
75
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
76
|
+
process.on(signal, () => child.kill(signal));
|
|
77
|
+
}
|
package/bin/banana-gui.mjs
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
// 启动:banana-gui(首次会进配置向导)
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
7
9
|
|
|
8
10
|
const require = createRequire(import.meta.url);
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
9
13
|
|
|
14
|
+
/** CLI 内核入口。依赖可能被 npm 提升到上层 node_modules,用 require 解析更稳。 */
|
|
10
15
|
function resolveCli() {
|
|
11
|
-
// 依赖可能被 npm 提升到上层 node_modules,用 require 解析更稳;
|
|
12
|
-
// 注意只能解析包根(子路径受 exports 限制)
|
|
13
16
|
try {
|
|
14
17
|
return require.resolve('@vincentbanana/bananacli');
|
|
15
18
|
} catch {
|
|
@@ -17,23 +20,43 @@ function resolveCli() {
|
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
23
|
+
/** electron 是可选依赖:装上了就能开独立窗口,没装就走浏览器。 */
|
|
24
|
+
function electronBinary() {
|
|
25
|
+
for (const specifier of ['electron', '@vincentbanana/banana-gui/node_modules/electron']) {
|
|
26
|
+
try {
|
|
27
|
+
const resolved = require.resolve(specifier);
|
|
28
|
+
// electron 包的主入口导出的是可执行文件路径
|
|
29
|
+
const binary = require(resolved);
|
|
30
|
+
if (typeof binary === 'string') return binary;
|
|
31
|
+
} catch {
|
|
32
|
+
/* 换下一个候选 */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
const args = process.argv.slice(2);
|
|
21
|
-
const passthrough = ['--port', '--cwd', '--provider', '--model', '--base-url', '--api-key', '--setup'];
|
|
22
39
|
|
|
23
|
-
if (args.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
41
|
+
console.log(pkg.version);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
28
46
|
console.log('banana-gui — Banana 的图形界面');
|
|
29
47
|
console.log('');
|
|
30
48
|
console.log('用法: banana-gui [选项]');
|
|
49
|
+
console.log(' --desktop 用独立窗口打开(需要已安装 electron)');
|
|
31
50
|
console.log(' --port <端口> 指定端口(默认随机)');
|
|
32
51
|
console.log(' --cwd <目录> 指定工作区');
|
|
33
52
|
console.log(' --provider <名> 指定 provider');
|
|
34
53
|
console.log(' --model <名> 指定模型');
|
|
35
|
-
console.log(' --open
|
|
54
|
+
console.log(' --no-open 不自动打开浏览器');
|
|
55
|
+
console.log(' --version 显示版本');
|
|
36
56
|
console.log(' --help 显示帮助');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('提示:默认用浏览器打开。想要独立窗口就加 --desktop,');
|
|
59
|
+
console.log(' 或直接运行 banana-gui-desktop。');
|
|
37
60
|
process.exit(0);
|
|
38
61
|
}
|
|
39
62
|
|
|
@@ -43,19 +66,45 @@ if (!entry) {
|
|
|
43
66
|
process.exit(1);
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
// 界面资源用本包自带的那份,保证 GUI 包和它的 ui/ 版本一致
|
|
70
|
+
const uiDir = join(here, '..', 'ui');
|
|
71
|
+
|
|
72
|
+
/** 用户参数原样转给内核;只摘掉本启动器自己的开关。 */
|
|
73
|
+
function kernelArgs() {
|
|
74
|
+
const rest = [];
|
|
75
|
+
for (const arg of args) {
|
|
76
|
+
if (arg === '--desktop' || arg === '--open') continue;
|
|
77
|
+
rest.push(arg);
|
|
52
78
|
}
|
|
53
|
-
|
|
54
|
-
cliArgs.push(arg);
|
|
79
|
+
return rest;
|
|
55
80
|
}
|
|
56
81
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
if (args.includes('--desktop')) {
|
|
83
|
+
// --desktop:交给 electron 开独立窗口,本进程只做转发
|
|
84
|
+
const binary = electronBinary();
|
|
85
|
+
if (!binary) {
|
|
86
|
+
console.error('没装 electron,用不了独立窗口。补装一次即可:');
|
|
87
|
+
console.error(' npm install -g electron --electron_mirror=https://npmmirror.com/mirrors/electron/');
|
|
88
|
+
console.error('(国内网络必须加镜像参数,GitHub 连不上会装不了)');
|
|
89
|
+
console.error('不装也没关系:直接运行 banana-gui 用浏览器版,功能完全一样。');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
launch(binary, [join(here, '..', 'desktop', 'main.cjs'), ...kernelArgs()], {});
|
|
93
|
+
} else {
|
|
94
|
+
launch(process.execPath, [entry, '--ui', ...kernelArgs()], { BANANA_UI_DIR: uiDir });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function launch(command, commandArgs, extraEnv) {
|
|
98
|
+
const child = spawn(command, commandArgs, {
|
|
99
|
+
stdio: 'inherit',
|
|
100
|
+
env: { ...process.env, ...extraEnv },
|
|
101
|
+
});
|
|
102
|
+
child.on('error', (error) => {
|
|
103
|
+
console.error('启动失败:' + error.message);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
107
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
108
|
+
process.on(signal, () => child.kill(signal));
|
|
109
|
+
}
|
|
110
|
+
}
|
package/desktop/main.cjs
CHANGED
|
@@ -8,12 +8,39 @@ const require_ = createRequire(__filename);
|
|
|
8
8
|
let child = null;
|
|
9
9
|
let win = null;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// electron 自带的 node 运行 CLI 内核;ELECTRON_RUN_AS_NODE 让 electron.exe 当纯 node 用,
|
|
12
|
+
// 否则会再开一个 Electron 窗口,而不是执行脚本。
|
|
13
|
+
const PASSTHROUGH = ['--port', '--cwd', '--provider', '--model', '--base-url', '--api-key'];
|
|
14
|
+
|
|
15
|
+
function serverArgs() {
|
|
16
|
+
const argv = process.argv.slice(2).filter((arg) => arg.startsWith('--'));
|
|
17
|
+
const extra = [];
|
|
18
|
+
for (const arg of argv) {
|
|
19
|
+
if (PASSTHROUGH.includes(arg)) {
|
|
20
|
+
extra.push(arg);
|
|
21
|
+
const value = process.argv[process.argv.indexOf(arg) + 1];
|
|
22
|
+
if (value && !value.startsWith('--')) extra.push(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
13
25
|
// 端口交给 CLI 随机分配,避免多开时互相抢占
|
|
14
|
-
|
|
26
|
+
return ['--ui', '--no-open', ...extra];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function startServer() {
|
|
30
|
+
let entry;
|
|
31
|
+
try {
|
|
32
|
+
// 依赖可能被提升到上层 node_modules;只能解析包根(子路径受 exports 限制)
|
|
33
|
+
entry = require_.resolve('@vincentbanana/bananacli');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error('找不到依赖 @vincentbanana/bananacli,请重新安装:npm install -g @vincentbanana/banana-gui');
|
|
36
|
+
}
|
|
37
|
+
child = spawn(process.execPath, [entry, ...serverArgs()], {
|
|
15
38
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
-
env:
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
42
|
+
BANANA_UI_DIR: process.env.BANANA_UI_DIR ?? join(__dirname, '..', 'ui'),
|
|
43
|
+
},
|
|
17
44
|
});
|
|
18
45
|
return new Promise((resolve, reject) => {
|
|
19
46
|
let buffer = '';
|
|
@@ -58,6 +85,28 @@ async function createWindow() {
|
|
|
58
85
|
shell.openExternal(target);
|
|
59
86
|
return { action: 'deny' };
|
|
60
87
|
});
|
|
88
|
+
if (process.argv.includes('--selftest')) await selfTest(url);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** --selftest:不靠人眼看,直接在页面里断言界面渲染成功,然后退出。 */
|
|
92
|
+
async function selfTest(url) {
|
|
93
|
+
try {
|
|
94
|
+
const probe = await win.webContents.executeJavaScript(`(() => ({
|
|
95
|
+
title: document.title,
|
|
96
|
+
composed: Boolean(document.querySelector('#composer')),
|
|
97
|
+
sidebar: Boolean(document.querySelector('#sidebar')),
|
|
98
|
+
scriptLoaded: typeof window.fetch === 'function' && Boolean(document.querySelector('#input')),
|
|
99
|
+
theme: getComputedStyle(document.documentElement).getPropertyValue('--bg').trim(),
|
|
100
|
+
}))()`);
|
|
101
|
+
const ok = probe.title === 'Banana' && probe.composed && probe.sidebar && probe.scriptLoaded;
|
|
102
|
+
console.log('SELFTEST ' + JSON.stringify({ ok, url: url.replace(/\/t\/[a-f0-9]+\//, '/t/<token>/'), ...probe }));
|
|
103
|
+
child?.kill();
|
|
104
|
+
app.exit(ok ? 0 : 1);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.log('SELFTEST ' + JSON.stringify({ ok: false, error: String(error?.message ?? error) }));
|
|
107
|
+
child?.kill();
|
|
108
|
+
app.exit(1);
|
|
109
|
+
}
|
|
61
110
|
}
|
|
62
111
|
|
|
63
112
|
const menuTemplate = [
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentbanana/banana-gui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Banana 的图形界面:本地服务 + 浏览器。黑白主题,自绘图标,无第三方 UI 依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.0.0"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"banana-gui": "bin/banana-gui.mjs"
|
|
10
|
+
"banana-gui": "bin/banana-gui.mjs",
|
|
11
|
+
"banana-gui-desktop": "bin/banana-gui-desktop.mjs"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"bin",
|
|
@@ -15,6 +16,16 @@
|
|
|
15
16
|
"desktop",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agent",
|
|
22
|
+
"gui",
|
|
23
|
+
"web-ui",
|
|
24
|
+
"llm",
|
|
25
|
+
"terminal",
|
|
26
|
+
"desktop",
|
|
27
|
+
"electron"
|
|
28
|
+
],
|
|
18
29
|
"scripts": {
|
|
19
30
|
"sync": "node ../scripts/sync-gui.mjs",
|
|
20
31
|
"test": "node test/gui-package.test.mjs",
|
|
@@ -34,6 +45,7 @@
|
|
|
34
45
|
"bin/**/*",
|
|
35
46
|
"package.json"
|
|
36
47
|
],
|
|
48
|
+
"npmRebuild": false,
|
|
37
49
|
"mac": {
|
|
38
50
|
"category": "public.app-category.developer-tools",
|
|
39
51
|
"target": [
|
|
@@ -52,21 +64,15 @@
|
|
|
52
64
|
"category": "Development"
|
|
53
65
|
}
|
|
54
66
|
},
|
|
67
|
+
"optionalDependencies": {
|
|
68
|
+
"electron": "^33.4.11"
|
|
69
|
+
},
|
|
55
70
|
"devDependencies": {
|
|
56
|
-
"electron": "^33.0.0",
|
|
57
71
|
"electron-builder": "^25.0.0"
|
|
58
72
|
},
|
|
59
73
|
"dependencies": {
|
|
60
|
-
"@vincentbanana/bananacli": "^0.
|
|
74
|
+
"@vincentbanana/bananacli": "^0.4.0"
|
|
61
75
|
},
|
|
62
|
-
"keywords": [
|
|
63
|
-
"ai",
|
|
64
|
-
"agent",
|
|
65
|
-
"gui",
|
|
66
|
-
"web-ui",
|
|
67
|
-
"llm",
|
|
68
|
-
"terminal"
|
|
69
|
-
],
|
|
70
76
|
"license": "MIT",
|
|
71
77
|
"publishConfig": {
|
|
72
78
|
"access": "public"
|
package/ui/app.js
CHANGED
|
@@ -130,6 +130,11 @@ function relativeTime(iso) {
|
|
|
130
130
|
return `${Math.round(hours / 24)} 天前`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/** 窄屏下侧边栏是覆盖层,选完会话必须收起来,否则挡住对话。 */
|
|
134
|
+
function closeSidebarIfOverlay() {
|
|
135
|
+
if (window.matchMedia('(max-width: 900px)').matches) $('sidebar').classList.remove('open');
|
|
136
|
+
}
|
|
137
|
+
|
|
133
138
|
function renderSidebar() {
|
|
134
139
|
const list = $('sessionList');
|
|
135
140
|
list.innerHTML = '';
|
|
@@ -147,7 +152,10 @@ function renderSidebar() {
|
|
|
147
152
|
|
|
148
153
|
node.addEventListener('click', (event) => {
|
|
149
154
|
if (event.target.closest('[data-del], [data-menu]')) return;
|
|
150
|
-
post('session', { action: 'switch', ref: item.id }).then(() =>
|
|
155
|
+
post('session', { action: 'switch', ref: item.id }).then(() => {
|
|
156
|
+
refresh(true);
|
|
157
|
+
closeSidebarIfOverlay();
|
|
158
|
+
});
|
|
151
159
|
});
|
|
152
160
|
|
|
153
161
|
node.querySelector('[data-del]').addEventListener('click', (event) => {
|
|
@@ -201,6 +209,7 @@ function openSessionMenu(item, anchor) {
|
|
|
201
209
|
if (action === 'switch') {
|
|
202
210
|
await post('session', { action: 'switch', ref: item.id });
|
|
203
211
|
await refresh(true);
|
|
212
|
+
closeSidebarIfOverlay();
|
|
204
213
|
} else if (action === 'rename') {
|
|
205
214
|
const title = prompt('新的标题', item.title || '');
|
|
206
215
|
if (title && title.trim()) {
|
|
@@ -1210,6 +1219,14 @@ function bind() {
|
|
|
1210
1219
|
}
|
|
1211
1220
|
});
|
|
1212
1221
|
|
|
1222
|
+
// 窄屏下点空白处收起侧边栏(点侧边栏内部或开合按钮不算)
|
|
1223
|
+
document.addEventListener('click', (event) => {
|
|
1224
|
+
if (!window.matchMedia('(max-width: 900px)').matches) return;
|
|
1225
|
+
if (!$('sidebar').classList.contains('open')) return;
|
|
1226
|
+
if (event.target.closest('#sidebar, #toggleSidebar')) return;
|
|
1227
|
+
$('sidebar').classList.remove('open');
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1213
1230
|
$('input').addEventListener('paste', (event) => {
|
|
1214
1231
|
for (const item of event.clipboardData?.files ?? []) attach(item);
|
|
1215
1232
|
});
|