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.
@@ -2,26 +2,9 @@
2
2
  <!--
3
3
  pi-web-ui Task Scheduler task — Windows auto-start at logon.
4
4
 
5
- The easy way (no admin needed, generates everything for you):
6
- npm i -g pi-web-ui
7
- pi-web-ui server install --port 8787 --cwd C:\path\to\project
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>
@@ -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=PORT=8787
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 SessionManager.list(this.cwd);
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
- // Refuse to pull the file out from under a live conversation.
2360
- for (const conv of this.convs.values()) {
2361
- if (conv.session.sessionFile === abs) {
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);
@@ -7,7 +7,7 @@
7
7
  * protocol defined in protocol.ts.
8
8
  *
9
9
  * Env:
10
- * PORT HTTP port (default 8787)
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.52.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 PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
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-KGJNsUEy.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?`
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};