pi-web-ui 0.53.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/dist/server/agent-service.js +126 -1
- package/dist/server/index.js +3 -0
- package/package.json +1 -1
- 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
|
@@ -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, {
|
|
@@ -2664,6 +2755,40 @@ export class ClientSession {
|
|
|
2664
2755
|
async searchFiles(query, reqId) {
|
|
2665
2756
|
return this.files.searchFiles(query, reqId);
|
|
2666
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
|
+
}
|
|
2667
2792
|
/** SCM 只读查询(结构化 JSON,reqId 匹配)。 */
|
|
2668
2793
|
async scmQuery(kind, reqId, arg) {
|
|
2669
2794
|
return this.files.scmQuery(kind, reqId, arg);
|
package/dist/server/index.js
CHANGED
|
@@ -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": {
|
|
@@ -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};
|