pipane 0.1.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/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/pipane.js +22 -0
- package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +1 -0
- package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +1 -0
- package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +1 -0
- package/dist/client/assets/_node-stub_node_os-TTKJha15.js +1 -0
- package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +1 -0
- package/dist/client/assets/_node-stub_stream-DDexIdn_.js +1 -0
- package/dist/client/assets/index-CQP8RzjO.js +45 -0
- package/dist/client/assets/index-CVr_JVrA.js +131 -0
- package/dist/client/assets/index-DQxrqu4K.js +4412 -0
- package/dist/client/assets/index-DuJn-Nwv.js +3 -0
- package/dist/client/assets/index-Pj8zaBYJ.js +1 -0
- package/dist/client/assets/index-yL1A-vgM.css +1 -0
- package/dist/client/assets/pdf.worker.min-Cpi8b8z3.mjs +28 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/index.html +118 -0
- package/dist/server/server/attached-session.js +209 -0
- package/dist/server/server/load-trace-store.js +48 -0
- package/dist/server/server/local-settings.js +376 -0
- package/dist/server/server/pi-launch.js +10 -0
- package/dist/server/server/pi-runtime.js +32 -0
- package/dist/server/server/process-pool.js +254 -0
- package/dist/server/server/rest-api.js +289 -0
- package/dist/server/server/server.js +355 -0
- package/dist/server/server/session-cwd.js +33 -0
- package/dist/server/server/session-index.js +231 -0
- package/dist/server/server/session-jsonl.js +260 -0
- package/dist/server/server/session-lifecycle.js +155 -0
- package/dist/server/server/ws-handler.js +808 -0
- package/dist/server/shared/jsonl-sync.js +141 -0
- package/extensions/canvas.ts +57 -0
- package/package.json +82 -0
- package/patches/@mariozechner+pi-web-ui+0.55.3.patch +3279 -0
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
|
+
<title>pipane</title>
|
|
8
|
+
<script>
|
|
9
|
+
// Apply theme before first paint so the initial skeleton matches user prefs.
|
|
10
|
+
(function() {
|
|
11
|
+
var t = localStorage.getItem("theme") || "system";
|
|
12
|
+
var colorTheme = localStorage.getItem("color-theme") || "default";
|
|
13
|
+
var dark = t === "dark" || (t === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
14
|
+
var root = document.documentElement;
|
|
15
|
+
|
|
16
|
+
if (dark) root.classList.add("dark");
|
|
17
|
+
if (colorTheme && colorTheme !== "default") root.setAttribute("data-color-theme", colorTheme);
|
|
18
|
+
})();
|
|
19
|
+
</script>
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-DQxrqu4K.js"></script>
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-yL1A-vgM.css">
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div id="app">
|
|
25
|
+
<!-- Static skeleton shell: visible until JS renders the real app.
|
|
26
|
+
Uses only inline styles so it works before any CSS loads. -->
|
|
27
|
+
<div id="skeleton-shell" style="display:flex;flex-direction:column;width:100%;height:100vh;font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--sk-background, #ffffff);color:var(--sk-muted-foreground, #6b7280);">
|
|
28
|
+
<!-- Header bar -->
|
|
29
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 1rem;border-bottom:1px solid var(--sk-border, #e5e7eb);flex-shrink:0;">
|
|
30
|
+
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
31
|
+
<div class="skeleton-bar" style="width:18px;height:18px;border-radius:4px;"></div>
|
|
32
|
+
<div class="skeleton-bar" style="width:56px;height:14px;border-radius:4px;"></div>
|
|
33
|
+
</div>
|
|
34
|
+
<div style="display:flex;gap:0.35rem;">
|
|
35
|
+
<div class="skeleton-bar" style="width:18px;height:18px;border-radius:4px;"></div>
|
|
36
|
+
<div class="skeleton-bar" style="width:18px;height:18px;border-radius:4px;"></div>
|
|
37
|
+
<div class="skeleton-bar" style="width:18px;height:18px;border-radius:4px;"></div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<!-- Body: sidebar + content -->
|
|
41
|
+
<div style="display:flex;flex:1;overflow:hidden;">
|
|
42
|
+
<!-- Sidebar skeleton -->
|
|
43
|
+
<div style="width:280px;flex-shrink:0;border-right:1px solid var(--sk-border, #e5e7eb);padding:0;">
|
|
44
|
+
<!-- Sessions header -->
|
|
45
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:0.5rem 0.75rem;border-bottom:1px solid var(--sk-border, #e5e7eb);">
|
|
46
|
+
<div class="skeleton-bar" style="width:64px;height:12px;border-radius:3px;"></div>
|
|
47
|
+
<div class="skeleton-bar" style="width:44px;height:20px;border-radius:4px;"></div>
|
|
48
|
+
</div>
|
|
49
|
+
<!-- Search bar -->
|
|
50
|
+
<div style="padding:0.5rem 0.75rem;border-bottom:1px solid var(--sk-border, #e5e7eb);">
|
|
51
|
+
<div class="skeleton-bar" style="width:100%;height:28px;border-radius:4px;"></div>
|
|
52
|
+
</div>
|
|
53
|
+
<!-- Skeleton session items -->
|
|
54
|
+
<div style="padding:0.25rem 0;">
|
|
55
|
+
<!-- Group header -->
|
|
56
|
+
<div style="padding:0.35rem 0.75rem;margin-top:0.25rem;">
|
|
57
|
+
<div class="skeleton-bar" style="width:72px;height:10px;border-radius:3px;"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<!-- Session items (6 placeholders) -->
|
|
60
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:80%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:45%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
61
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:65%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:55%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
62
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:90%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:40%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
63
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:70%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:50%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
64
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:85%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:35%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
65
|
+
<div class="skeleton-session-item"><div class="skeleton-bar" style="width:60%;height:11px;border-radius:3px;"></div><div class="skeleton-bar" style="width:48%;height:9px;border-radius:3px;margin-top:5px;"></div></div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<!-- Main content area (empty) -->
|
|
69
|
+
<div style="flex:1;"></div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
<style>
|
|
74
|
+
/* Skeleton styles — inlined for instant paint before CSS bundle loads */
|
|
75
|
+
:root {
|
|
76
|
+
--sk-background: #ffffff;
|
|
77
|
+
--sk-border: #e5e7eb;
|
|
78
|
+
--sk-muted: #e5e7eb;
|
|
79
|
+
--sk-muted-foreground: #6b7280;
|
|
80
|
+
}
|
|
81
|
+
.dark {
|
|
82
|
+
--sk-background: #111827;
|
|
83
|
+
--sk-border: #374151;
|
|
84
|
+
--sk-muted: #374151;
|
|
85
|
+
--sk-muted-foreground: #9ca3af;
|
|
86
|
+
}
|
|
87
|
+
[data-color-theme="gruvbox"] {
|
|
88
|
+
--sk-background: #fbf1c7;
|
|
89
|
+
--sk-border: #bdae93;
|
|
90
|
+
--sk-muted: #ebdbb2;
|
|
91
|
+
--sk-muted-foreground: #7c6f64;
|
|
92
|
+
}
|
|
93
|
+
.dark[data-color-theme="gruvbox"],
|
|
94
|
+
[data-color-theme="gruvbox"].dark {
|
|
95
|
+
--sk-background: #282828;
|
|
96
|
+
--sk-border: #665c54;
|
|
97
|
+
--sk-muted: #504945;
|
|
98
|
+
--sk-muted-foreground: #a89984;
|
|
99
|
+
}
|
|
100
|
+
html, body, #app {
|
|
101
|
+
height: 100%;
|
|
102
|
+
margin: 0;
|
|
103
|
+
background: var(--sk-background, #ffffff);
|
|
104
|
+
}
|
|
105
|
+
.skeleton-bar {
|
|
106
|
+
background: var(--sk-muted, #e5e7eb);
|
|
107
|
+
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
|
108
|
+
}
|
|
109
|
+
.skeleton-session-item {
|
|
110
|
+
padding: 0.4rem 0.75rem;
|
|
111
|
+
}
|
|
112
|
+
@keyframes skeleton-pulse {
|
|
113
|
+
0%, 100% { opacity: 0.6; }
|
|
114
|
+
50% { opacity: 0.25; }
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attached session state — only exists while a pi process is running a turn.
|
|
3
|
+
*
|
|
4
|
+
* This is the in-memory state for sessions that have a pi process attached.
|
|
5
|
+
* It accumulates streaming events (message_start, message_update, message_end,
|
|
6
|
+
* tool_execution_start/end) into a canonical state.
|
|
7
|
+
*
|
|
8
|
+
* When the turn ends (agent_end), this object is discarded and the final
|
|
9
|
+
* authoritative state is read from the JSONL file on disk.
|
|
10
|
+
*
|
|
11
|
+
* Detached sessions have NO in-memory state — they are read from disk on demand.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
14
|
+
import { parseSessionEntries, buildSessionContext, } from "@mariozechner/pi-coding-agent";
|
|
15
|
+
// ── Attached session ───────────────────────────────────────────────────────
|
|
16
|
+
export class AttachedSession {
|
|
17
|
+
messages;
|
|
18
|
+
streamMessage = null;
|
|
19
|
+
pendingToolCalls = [];
|
|
20
|
+
/** Partial tool results from tool_execution_update, keyed by toolCallId */
|
|
21
|
+
partialToolResults = {};
|
|
22
|
+
model;
|
|
23
|
+
thinkingLevel;
|
|
24
|
+
steeringQueue = [];
|
|
25
|
+
error;
|
|
26
|
+
/** Monotonically increasing version for change detection */
|
|
27
|
+
_version = 1;
|
|
28
|
+
/** Whether the stream message changed since last push */
|
|
29
|
+
_streamDirty = false;
|
|
30
|
+
constructor(init) {
|
|
31
|
+
this.messages = init.messages;
|
|
32
|
+
this.model = init.model;
|
|
33
|
+
this.thinkingLevel = init.thinkingLevel;
|
|
34
|
+
}
|
|
35
|
+
get version() { return this._version; }
|
|
36
|
+
/**
|
|
37
|
+
* Apply a streaming event from the pi process.
|
|
38
|
+
* Returns true if state changed.
|
|
39
|
+
*/
|
|
40
|
+
applyEvent(event) {
|
|
41
|
+
switch (event.type) {
|
|
42
|
+
case "agent_start":
|
|
43
|
+
this.error = undefined;
|
|
44
|
+
this.streamMessage = null;
|
|
45
|
+
this._version++;
|
|
46
|
+
return true;
|
|
47
|
+
case "message_start":
|
|
48
|
+
this.streamMessage = event.message;
|
|
49
|
+
this._streamDirty = true;
|
|
50
|
+
this._version++;
|
|
51
|
+
return true;
|
|
52
|
+
case "message_update":
|
|
53
|
+
this.streamMessage = event.message;
|
|
54
|
+
this._streamDirty = true;
|
|
55
|
+
this._version++;
|
|
56
|
+
return true;
|
|
57
|
+
case "message_end": {
|
|
58
|
+
this.streamMessage = null;
|
|
59
|
+
const msg = event.message;
|
|
60
|
+
if (msg) {
|
|
61
|
+
this.messages = [...this.messages, msg];
|
|
62
|
+
}
|
|
63
|
+
this._version++;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
case "turn_end": {
|
|
67
|
+
if (event.message?.role === "assistant" && event.message?.errorMessage) {
|
|
68
|
+
this.error = event.message.errorMessage;
|
|
69
|
+
this._version++;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
case "tool_execution_start": {
|
|
75
|
+
const toolCallId = event.toolCallId;
|
|
76
|
+
if (toolCallId && !this.pendingToolCalls.includes(toolCallId)) {
|
|
77
|
+
this.pendingToolCalls = [...this.pendingToolCalls, toolCallId];
|
|
78
|
+
this._streamDirty = true;
|
|
79
|
+
this._version++;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
case "tool_execution_update": {
|
|
85
|
+
const toolCallId = event.toolCallId;
|
|
86
|
+
const partialResult = event.partialResult;
|
|
87
|
+
if (toolCallId && partialResult) {
|
|
88
|
+
this.partialToolResults = { ...this.partialToolResults, [toolCallId]: partialResult };
|
|
89
|
+
this._streamDirty = true;
|
|
90
|
+
this._version++;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
case "tool_execution_end": {
|
|
96
|
+
const toolCallId = event.toolCallId;
|
|
97
|
+
if (toolCallId) {
|
|
98
|
+
this.pendingToolCalls = this.pendingToolCalls.filter(id => id !== toolCallId);
|
|
99
|
+
// Clear partial result for this tool
|
|
100
|
+
if (toolCallId in this.partialToolResults) {
|
|
101
|
+
const { [toolCallId]: _, ...rest } = this.partialToolResults;
|
|
102
|
+
this.partialToolResults = rest;
|
|
103
|
+
}
|
|
104
|
+
this._streamDirty = true;
|
|
105
|
+
this._version++;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a full snapshot of the current state.
|
|
116
|
+
*/
|
|
117
|
+
toSnapshot() {
|
|
118
|
+
return {
|
|
119
|
+
messages: this.messages,
|
|
120
|
+
streamMessage: this.streamMessage,
|
|
121
|
+
status: "streaming",
|
|
122
|
+
pendingToolCalls: this.pendingToolCalls,
|
|
123
|
+
partialToolResults: this.partialToolResults,
|
|
124
|
+
model: this.model,
|
|
125
|
+
thinkingLevel: this.thinkingLevel,
|
|
126
|
+
steeringQueue: this.steeringQueue,
|
|
127
|
+
error: this.error,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Compute the cheapest update op to send to a client.
|
|
132
|
+
*
|
|
133
|
+
* If only streamMessage/pendingToolCalls changed (the high-frequency case
|
|
134
|
+
* during streaming), returns a lightweight stream_delta.
|
|
135
|
+
* Otherwise returns a full snapshot.
|
|
136
|
+
*
|
|
137
|
+
* Returns null if nothing changed since the client's last known version.
|
|
138
|
+
*/
|
|
139
|
+
computeUpdateOp(clientVersion, clientMessageCount) {
|
|
140
|
+
if (this._version === clientVersion)
|
|
141
|
+
return null;
|
|
142
|
+
// If client has never seen this session, or messages changed, send snapshot
|
|
143
|
+
if (clientVersion === 0
|
|
144
|
+
|| this.messages.length !== clientMessageCount
|
|
145
|
+
|| this.messages.length < clientMessageCount) {
|
|
146
|
+
this._streamDirty = false;
|
|
147
|
+
return { op: "snapshot", state: this.toSnapshot() };
|
|
148
|
+
}
|
|
149
|
+
// Messages didn't change — only stream state changed
|
|
150
|
+
if (this._streamDirty) {
|
|
151
|
+
this._streamDirty = false;
|
|
152
|
+
return {
|
|
153
|
+
op: "stream_delta",
|
|
154
|
+
streamMessage: this.streamMessage,
|
|
155
|
+
pendingToolCalls: this.pendingToolCalls,
|
|
156
|
+
partialToolResults: this.partialToolResults,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Something else changed (error, steering queue, etc.) — send snapshot
|
|
160
|
+
return { op: "snapshot", state: this.toSnapshot() };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// ── Disk reader ────────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Read a session from disk and return a snapshot.
|
|
166
|
+
* Used for detached sessions (no pi process running).
|
|
167
|
+
*/
|
|
168
|
+
export function readSessionFromDisk(sessionPath) {
|
|
169
|
+
let messages = [];
|
|
170
|
+
let model = null;
|
|
171
|
+
let thinkingLevel = "off";
|
|
172
|
+
try {
|
|
173
|
+
if (existsSync(sessionPath)) {
|
|
174
|
+
const content = readFileSync(sessionPath, "utf8");
|
|
175
|
+
const entries = parseSessionEntries(content);
|
|
176
|
+
const context = buildSessionContext(entries);
|
|
177
|
+
messages = context.messages ?? [];
|
|
178
|
+
model = context.model ?? null;
|
|
179
|
+
thinkingLevel = context.thinkingLevel ?? "off";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
console.error(`[session] Failed to read session ${sessionPath}:`, err);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
messages,
|
|
187
|
+
streamMessage: null,
|
|
188
|
+
status: "idle",
|
|
189
|
+
pendingToolCalls: [],
|
|
190
|
+
partialToolResults: {},
|
|
191
|
+
model,
|
|
192
|
+
thinkingLevel,
|
|
193
|
+
steeringQueue: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get the file size of a session file. Returns 0 if the file doesn't exist.
|
|
198
|
+
*/
|
|
199
|
+
export function getSessionFileSize(sessionPath) {
|
|
200
|
+
try {
|
|
201
|
+
if (existsSync(sessionPath)) {
|
|
202
|
+
return statSync(sessionPath).size;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// ignore
|
|
207
|
+
}
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const MAX_TRACES = 50;
|
|
2
|
+
const MAX_EVENTS_PER_TRACE = 1000;
|
|
3
|
+
export class LoadTraceStore {
|
|
4
|
+
traces = new Map();
|
|
5
|
+
record(traceId, event) {
|
|
6
|
+
if (!traceId)
|
|
7
|
+
return;
|
|
8
|
+
const trace = this.ensure(traceId);
|
|
9
|
+
trace.events.push(event);
|
|
10
|
+
if (trace.events.length > MAX_EVENTS_PER_TRACE) {
|
|
11
|
+
trace.events.splice(0, trace.events.length - MAX_EVENTS_PER_TRACE);
|
|
12
|
+
}
|
|
13
|
+
trace.updatedAt = event.ts;
|
|
14
|
+
}
|
|
15
|
+
get(traceId) {
|
|
16
|
+
return this.traces.get(traceId);
|
|
17
|
+
}
|
|
18
|
+
getLatest() {
|
|
19
|
+
return Array.from(this.traces.values())
|
|
20
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
21
|
+
.slice(0, 10);
|
|
22
|
+
}
|
|
23
|
+
ensure(traceId) {
|
|
24
|
+
let trace = this.traces.get(traceId);
|
|
25
|
+
if (!trace) {
|
|
26
|
+
const now = new Date().toISOString();
|
|
27
|
+
trace = {
|
|
28
|
+
traceId,
|
|
29
|
+
startedAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
events: [],
|
|
32
|
+
};
|
|
33
|
+
this.traces.set(traceId, trace);
|
|
34
|
+
this.compact();
|
|
35
|
+
}
|
|
36
|
+
return trace;
|
|
37
|
+
}
|
|
38
|
+
compact() {
|
|
39
|
+
if (this.traces.size <= MAX_TRACES)
|
|
40
|
+
return;
|
|
41
|
+
const oldest = Array.from(this.traces.values())
|
|
42
|
+
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt))
|
|
43
|
+
.slice(0, this.traces.size - MAX_TRACES);
|
|
44
|
+
for (const trace of oldest) {
|
|
45
|
+
this.traces.delete(trace.traceId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|