pi-web-ui 0.13.0 → 0.14.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.
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentService — wraps the pi SDK (@earendil-works/pi-coding-agent) for the web
|
|
3
3
|
* frontend. Each browser client (identified by a persistent clientId) gets its
|
|
4
|
-
* own AgentSessionRuntime
|
|
5
|
-
*
|
|
4
|
+
* own AgentSessionRuntime, but sessions live in the SDK default per-project
|
|
5
|
+
* directory (<agentDir>/sessions/--<cwd>--/) — the same transcript files the
|
|
6
|
+
* pi CLI/TUI use — so every conversation of a folder shows up everywhere.
|
|
6
7
|
*
|
|
7
8
|
* Streaming model: the SDK emits AgentSessionEvents; we forward lightweight
|
|
8
9
|
* `tool_delta` messages for live tool output and schedule throttled full-state
|
|
@@ -446,10 +447,6 @@ export class WebUIContext {
|
|
|
446
447
|
this.pendingDialogs.clear();
|
|
447
448
|
}
|
|
448
449
|
}
|
|
449
|
-
/** Sanitize a clientId (UUID) for use as a directory name. */
|
|
450
|
-
function sanitizeId(id) {
|
|
451
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80) || "anon";
|
|
452
|
-
}
|
|
453
450
|
const IS_WIN32 = process.platform === "win32";
|
|
454
451
|
// mac/linux: hide build & dependency noise (original behavior).
|
|
455
452
|
const IGNORED_ENTRIES = new Set([
|
|
@@ -683,9 +680,6 @@ function conversationTitle(session) {
|
|
|
683
680
|
export class ClientSession {
|
|
684
681
|
clientId;
|
|
685
682
|
cwd;
|
|
686
|
-
/** Absolute per-client session directory.
|
|
687
|
-
/** Absolute per-client session directory. */
|
|
688
|
-
sessionDir;
|
|
689
683
|
/** pi config dir (auth/models/skills). */
|
|
690
684
|
agentDir;
|
|
691
685
|
/** Persisted per-client UI state (last workspace + recent projects). */
|
|
@@ -734,22 +728,22 @@ export class ClientSession {
|
|
|
734
728
|
disposed = false;
|
|
735
729
|
/** pi-config readiness check, cached briefly so 60ms snapshots don't hit disk. */
|
|
736
730
|
piCheckCache = null;
|
|
737
|
-
constructor(clientId, cwd,
|
|
731
|
+
constructor(clientId, cwd, agentDir, stateStore) {
|
|
738
732
|
this.clientId = clientId;
|
|
739
733
|
this.cwd = cwd;
|
|
740
|
-
this.sessionDir = sessionDir;
|
|
741
734
|
this.agentDir = agentDir;
|
|
742
735
|
this.stateStore = stateStore;
|
|
743
736
|
}
|
|
744
|
-
static async create(clientId, cwd,
|
|
737
|
+
static async create(clientId, cwd, stateStore) {
|
|
745
738
|
const agentDir = process.env.PI_CODING_AGENT_DIR ?? getAgentDir();
|
|
746
|
-
const cs = new ClientSession(clientId, cwd,
|
|
739
|
+
const cs = new ClientSession(clientId, cwd, agentDir, stateStore);
|
|
747
740
|
const runtime = await createAgentSessionRuntime(cs.makeRuntimeFactory(), {
|
|
748
741
|
cwd,
|
|
749
742
|
agentDir,
|
|
750
|
-
// Resume the most recent session for this
|
|
751
|
-
//
|
|
752
|
-
|
|
743
|
+
// Resume the most recent session for this project — the SDK default
|
|
744
|
+
// per-project dir (<agentDir>/sessions/--<cwd>--/, shared with the
|
|
745
|
+
// pi CLI/TUI) — or start a fresh one on first visit.
|
|
746
|
+
sessionManager: SessionManager.continueRecent(cwd),
|
|
753
747
|
});
|
|
754
748
|
// First conversation = the resumed session; it also seeds the shared
|
|
755
749
|
// ModelRuntime that every later conversation reuses.
|
|
@@ -809,6 +803,7 @@ export class ClientSession {
|
|
|
809
803
|
lastMessagesArray: [],
|
|
810
804
|
queueSteering: 0,
|
|
811
805
|
queueFollowUp: 0,
|
|
806
|
+
toolStartTimes: new Map(),
|
|
812
807
|
};
|
|
813
808
|
}
|
|
814
809
|
/** Add a socket to this client's broadcast set; flushes buffered startup notices. */
|
|
@@ -883,6 +878,52 @@ export class ClientSession {
|
|
|
883
878
|
}
|
|
884
879
|
break;
|
|
885
880
|
}
|
|
881
|
+
case "tool_execution_start": {
|
|
882
|
+
// Record the moment the tool actually starts so tool_status can
|
|
883
|
+
// report real execution time (vs. time spent waiting on the model).
|
|
884
|
+
conv.toolStartTimes.set(event.toolCallId, Date.now());
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
case "tool_execution_end": {
|
|
888
|
+
const startedAt = conv.toolStartTimes.get(event.toolCallId);
|
|
889
|
+
conv.toolStartTimes.delete(event.toolCallId);
|
|
890
|
+
const durationMs = startedAt !== undefined ? Date.now() - startedAt : undefined;
|
|
891
|
+
// The bash tool does not put its exit code in result.details — on
|
|
892
|
+
// failure it throws "Command exited with code N" and the agent
|
|
893
|
+
// wraps that into the error result text. Try details first (future
|
|
894
|
+
// tools / SDK changes), then parse the error text.
|
|
895
|
+
const details = event.result?.details;
|
|
896
|
+
let exitCode;
|
|
897
|
+
if (typeof details === "object" &&
|
|
898
|
+
details !== null &&
|
|
899
|
+
typeof details.exitCode === "number") {
|
|
900
|
+
exitCode = details.exitCode;
|
|
901
|
+
}
|
|
902
|
+
else if (event.isError) {
|
|
903
|
+
const content = event.result?.content;
|
|
904
|
+
const text = Array.isArray(content)
|
|
905
|
+
? content
|
|
906
|
+
.map((c) => (typeof c === "object" &&
|
|
907
|
+
c !== null &&
|
|
908
|
+
c.type === "text")
|
|
909
|
+
? (c.text ?? "")
|
|
910
|
+
: "")
|
|
911
|
+
.join("\n")
|
|
912
|
+
: "";
|
|
913
|
+
const m = text.match(/exited with code (\d+)/);
|
|
914
|
+
if (m)
|
|
915
|
+
exitCode = Number(m[1]);
|
|
916
|
+
}
|
|
917
|
+
this.emit({
|
|
918
|
+
type: "tool_status",
|
|
919
|
+
toolCallId: event.toolCallId,
|
|
920
|
+
toolName: event.toolName,
|
|
921
|
+
isError: event.isError,
|
|
922
|
+
exitCode,
|
|
923
|
+
durationMs,
|
|
924
|
+
});
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
886
927
|
case "tool_execution_update": {
|
|
887
928
|
const text = extractPartialText(event.partialResult);
|
|
888
929
|
if (text) {
|
|
@@ -2100,7 +2141,7 @@ export class ClientSession {
|
|
|
2100
2141
|
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
2101
2142
|
cwd: this.cwd,
|
|
2102
2143
|
agentDir: this.agentDir,
|
|
2103
|
-
sessionManager: SessionManager.create(this.cwd
|
|
2144
|
+
sessionManager: SessionManager.create(this.cwd),
|
|
2104
2145
|
});
|
|
2105
2146
|
const conv = this.makeConversation(runtime);
|
|
2106
2147
|
this.convs.set(conv.id, conv);
|
|
@@ -2206,23 +2247,12 @@ export class ClientSession {
|
|
|
2206
2247
|
}
|
|
2207
2248
|
async pushSessions() {
|
|
2208
2249
|
try {
|
|
2209
|
-
|
|
2210
|
-
//
|
|
2211
|
-
//
|
|
2212
|
-
|
|
2213
|
-
// ones created in this web UI.
|
|
2214
|
-
const safePath = `--${resolve(this.cwd)
|
|
2215
|
-
.replace(/^[/\\]/, "")
|
|
2216
|
-
.replace(/[/\\:]/g, "-")}--`;
|
|
2217
|
-
const tuiSessionDir = join(this.agentDir, "sessions", safePath);
|
|
2218
|
-
const [webInfos, tuiInfos] = await Promise.all([
|
|
2219
|
-
SessionManager.list(this.cwd, this.sessionDir),
|
|
2220
|
-
existsSync(tuiSessionDir)
|
|
2221
|
-
? SessionManager.list(this.cwd, tuiSessionDir).catch(() => [])
|
|
2222
|
-
: Promise.resolve([]),
|
|
2223
|
-
]);
|
|
2250
|
+
// Sessions live in the SDK default per-project dir
|
|
2251
|
+
// (<agentDir>/sessions/--<cwd>--/), the same files the pi CLI/TUI
|
|
2252
|
+
// use — one listing covers every conversation of the current folder.
|
|
2253
|
+
const infos = await SessionManager.list(this.cwd);
|
|
2224
2254
|
const sessions = new Map();
|
|
2225
|
-
for (const s of
|
|
2255
|
+
for (const s of infos) {
|
|
2226
2256
|
sessions.set(s.path, {
|
|
2227
2257
|
path: s.path,
|
|
2228
2258
|
name: s.name,
|
|
@@ -2232,16 +2262,6 @@ export class ClientSession {
|
|
|
2232
2262
|
source: "web",
|
|
2233
2263
|
});
|
|
2234
2264
|
}
|
|
2235
|
-
for (const s of tuiInfos) {
|
|
2236
|
-
sessions.set(s.path, {
|
|
2237
|
-
path: s.path,
|
|
2238
|
-
name: s.name,
|
|
2239
|
-
firstMessage: s.firstMessage,
|
|
2240
|
-
messageCount: s.messageCount,
|
|
2241
|
-
modified: s.modified.getTime(),
|
|
2242
|
-
source: "tui",
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
2265
|
const sorted = [...sessions.values()].sort((a, b) => b.modified - a.modified);
|
|
2246
2266
|
this.emit({ type: "sessions", sessions: sorted });
|
|
2247
2267
|
}
|
|
@@ -2366,7 +2386,7 @@ export class ClientSession {
|
|
|
2366
2386
|
const map = new Map();
|
|
2367
2387
|
for (const p of saved.projects)
|
|
2368
2388
|
map.set(p.path, p.lastUsed);
|
|
2369
|
-
const all = await SessionManager.listAll(
|
|
2389
|
+
const all = await SessionManager.listAll();
|
|
2370
2390
|
for (const s of all) {
|
|
2371
2391
|
if (s.cwd) {
|
|
2372
2392
|
const t = s.modified.getTime();
|
|
@@ -2655,7 +2675,7 @@ export class ClientSession {
|
|
|
2655
2675
|
const newRuntime = await createAgentSessionRuntime(this.makeRuntimeFactory(), {
|
|
2656
2676
|
cwd: abs,
|
|
2657
2677
|
agentDir: this.agentDir,
|
|
2658
|
-
sessionManager: SessionManager.continueRecent(abs
|
|
2678
|
+
sessionManager: SessionManager.continueRecent(abs),
|
|
2659
2679
|
});
|
|
2660
2680
|
const conv = this.makeConversation(newRuntime);
|
|
2661
2681
|
this.convs.set(conv.id, conv);
|
|
@@ -2816,7 +2836,6 @@ export class ClientSession {
|
|
|
2816
2836
|
}
|
|
2817
2837
|
export class AgentService {
|
|
2818
2838
|
cwd;
|
|
2819
|
-
sessionDirRoot;
|
|
2820
2839
|
clients = new Map();
|
|
2821
2840
|
pending = new Map();
|
|
2822
2841
|
stateStore;
|
|
@@ -2825,9 +2844,8 @@ export class AgentService {
|
|
|
2825
2844
|
* self-update; returns whether the process will restart itself.
|
|
2826
2845
|
*/
|
|
2827
2846
|
onUpdateReady = undefined;
|
|
2828
|
-
constructor(cwd,
|
|
2847
|
+
constructor(cwd, stateFile) {
|
|
2829
2848
|
this.cwd = cwd;
|
|
2830
|
-
this.sessionDirRoot = sessionDirRoot;
|
|
2831
2849
|
this.stateStore = new ClientStateStore(stateFile);
|
|
2832
2850
|
}
|
|
2833
2851
|
/** Get or create the session for a client, racing attach calls safely. */
|
|
@@ -2852,7 +2870,8 @@ export class AgentService {
|
|
|
2852
2870
|
// gone (unmounted drive / deleted) — fall back to the default
|
|
2853
2871
|
}
|
|
2854
2872
|
}
|
|
2855
|
-
|
|
2873
|
+
// Sessions use the SDK default per-project dir — no per-client dir.
|
|
2874
|
+
const creating = ClientSession.create(clientId, cwd, this.stateStore).finally(() => {
|
|
2856
2875
|
this.pending.delete(clientId);
|
|
2857
2876
|
});
|
|
2858
2877
|
this.pending.set(clientId, creating);
|
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* Env:
|
|
10
10
|
* PORT HTTP port (default 8787)
|
|
11
11
|
* PI_WEB_CWD workspace the agent operates in (default: process.cwd())
|
|
12
|
-
* PI_WEB_DATA_DIR where per-client
|
|
12
|
+
* PI_WEB_DATA_DIR where per-client UI state is stored (client-state.json,
|
|
13
|
+
* default: <home>/.pi-web). Chat sessions are NOT stored here — they live
|
|
14
|
+
* in the pi agent's global TUI session dir (~/.pi/agent/sessions/--<cwd>--/)
|
|
15
|
+
* via the SDK default, so this web UI, the dev instance, and the pi CLI/TUI
|
|
16
|
+
* all share one conversation list per project.
|
|
13
17
|
* PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
|
|
14
18
|
*/
|
|
15
19
|
import { existsSync } from "node:fs";
|
|
@@ -18,16 +22,20 @@ import { createServer } from "node:http";
|
|
|
18
22
|
import { createConnection } from "node:net";
|
|
19
23
|
import { spawn } from "node:child_process";
|
|
20
24
|
import { basename, dirname, join, resolve } from "node:path";
|
|
25
|
+
import { homedir } from "node:os";
|
|
21
26
|
import { fileURLToPath } from "node:url";
|
|
22
27
|
import { randomUUID } from "node:crypto";
|
|
23
28
|
import express from "express";
|
|
24
29
|
import { WebSocket, WebSocketServer } from "ws";
|
|
25
|
-
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import { VERSION, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
26
31
|
import { AgentService, previewKind, workspacePath } from "./agent-service.js";
|
|
27
32
|
const PORT = Number(process.env.PORT ?? 8787);
|
|
28
33
|
const CWD = resolve(process.env.PI_WEB_CWD ?? process.cwd());
|
|
29
|
-
const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(
|
|
30
|
-
|
|
34
|
+
const DATA_DIR = resolve(process.env.PI_WEB_DATA_DIR ?? join(homedir(), ".pi-web"));
|
|
35
|
+
// Root of the SDK default per-project session dirs — chat transcripts live in
|
|
36
|
+
// <SESSION_DIR_ROOT>/--<cwd>--/, shared with the pi CLI/TUI (getAgentDir
|
|
37
|
+
// honors PI_CODING_AGENT_DIR).
|
|
38
|
+
const SESSION_DIR_ROOT = join(getAgentDir(), "sessions");
|
|
31
39
|
const app = express();
|
|
32
40
|
app.use(express.json({ limit: "10mb" }));
|
|
33
41
|
app.get("/api/health", (_req, res) => {
|
|
@@ -126,7 +134,7 @@ const heartbeatTimer = setInterval(() => {
|
|
|
126
134
|
}
|
|
127
135
|
}
|
|
128
136
|
}, 10_000);
|
|
129
|
-
const service = new AgentService(CWD,
|
|
137
|
+
const service = new AgentService(CWD,
|
|
130
138
|
// Per-client persisted UI state: last-used workspace + recent projects.
|
|
131
139
|
join(DATA_DIR, "client-state.json"));
|
|
132
140
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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
|
"type": "module",
|