pi-web-ui 0.45.0 → 0.46.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.
@@ -1030,6 +1030,7 @@ export class ClientSession {
1030
1030
  // (new chat + first message, completed turns, compaction, etc.).
1031
1031
  case "agent_end": {
1032
1032
  this.scheduleSessionsRefresh();
1033
+ this.refreshConversationTitle(conv);
1033
1034
  // Manual interrupt (Stop button / abort): the last assistant message
1034
1035
  // carries stopReason "aborted". A half-finished run should NOT be
1035
1036
  // reviewed (it would fail and inject a revision, only to be stopped
@@ -1059,6 +1060,7 @@ export class ClientSession {
1059
1060
  }
1060
1061
  case "entry_appended":
1061
1062
  this.scheduleSessionsRefresh();
1063
+ this.refreshConversationTitle(conv);
1062
1064
  break;
1063
1065
  case "message_update": {
1064
1066
  // Live assistant-message increment, deliberately OUTSIDE the snapshot
@@ -1125,6 +1127,21 @@ export class ClientSession {
1125
1127
  }, 800);
1126
1128
  // pushSessions no-ops unless the client opted in via list_sessions.
1127
1129
  }
1130
+ /** Refresh a conversation's title from its persisted first user message
1131
+ * while it is still unnamed. Runs off the event stream (entry_appended /
1132
+ * agent_end) rather than the prompt() call site, so ANY entry path that
1133
+ * lands a message names the chat the moment it is persisted — a rename
1134
+ * skipped by the prompt-start fast path (e.g. a concurrent switch) is
1135
+ * recovered here instead of leaving a permanent “新对话”. */
1136
+ refreshConversationTitle(conv) {
1137
+ if (conv.title !== DEFAULT_CONV_TITLE)
1138
+ return;
1139
+ const title = conversationTitle(conv.session);
1140
+ if (title === DEFAULT_CONV_TITLE)
1141
+ return;
1142
+ conv.title = title;
1143
+ this.emitConversations();
1144
+ }
1128
1145
  /** Serialize a persisted message with a STABLE id + cached object reference. */
1129
1146
  serializeCached(m) {
1130
1147
  const conv = this.conv;
@@ -1745,6 +1762,11 @@ export class ClientSession {
1745
1762
  * after the current turn settles, skipping remaining planned tool calls.
1746
1763
  */
1747
1764
  queue = false) {
1765
+ // Captured at the START (before any await): the conversation being
1766
+ // addressed by this prompt. See the naming block below — a concurrent
1767
+ // switch/new_chat while prompt() is in flight must never target a
1768
+ // different conversation.
1769
+ const conv = this.conv;
1748
1770
  try {
1749
1771
  const s = this.session;
1750
1772
  // Native slash commands (see NATIVE_COMMANDS) are executed here and
@@ -1760,6 +1782,19 @@ export class ClientSession {
1760
1782
  // is refused until admission reopens.
1761
1783
  if (this.quiesceBlocked())
1762
1784
  return;
1785
+ // Name the conversation from its FIRST prompt immediately, before any
1786
+ // await: the typed text IS the name. The `conv` reference was captured
1787
+ // before the try block, so a concurrent switch/new_chat while prompt()
1788
+ // is in flight can never rename a DIFFERENT conversation — or miss the
1789
+ // rename entirely. A failed send still leaves the name, which matches
1790
+ // what the user typed intent-wise; the entry_appended fallback below
1791
+ // re-derives it from the persisted transcript when needed.
1792
+ if (conv.title === DEFAULT_CONV_TITLE && text.trim()) {
1793
+ const trimmed = text.trim().replace(/\s+/g, " ");
1794
+ conv.title =
1795
+ trimmed.length > 30 ? `${trimmed.slice(0, 30)}…` : trimmed;
1796
+ this.emitConversations();
1797
+ }
1763
1798
  // Attach files as independent nextTurn context messages (asides) so the
1764
1799
  // user message stays clean; they render as separate attachment cards.
1765
1800
  const asides = await buildAttachmentMessages({
@@ -1798,16 +1833,10 @@ export class ClientSession {
1798
1833
  text: `提示发送失败:${err.message}`,
1799
1834
  });
1800
1835
  }
1801
- // Name the conversation after its first user prompt.
1802
- const conv = this.conv;
1803
- if (conv.title === DEFAULT_CONV_TITLE && text.trim()) {
1804
- const trimmed = text.trim().replace(/\s+/g, " ");
1805
- conv.title = trimmed.length > 30 ? `${trimmed.slice(0, 30)}…` : trimmed;
1806
- this.emitConversations();
1807
- }
1808
- // The active conversation has been continued since it was opened — it
1809
- // must not be dismissed when the user switches away. (Also bumps the
1810
- // per-project "most recently active" order used by set_cwd.)
1836
+ // The active conversation (captured at prompt start — see above) has been
1837
+ // continued since it was opened — it must not be dismissed when the user
1838
+ // switches away. (Also bumps the per-project "most recently active"
1839
+ // order used by set_cwd.)
1811
1840
  conv.promptedSinceActive = true;
1812
1841
  conv.lastActiveAt = Date.now();
1813
1842
  // Fresh run — restart the stall watchdog window.
@@ -2133,9 +2162,12 @@ export class ClientSession {
2133
2162
  return;
2134
2163
  const displaced = this.displaceActive();
2135
2164
  this.activeId = id;
2136
- this.cwd = this.conv.cwd;
2137
- // All listed conversations share the current project's cwd, so this is
2138
- // normally a no-op kept defensive for stale clients.
2165
+ const newCwd = this.conv.cwd;
2166
+ // A listed conversation may belong to ANOTHER project (cross-project
2167
+ // running list). Switching to it must also switch the active workspace
2168
+ // — otherwise the file tree / session history / recent-projects order
2169
+ // would keep showing the OLD project while the chat shows the new one.
2170
+ const cwdChanged = newCwd !== this.cwd;
2139
2171
  if (displaced)
2140
2172
  this.removeConversation(displaced.id);
2141
2173
  this.conv.promptedSinceActive = false;
@@ -2146,15 +2178,33 @@ export class ClientSession {
2146
2178
  this.pushTerminals();
2147
2179
  // The switched-to conversation has its own runtime (own resource cache).
2148
2180
  void this.pushSlashCommands();
2181
+ if (cwdChanged) {
2182
+ this.cwd = newCwd;
2183
+ // Mirror set_cwd's project-switch side-effects so the whole UI follows
2184
+ // the new workspace, not just the chat pane.
2185
+ try {
2186
+ this.onCwdChanged?.(newCwd);
2187
+ }
2188
+ catch {
2189
+ /* hook failure must not break the switch */
2190
+ }
2191
+ this.stateStore.remember(this.clientId, newCwd);
2192
+ void this.pushProjects();
2193
+ void this.refreshSessions();
2194
+ void this.listFiles(undefined);
2195
+ void this.listCommands();
2196
+ }
2149
2197
  this.flushSnapshot();
2150
2198
  }
2151
- /** Push the current project's running-conversation list to the client. */
2199
+ /** Push every running conversation across ALL projects to the client. The
2200
+ * running-conversation list is global so a background run from another
2201
+ * workspace stays visible; clicking one switches both the conversation and
2202
+ * its project (see switchConversation). The client groups the list by cwd. */
2152
2203
  emitConversations() {
2153
2204
  const conversations = [];
2154
2205
  for (const conv of this.convs.values()) {
2155
- // The running-conversation list is per project and only contains
2156
- // conversations that were displaced to the background while running.
2157
- if (conv.cwd !== this.cwd || !conv.listed)
2206
+ // Only conversations that were displaced to the background while running.
2207
+ if (!conv.listed)
2158
2208
  continue;
2159
2209
  let messageCount = 0;
2160
2210
  let isStreaming = false;
@@ -2261,26 +2311,86 @@ export class ClientSession {
2261
2311
  });
2262
2312
  }
2263
2313
  }
2264
- /** Switch the active session to a persisted one (from listSessions). */
2314
+ /** Open a persisted session as the active conversation (from listSessions).
2315
+ *
2316
+ * A persisted-session click must follow the same ownership rule as
2317
+ * new_chat/switch_conversation: every open conversation keeps its own
2318
+ * runtime. AgentSessionRuntime.switchSession() tears down (and aborts) the
2319
+ * current runtime, which would otherwise stop a response merely because the
2320
+ * user opened history while it was streaming.
2321
+ */
2265
2322
  async switchSession(path) {
2266
2323
  if (this.quiesceBlocked())
2267
2324
  return;
2325
+ let openedRuntime = null;
2326
+ let openedTerminals = null;
2268
2327
  try {
2269
- await this.runtime.switchSession(path);
2270
- await this.bindSession();
2271
- // The resumed session carries its own cwd sync it into the ACTIVE
2272
- // conversation (other open conversations are untouched).
2273
- this.conv.cwd = this.runtime.cwd;
2274
- this.cwd = this.runtime.cwd;
2275
- this.conv.title = conversationTitle(this.runtime.session);
2328
+ const targetPath = resolve(path);
2329
+ // A session may already be open in the running-conversation map. Reuse it
2330
+ // instead of creating a second writer for the same JSONL transcript.
2331
+ for (const conv of this.convs.values()) {
2332
+ const sessionFile = conv.session.sessionFile;
2333
+ if (sessionFile && resolve(sessionFile) === targetPath) {
2334
+ await this.switchConversation(conv.id);
2335
+ return;
2336
+ }
2337
+ }
2338
+ const sessionManager = SessionManager.open(targetPath);
2339
+ const targetCwd = sessionManager.getCwd();
2340
+ const conversationId = this.nextConversationId();
2341
+ openedTerminals = this.makeTerminalManager(conversationId, targetCwd);
2342
+ openedRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(openedTerminals), {
2343
+ cwd: targetCwd,
2344
+ agentDir: this.agentDir,
2345
+ sessionManager,
2346
+ });
2347
+ // Only displace the old active conversation after the replacement runtime
2348
+ // is known-good. This keeps a failed history open entirely non-destructive.
2349
+ const oldListed = this.conv.listed;
2350
+ const displaced = this.displaceActive();
2351
+ const openInProject = [...this.convs.values()].filter((c) => c.cwd === targetCwd).length +
2352
+ 1 -
2353
+ (displaced?.cwd === targetCwd ? 1 : 0);
2354
+ if (openInProject > MAX_OPEN_CONVERSATIONS) {
2355
+ // displaceActive() may have promoted a streaming conversation into the
2356
+ // running list. Roll that presentation-only mutation back because no
2357
+ // switch will take place.
2358
+ this.conv.listed = oldListed;
2359
+ openedTerminals.killAll();
2360
+ await openedRuntime.dispose();
2361
+ openedRuntime = null;
2362
+ openedTerminals = null;
2363
+ this.emit({
2364
+ type: "notice",
2365
+ level: "warning",
2366
+ text: `当前项目运行的对话已达上限(${MAX_OPEN_CONVERSATIONS} 个),请先打开某个对话并离开(不继续对话)以移出列表`,
2367
+ });
2368
+ return;
2369
+ }
2370
+ const conv = this.makeConversation(openedRuntime, conversationId, openedTerminals);
2276
2371
  // Deliberately resumed — must not be dismissed when the user later
2277
2372
  // switches away without sending a new message.
2278
- this.conv.promptedSinceActive = true;
2373
+ conv.promptedSinceActive = true;
2374
+ this.convs.set(conv.id, conv);
2375
+ this.activeId = conv.id;
2376
+ openedRuntime = null;
2377
+ openedTerminals = null;
2378
+ if (displaced)
2379
+ this.removeConversation(displaced.id);
2380
+ await this.bindSession();
2381
+ this.cwd = targetCwd;
2382
+ this.conv.lastActiveAt = Date.now();
2383
+ this.webUi.refresh();
2279
2384
  this.emitConversations();
2280
- // switchSession replaced the runtime — its resource cache is fresh.
2385
+ this.goalSvc.emitGoalStatus();
2386
+ this.pushTerminals();
2387
+ // The restored conversation has a fresh project-bound resource cache.
2281
2388
  void this.pushSlashCommands();
2282
2389
  }
2283
2390
  catch (err) {
2391
+ openedTerminals?.killAll();
2392
+ if (openedRuntime)
2393
+ await openedRuntime.dispose().catch(() => { });
2284
2394
  this.emit({
2285
2395
  type: "notice",
2286
2396
  level: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.45.0",
3
+ "version": "0.46.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": {
@@ -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-CeePtC8A.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-B71Da7xx.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};