pi-web-ui 0.52.0 → 0.54.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 +1 -1
- package/bin/pi-web-ui.mjs +210 -180
- package/deploy/com.xingshuyin.pi-web-ui.plist +1 -1
- package/deploy/nginx-subpath.conf +1 -1
- package/deploy/pi-web-ui-task.xml +3 -20
- package/deploy/pi-web-ui.service +1 -1
- package/dist/server/agent-service.js +184 -6
- package/dist/server/index.js +5 -2
- package/package.json +2 -2
- package/web/dist/assets/{TerminalPanel-CgkoEcJf.js → TerminalPanel-D7bIj91c.js} +1 -1
- package/web/dist/assets/index-tM4GjOsU.js +320 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-KGJNsUEy.js +0 -320
|
@@ -2,26 +2,9 @@
|
|
|
2
2
|
<!--
|
|
3
3
|
pi-web-ui Task Scheduler task — Windows auto-start at logon.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
pi-web-ui server status | restart | stop | uninstall
|
|
9
|
-
|
|
10
|
-
Manual install with this template (edit the paths below first):
|
|
11
|
-
schtasks /Create /TN "pi-web-ui" /XML pi-web-ui-task.xml /F
|
|
12
|
-
schtasks /Run /TN "pi-web-ui"
|
|
13
|
-
|
|
14
|
-
Notes:
|
|
15
|
-
- The task runs the PowerShell launcher the CLI generates at
|
|
16
|
-
%APPDATA%\pi-web-ui\pi-web-ui.ps1 with -WindowStyle Hidden, so the
|
|
17
|
-
server runs with no black console window (nothing to accidentally
|
|
18
|
-
close/kill). The ps1 sets PORT/PI_WEB_CWD, cd's to the workspace,
|
|
19
|
-
launches node, and appends output to %USERPROFILE%\pi-web-ui.log.
|
|
20
|
-
Preview both generated files with: pi-web-ui server install --print
|
|
21
|
-
- Save this file as UTF-16 LE (schtasks requires it; the CLI does this
|
|
22
|
-
automatically when it writes the task XML).
|
|
23
|
-
- LogonTrigger = starts when you log in, same as a launchd user agent.
|
|
24
|
-
For boot-start without login, use Docker instead (see README).
|
|
5
|
+
pi-web-ui server install 默认使用「登录 Run 键 + wscript 隐藏启动」(HKCU,无需管理员、
|
|
6
|
+
无黑窗),并自动迁移旧版计划任务安装。本 XML 仅保留给需要手动注册计划任务的场景
|
|
7
|
+
(例如管理员环境),不是默认路径。
|
|
25
8
|
-->
|
|
26
9
|
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
27
10
|
<RegistrationInfo>
|
package/deploy/pi-web-ui.service
CHANGED
|
@@ -19,7 +19,7 @@ Type=simple
|
|
|
19
19
|
User=YOUR_USER
|
|
20
20
|
# The workspace the agent operates in (read/edit/bash/write)
|
|
21
21
|
WorkingDirectory=/home/YOUR_USER
|
|
22
|
-
Environment=
|
|
22
|
+
Environment=PI_WEB_PORT=8787
|
|
23
23
|
# Point at your pi config dir if it's not the default ~/.pi/agent
|
|
24
24
|
#Environment=PI_CODING_AGENT_DIR=/home/YOUR_USER/.pi/agent
|
|
25
25
|
ExecStart=/usr/bin/pi-web-ui
|
|
@@ -263,6 +263,78 @@ function conversationTitle(session) {
|
|
|
263
263
|
}
|
|
264
264
|
return DEFAULT_CONV_TITLE;
|
|
265
265
|
}
|
|
266
|
+
/** 全局搜索的会话匹配:大小写不敏感,命中任一项即算 ——
|
|
267
|
+
* 显示名、当前项目内的文件名片段、首条消息,以及完整转录文本
|
|
268
|
+
* (SDK 的 allMessagesText 包含每一段 user 与 assistant 消息,AI 输出也在内)。 */
|
|
269
|
+
function sessionMatchesSearch(q, s) {
|
|
270
|
+
if (s.name && s.name.toLowerCase().includes(q))
|
|
271
|
+
return true;
|
|
272
|
+
if (basename(s.path).toLowerCase().includes(q))
|
|
273
|
+
return true;
|
|
274
|
+
if (s.firstMessage.toLowerCase().includes(q))
|
|
275
|
+
return true;
|
|
276
|
+
if (s.allMessagesText.toLowerCase().includes(q))
|
|
277
|
+
return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
/** 抽取一条 AgentMessage 的可搜索文本(user/assistant 的 text 块;
|
|
281
|
+
* 镜像 SDK buildSessionInfo 的 allMessagesText 范围,保证搜索与定位一致)。 */
|
|
282
|
+
function messageSearchText(m) {
|
|
283
|
+
const c = m.content;
|
|
284
|
+
if (typeof c === "string")
|
|
285
|
+
return c;
|
|
286
|
+
if (!Array.isArray(c))
|
|
287
|
+
return "";
|
|
288
|
+
const parts = [];
|
|
289
|
+
for (const b of c) {
|
|
290
|
+
if (!b || typeof b !== "object")
|
|
291
|
+
continue;
|
|
292
|
+
const blk = b;
|
|
293
|
+
if (blk.type === "text" && typeof blk.text === "string")
|
|
294
|
+
parts.push(blk.text);
|
|
295
|
+
}
|
|
296
|
+
return parts.join("\n");
|
|
297
|
+
}
|
|
298
|
+
/** 扫描一个会话转录文件,收集文本命中查询的消息锚点(role + timestamp,
|
|
299
|
+
* 按转录顺序,最多 cap 个)。仅 user/assistant 消息参与,与搜索范围一致。 */
|
|
300
|
+
function collectSessionAnchors(filePath, q, cap = 10) {
|
|
301
|
+
const anchors = [];
|
|
302
|
+
if (!q)
|
|
303
|
+
return anchors;
|
|
304
|
+
try {
|
|
305
|
+
const lines = readFileSync(filePath, "utf8").split("\n");
|
|
306
|
+
for (const line of lines) {
|
|
307
|
+
if (!line.trim())
|
|
308
|
+
continue;
|
|
309
|
+
let e;
|
|
310
|
+
try {
|
|
311
|
+
e = JSON.parse(line);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (e?.type !== "message")
|
|
317
|
+
continue;
|
|
318
|
+
const m = e.message;
|
|
319
|
+
if (!m)
|
|
320
|
+
continue;
|
|
321
|
+
if (m.role !== "user" && m.role !== "assistant")
|
|
322
|
+
continue;
|
|
323
|
+
if (typeof m.timestamp !== "number")
|
|
324
|
+
continue;
|
|
325
|
+
const text = messageSearchText(m);
|
|
326
|
+
if (!text || !text.toLowerCase().includes(q))
|
|
327
|
+
continue;
|
|
328
|
+
anchors.push({ role: m.role, timestamp: m.timestamp });
|
|
329
|
+
if (anchors.length >= cap)
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// 单个转录损坏不影响其余会话
|
|
335
|
+
}
|
|
336
|
+
return anchors;
|
|
337
|
+
}
|
|
266
338
|
export class ClientSession {
|
|
267
339
|
clientId;
|
|
268
340
|
/** Set by AgentService.attach: reflects the SERVICE-wide quiesce flag
|
|
@@ -2301,6 +2373,25 @@ export class ClientSession {
|
|
|
2301
2373
|
* background refreshes only re-push when this is true, so a mobile
|
|
2302
2374
|
* client that never opened the panel never pays the disk scan. */
|
|
2303
2375
|
sessionsRequested = false;
|
|
2376
|
+
/**
|
|
2377
|
+
* Last parsed session list for this cwd, cached briefly so repeated
|
|
2378
|
+
* global-search keystrokes don't re-parse every transcript file on each
|
|
2379
|
+
* request (a project can hold 100+ sessions of several MB each).
|
|
2380
|
+
* pushSessions() and searchSessions() share this fridge — opening the
|
|
2381
|
+
* panel warms it, then every keystroke inside the TTL is free.
|
|
2382
|
+
*/
|
|
2383
|
+
sessionInfosCache = null;
|
|
2384
|
+
static SESSION_INFO_CACHE_TTL = 3000;
|
|
2385
|
+
async loadSessionInfos() {
|
|
2386
|
+
const now = Date.now();
|
|
2387
|
+
const c = this.sessionInfosCache;
|
|
2388
|
+
if (c && c.cwd === this.cwd && now - c.at < ClientSession.SESSION_INFO_CACHE_TTL) {
|
|
2389
|
+
return c.infos;
|
|
2390
|
+
}
|
|
2391
|
+
const infos = await SessionManager.list(this.cwd);
|
|
2392
|
+
this.sessionInfosCache = { cwd: this.cwd, infos, at: now };
|
|
2393
|
+
return infos;
|
|
2394
|
+
}
|
|
2304
2395
|
/** Push the persisted session list to the client (client-requested). */
|
|
2305
2396
|
async refreshSessions() {
|
|
2306
2397
|
this.sessionsRequested = true;
|
|
@@ -2315,7 +2406,7 @@ export class ClientSession {
|
|
|
2315
2406
|
// Sessions live in the SDK default per-project dir
|
|
2316
2407
|
// (<agentDir>/sessions/--<cwd>--/), the same files the pi CLI/TUI
|
|
2317
2408
|
// use — one listing covers every conversation of the current folder.
|
|
2318
|
-
const infos = await
|
|
2409
|
+
const infos = await this.loadSessionInfos();
|
|
2319
2410
|
const sessions = new Map();
|
|
2320
2411
|
for (const s of infos) {
|
|
2321
2412
|
sessions.set(s.path, {
|
|
@@ -2341,7 +2432,16 @@ export class ClientSession {
|
|
|
2341
2432
|
this.stateStore.removeProject(this.clientId, path);
|
|
2342
2433
|
await this.pushProjects();
|
|
2343
2434
|
}
|
|
2344
|
-
/** Permanently delete a persisted session transcript file (history list ✕).
|
|
2435
|
+
/** Permanently delete a persisted session transcript file (history list ✕).
|
|
2436
|
+
*
|
|
2437
|
+
* Deleting the ACTIVE conversation's own transcript is allowed: the session
|
|
2438
|
+
* first switches away to the next-latest persisted chat (or a fresh blank
|
|
2439
|
+
* chat when no other history exists). If the displacement could not release
|
|
2440
|
+
* the file (streaming / open terminals / pending wake subscription /
|
|
2441
|
+
* conversation cap), the deletion is aborted with a notice instead of
|
|
2442
|
+
* yanking the file out of a live runtime. Background conversations still
|
|
2443
|
+
* block deletion outright.
|
|
2444
|
+
*/
|
|
2345
2445
|
async deleteSession(path) {
|
|
2346
2446
|
try {
|
|
2347
2447
|
const abs = resolve(path);
|
|
@@ -2356,13 +2456,57 @@ export class ClientSession {
|
|
|
2356
2456
|
});
|
|
2357
2457
|
return;
|
|
2358
2458
|
}
|
|
2359
|
-
//
|
|
2360
|
-
|
|
2361
|
-
|
|
2459
|
+
// A live conversation may hold the target transcript. A BACKGROUND
|
|
2460
|
+
// conversation must still block deletion outright, but when the ACTIVE
|
|
2461
|
+
// conversation holds it the request can be satisfied by switching away
|
|
2462
|
+
// first (next-latest history chat, or a fresh blank one) and letting
|
|
2463
|
+
// the displacement drop the old runtime.
|
|
2464
|
+
const holdsTarget = (conv) => {
|
|
2465
|
+
const file = conv.session.sessionFile;
|
|
2466
|
+
return file !== undefined && resolve(file) === abs;
|
|
2467
|
+
};
|
|
2468
|
+
const holder = [...this.convs.values()].find(holdsTarget);
|
|
2469
|
+
if (holder && holder.id !== this.activeId) {
|
|
2470
|
+
this.emit({
|
|
2471
|
+
type: "notice",
|
|
2472
|
+
level: "warning",
|
|
2473
|
+
text: "该对话正在后台运行,请先停止或关闭该对话再删除",
|
|
2474
|
+
});
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (holder) {
|
|
2478
|
+
// Same source the history panel uses (refreshSessions): newest first.
|
|
2479
|
+
const infos = await SessionManager.list(this.cwd);
|
|
2480
|
+
const next = infos
|
|
2481
|
+
.filter((s) => resolve(s.path) !== abs)
|
|
2482
|
+
.sort((a, b) => b.modified.getTime() - a.modified.getTime())[0];
|
|
2483
|
+
if (next)
|
|
2484
|
+
await this.switchSession(next.path);
|
|
2485
|
+
else
|
|
2486
|
+
await this.newChat();
|
|
2487
|
+
// displaceActive() may have RETAINED the old conversation as a
|
|
2488
|
+
// background run (streaming, open terminals, pending wake
|
|
2489
|
+
// subscription, conversation cap) — in every such case the file is
|
|
2490
|
+
// still held, so abort instead of yanking it from a live runtime.
|
|
2491
|
+
// Only a conversation that is genuinely still running in the
|
|
2492
|
+
// background (streaming / listed) keeps the "wait for it" notice;
|
|
2493
|
+
// a retained-but-idle hold means the switch itself failed (cap,
|
|
2494
|
+
// quiesce, runtime creation) — say that instead.
|
|
2495
|
+
const stillHeld = [...this.convs.values()].find(holdsTarget);
|
|
2496
|
+
if (stillHeld) {
|
|
2497
|
+
let stillRunning = stillHeld.listed;
|
|
2498
|
+
try {
|
|
2499
|
+
stillRunning = stillHeld.session.isStreaming || stillRunning;
|
|
2500
|
+
}
|
|
2501
|
+
catch {
|
|
2502
|
+
// session being replaced — keep the listed-flag fallback
|
|
2503
|
+
}
|
|
2362
2504
|
this.emit({
|
|
2363
2505
|
type: "notice",
|
|
2364
2506
|
level: "warning",
|
|
2365
|
-
text:
|
|
2507
|
+
text: stillRunning
|
|
2508
|
+
? "对话仍在后台运行,已停止删除;请等待其结束后再删除"
|
|
2509
|
+
: "未能切换到其他对话,已取消删除本次操作",
|
|
2366
2510
|
});
|
|
2367
2511
|
return;
|
|
2368
2512
|
}
|
|
@@ -2611,6 +2755,40 @@ export class ClientSession {
|
|
|
2611
2755
|
async searchFiles(query, reqId) {
|
|
2612
2756
|
return this.files.searchFiles(query, reqId);
|
|
2613
2757
|
}
|
|
2758
|
+
/** 全局搜索:在当前工作区的会话转录全文里做大小写不敏感匹配 ——
|
|
2759
|
+
* 不止首条消息,而是每一段 user 与 assistant 文本(AI 输出也在内)。
|
|
2760
|
+
* 结果经 session_search_results 回推(reqId 匹配);复用 loadSessionInfos()
|
|
2761
|
+
* 缓存,避免每个按键都重新解析全部转录文件。 */
|
|
2762
|
+
async searchSessions(query, reqId) {
|
|
2763
|
+
const q = query.trim().toLowerCase();
|
|
2764
|
+
if (!q) {
|
|
2765
|
+
this.emit({ type: "session_search_results", reqId, query, ok: true, results: [] });
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
try {
|
|
2769
|
+
const infos = await this.loadSessionInfos();
|
|
2770
|
+
const results = infos
|
|
2771
|
+
.filter((s) => sessionMatchesSearch(q, s))
|
|
2772
|
+
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
2773
|
+
.slice(0, 50)
|
|
2774
|
+
.map((s) => {
|
|
2775
|
+
const base = {
|
|
2776
|
+
path: s.path,
|
|
2777
|
+
name: s.name,
|
|
2778
|
+
firstMessage: s.firstMessage,
|
|
2779
|
+
messageCount: s.messageCount,
|
|
2780
|
+
modified: s.modified.getTime(),
|
|
2781
|
+
source: "web",
|
|
2782
|
+
};
|
|
2783
|
+
// 命中会话里再定位具体消息(供点击跳转);仅元数据命中则无锚点
|
|
2784
|
+
return { ...base, anchors: collectSessionAnchors(s.path, q) };
|
|
2785
|
+
});
|
|
2786
|
+
this.emit({ type: "session_search_results", reqId, query, ok: true, results });
|
|
2787
|
+
}
|
|
2788
|
+
catch {
|
|
2789
|
+
this.emit({ type: "session_search_results", reqId, query, ok: false, results: [] });
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2614
2792
|
/** SCM 只读查询(结构化 JSON,reqId 匹配)。 */
|
|
2615
2793
|
async scmQuery(kind, reqId, arg) {
|
|
2616
2794
|
return this.files.scmQuery(kind, reqId, arg);
|
package/dist/server/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* protocol defined in protocol.ts.
|
|
8
8
|
*
|
|
9
9
|
* Env:
|
|
10
|
-
*
|
|
10
|
+
* PI_WEB_PORT HTTP port (default 8787; legacy PORT also honored)
|
|
11
11
|
* PI_WEB_CWD workspace the agent operates in (default: process.cwd())
|
|
12
12
|
* PI_WEB_DATA_DIR where per-client UI state is stored (client-state.json,
|
|
13
13
|
* default: <home>/.pi-web). Chat sessions are NOT stored here — they live
|
|
@@ -37,7 +37,7 @@ import { ensureWindowsBash, windowsBashDir } from "./ensure-bash.js";
|
|
|
37
37
|
import { listThemes, resolveThemeFile } from "./themes.js";
|
|
38
38
|
import { PluginManager, resolvePluginClientFile } from "./plugins.js";
|
|
39
39
|
import { McpBridge } from "./mcp-bridge.js";
|
|
40
|
-
const PORT = Number(process.env.PORT ?? 8787);
|
|
40
|
+
const PORT = Number(process.env.PI_WEB_PORT ?? process.env.PORT ?? 8787);
|
|
41
41
|
const CWD = resolve(process.env.PI_WEB_CWD ?? process.cwd());
|
|
42
42
|
const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
|
|
43
43
|
/** Bind address. Default is loopback ONLY — the service is a local personal
|
|
@@ -589,6 +589,9 @@ wss.on("connection", (ws) => {
|
|
|
589
589
|
case "search_files":
|
|
590
590
|
void cs.searchFiles(msg.query, msg.reqId);
|
|
591
591
|
break;
|
|
592
|
+
case "search_sessions":
|
|
593
|
+
void cs.searchSessions(msg.query, msg.reqId);
|
|
594
|
+
break;
|
|
592
595
|
case "scm_status":
|
|
593
596
|
void cs.scmQuery("status", msg.reqId);
|
|
594
597
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"prepublishOnly": "npm run build",
|
|
50
50
|
"dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
|
|
51
|
-
"dev:server": "cross-env
|
|
51
|
+
"dev:server": "cross-env PI_WEB_PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
|
|
52
52
|
"dev:web": "vite --config web/vite.config.ts",
|
|
53
53
|
"build": "npm run build:web && npm run build:server",
|
|
54
54
|
"build:web": "vite build --config web/vite.config.ts",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as d,j as n}from"./markdown-DRBrS2Nf.js";import{b as $,T as R,u as O,F as P,a as K,c as q,d as L,e as B,f as J,g as X,h as G,r as Q}from"./index-
|
|
1
|
+
import{a as d,j as n}from"./markdown-DRBrS2Nf.js";import{b as $,T as R,u as O,F as P,a as K,c as q,d as L,e as B,f as J,g as X,h as G,r as Q}from"./index-tM4GjOsU.js";import{D as U,o as V}from"./xterm-D1D2FVe3.js";import"./react-C9ovnpIm.js";function W({conversationId:s,terminalId:r,command:x,cwd:a,active:f,send:m,register:v}){const w=d.useRef(null),N=d.useRef(null),b=x?JSON.stringify(x):"";return d.useEffect(()=>{const p=w.current;if(!p)return;const t=new U({theme:$(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),c=new V;t.loadAddon(c),t.open(p),N.current={term:t,fit:c},f&&t.focus();const h=()=>{t.options.theme=$()};window.addEventListener(R,h),t.attachCustomKeyEventHandler(o=>{var F;if(o.type!=="keydown")return!0;const E=(F=o.key)==null?void 0:F.toLowerCase();if((o.ctrlKey||o.metaKey)&&E==="v")return!1;if(o.ctrlKey&&!o.shiftKey&&!o.altKey&&E==="c"&&t.hasSelection()){const T=t.textarea;return T&&(T.value=t.getSelection(),T.select()),!1}return!0});const g=v(s,r,{write:o=>t.write(o),dispose:()=>t.dispose()}),C=()=>{try{c.fit(),m({type:"terminal_resize",terminalId:r,conversationId:s,cols:t.cols,rows:t.rows})}catch{}},j=requestAnimationFrame(()=>{try{c.fit()}catch{}m(x?{type:"run_command",terminalId:r,conversationId:s,command:x,cols:t.cols,rows:t.rows}:{type:"terminal_create",terminalId:r,conversationId:s,cwd:a,cols:t.cols,rows:t.rows})}),k=t.onData(o=>{m({type:"terminal_input",terminalId:r,conversationId:s,data:o})});let y=null;return typeof ResizeObserver<"u"&&(y=new ResizeObserver(()=>{p.offsetWidth>0&&p.offsetHeight>0&&C()}),y.observe(p)),()=>{cancelAnimationFrame(j),k.dispose(),window.removeEventListener(R,h),y==null||y.disconnect(),g(),t.dispose(),N.current=null}},[s,r,b,m,v]),d.useEffect(()=>{if(!f)return;const p=requestAnimationFrame(()=>{const t=N.current;if(t){try{t.fit.fit(),m({type:"terminal_resize",terminalId:r,conversationId:s,cols:t.term.cols,rows:t.term.rows})}catch{}t.term.focus()}});return()=>cancelAnimationFrame(p)},[f]),n.jsx("div",{ref:w,className:`term-xterm ${f?"":"hidden"}`})}const A={name:"",command:"",cwd:"${pwd}"};function se({chat:s,send:r,terminal:x}){const a=O(),[f,m]=d.useState(null),[v,w]=d.useState(!1),[N,b]=d.useState(!1),[p,t]=d.useState(null),[c,h]=d.useState(A),[g,C]=d.useState(null),j=d.useRef(null);d.useEffect(()=>{s.terminals.length===0?m(null):s.terminals.some(e=>e.id===f)||m(s.terminals[s.terminals.length-1].id)},[s.terminals,f]),d.useEffect(()=>()=>{j.current&&clearTimeout(j.current)},[]);const k=e=>{var u;if(!s.ready)return;const i=Q(),l=s.activeConversationId||((u=s.state)==null?void 0:u.conversationId)||"";x.create({...e,id:i,conversationId:l,cols:e.cols??80,rows:e.rows??24,running:!0,exitCode:null}),m(i),w(!1)},y=()=>{var e;return k({title:a("terminalTitle",{n:s.terminals.length+1}),cwd:((e=s.state)==null?void 0:e.cwd)??""})},o=e=>{var u;const i=e.name||e.command,l=s.terminals.find(_=>_.title===i);if(l){x.restart(l.id),m(l.id),r({type:"run_command",terminalId:l.id,conversationId:l.conversationId,command:e,cols:80,rows:24});return}k({title:i,cwd:((u=s.state)==null?void 0:u.cwd)??"",command:e})},E=e=>{const i=s.terminals.find(l=>l.id===e);if(i&&r({type:"terminal_kill",terminalId:e,conversationId:i.conversationId}),x.close(e),f===e){const l=s.terminals.filter(u=>u.id!==e);m(l.length>0?l[l.length-1].id:null)}},F=()=>{b(!0),t(null),h(A)},T=e=>{const i=s.commands[e];i&&(b(!1),t(e),h({name:i.name,command:i.command,cwd:i.cwd??""}))},D=()=>{b(!1),t(null)},S=()=>{const e=c.name.trim(),i=c.command.trim();if(!e||!i)return;const l=c.cwd.trim(),u={name:e,command:i,cwd:l||void 0},_=N?[...s.commands,u]:p!==null?s.commands.map((z,H)=>H===p?u:z):s.commands;r({type:"save_commands",commands:_}),D()},I=e=>{if(g===e){const i=s.commands.filter((l,u)=>u!==e);r({type:"save_commands",commands:i}),C(null),j.current&&clearTimeout(j.current)}else C(e),j.current&&clearTimeout(j.current),j.current=setTimeout(()=>C(null),2500)},M=N||p!==null;return n.jsxs("div",{className:"terminal-view",children:[n.jsxs("aside",{className:`term-side term-commands ${v?"open":""}`,children:[n.jsxs("div",{className:"panel-header",children:[n.jsx("span",{className:"panel-title",children:a("commands")}),n.jsxs("div",{className:"panel-header-actions",children:[n.jsx("button",{type:"button",className:"panel-refresh",title:a("rerun"),onClick:()=>r({type:"list_commands"}),children:n.jsx(P,{})}),n.jsx("button",{type:"button",className:"panel-new",title:a("newCommand"),onClick:F,children:n.jsx(K,{})})]})]}),n.jsx("div",{className:"panel-body",children:M?n.jsxs("div",{className:"cmd-form",children:[n.jsx("label",{htmlFor:"cmd-name",children:a("name")}),n.jsx("input",{id:"cmd-name",className:"cmd-input",value:c.name,placeholder:a("exampleName"),autoFocus:!0,onChange:e=>h({...c,name:e.target.value})}),n.jsx("label",{htmlFor:"cmd-command",children:a("command")}),n.jsx("input",{id:"cmd-command",className:"cmd-input",value:c.command,placeholder:a("exampleCommand"),onChange:e=>h({...c,command:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&S()}}),n.jsxs("label",{htmlFor:"cmd-cwd",children:[a("directory")," ",n.jsx("span",{className:"cmd-hint",children:a("cwdHint")})]}),n.jsx("input",{id:"cmd-cwd",className:"cmd-input",value:c.cwd,placeholder:"${pwd}",onChange:e=>h({...c,cwd:e.target.value}),onKeyDown:e=>{e.key==="Enter"&&!e.nativeEvent.isComposing&&S()}}),n.jsxs("div",{className:"cmd-form-actions",children:[n.jsx("button",{type:"button",className:"btn",onClick:D,children:a("cancel")}),n.jsx("button",{type:"button",className:"btn primary",disabled:!c.name.trim()||!c.command.trim(),onClick:S,children:a("save")})]})]}):n.jsxs(n.Fragment,{children:[s.commands.length===0&&n.jsx("div",{className:"panel-empty",children:a("noCommands")}),s.commands.map((e,i)=>n.jsxs("div",{className:"cmd-item",children:[n.jsx("button",{type:"button",className:"cmd-run",title:a("clickToRun"),onClick:()=>o(e),children:n.jsx(q,{})}),n.jsxs("button",{type:"button",className:"cmd-main",title:a("clickToRun"),onClick:()=>o(e),children:[n.jsx("span",{className:"cmd-name",children:e.name}),n.jsx("span",{className:"cmd-command",children:e.command}),e.cwd&&n.jsx("span",{className:"cmd-cwd",children:e.cwd})]}),n.jsx("button",{type:"button",className:"cmd-act",title:a("edit"),onClick:()=>T(i),children:n.jsx(L,{})}),n.jsx("button",{type:"button",className:`cmd-act del ${g===i?"confirm":""}`,title:a("delete"),onClick:()=>I(i),children:g===i?a("confirmQ"):n.jsx(B,{})})]},i))]})}),n.jsxs("div",{className:"term-tabs-block",children:[n.jsxs("div",{className:"panel-header",children:[n.jsx("span",{className:"panel-title",children:a("terminal")}),n.jsx("button",{type:"button",className:"panel-new",title:a("newTerminal"),onClick:y,children:n.jsx(K,{})})]}),n.jsxs("div",{className:"panel-body",children:[s.terminals.length===0&&n.jsx("div",{className:"panel-empty",children:a("noTerminal")}),s.terminals.map(e=>n.jsxs("div",{className:`term-tab ${e.id===f?"active":""}`,children:[n.jsxs("button",{type:"button",className:"term-tab-main",title:`${e.cwd}${e.command?`
|
|
2
2
|
> ${e.command.command}`:""}`,onClick:()=>{m(e.id),w(!1)},children:[n.jsx("span",{className:`term-tab-dot ${e.running?"run":"exit"}`}),n.jsxs("span",{className:"term-tab-title",children:[e.title,!e.running&&n.jsx("span",{className:"term-tab-exit",children:a("exited",{code:e.exitCode===null?"":` ${e.exitCode}`})})]})]}),n.jsx("button",{type:"button",className:"term-tab-close",title:a("closeTerminal"),onClick:()=>E(e.id),children:n.jsx(J,{})})]},e.id))]})]})]}),n.jsxs("div",{className:"term-main",children:[v&&n.jsx("div",{className:"drawer-backdrop",onClick:()=>w(!1)}),n.jsx("button",{type:"button",className:"term-side-toggle",title:a("commands"),onClick:()=>w(e=>!e),children:n.jsx(X,{})}),s.terminals.length===0?n.jsxs("div",{className:"term-empty",children:[n.jsx(G,{className:"term-empty-icon"}),n.jsx("div",{className:"term-empty-title",children:a("builtinTerminal")}),n.jsx("div",{className:"term-empty-sub",children:a("termEmptySub")})]}):s.terminals.map(e=>n.jsx(W,{conversationId:e.conversationId,terminalId:e.id,command:e.command,cwd:e.cwd,active:e.id===f,send:r,register:x.register},`${e.conversationId}:${e.id}`))]})]})}export{se as TerminalPanel};
|