@narumitw/pi-webui 0.20.2
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 +160 -0
- package/package.json +51 -0
- package/src/conversation.ts +374 -0
- package/src/images.ts +221 -0
- package/src/pi-settings.ts +74 -0
- package/src/runtime.ts +697 -0
- package/src/server.ts +518 -0
- package/src/settings.ts +174 -0
- package/src/web/app.js +537 -0
- package/src/web/index.html +108 -0
- package/src/web/markdown.js +198 -0
- package/src/web/state.js +166 -0
- package/src/web/styles.css +781 -0
- package/src/web/transcript.js +222 -0
- package/src/webui.ts +9 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { renderMarkdown } from "./markdown.js";
|
|
2
|
+
|
|
3
|
+
export function createTranscriptRenderer({ documentRef = document, list }) {
|
|
4
|
+
const messages = new Map();
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
render(nextMessages, tools) {
|
|
8
|
+
const toolById = new Map(tools.map((tool) => [tool.id, tool]));
|
|
9
|
+
const retained = new Set();
|
|
10
|
+
const changed = [];
|
|
11
|
+
let cursor = list.firstChild;
|
|
12
|
+
for (const message of nextMessages) {
|
|
13
|
+
retained.add(message.id);
|
|
14
|
+
let view = messages.get(message.id);
|
|
15
|
+
if (!view) {
|
|
16
|
+
view = createMessageView(message.role, documentRef);
|
|
17
|
+
messages.set(message.id, view);
|
|
18
|
+
}
|
|
19
|
+
if (updateMessageView(view, message, toolById, documentRef)) changed.push(message.id);
|
|
20
|
+
if (view.node !== cursor) list.insertBefore(view.node, cursor);
|
|
21
|
+
cursor = view.node.nextSibling;
|
|
22
|
+
}
|
|
23
|
+
for (const [id, view] of messages) {
|
|
24
|
+
if (retained.has(id)) continue;
|
|
25
|
+
view.node.remove();
|
|
26
|
+
messages.delete(id);
|
|
27
|
+
}
|
|
28
|
+
return changed;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCollapsibleMessageRole(role) {
|
|
34
|
+
return role === "toolResult";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function toolPhaseLabel(tool) {
|
|
38
|
+
if (tool?.isError) return "Failed";
|
|
39
|
+
if (tool?.phase === "end") return "Completed";
|
|
40
|
+
if (tool?.phase === "start" || tool?.phase === "update") return "Running";
|
|
41
|
+
return "Requested";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toolCommandPreview(tool) {
|
|
45
|
+
const command = tool?.args?.command;
|
|
46
|
+
if (typeof command !== "string") return "";
|
|
47
|
+
return command.length > 120 ? `${command.slice(0, 120)}…` : command;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createMessageView(role, documentRef) {
|
|
51
|
+
const node = documentRef.createElement("li");
|
|
52
|
+
const body = documentRef.createElement("div");
|
|
53
|
+
body.className = "message-body";
|
|
54
|
+
let heading;
|
|
55
|
+
if (isCollapsibleMessageRole(role)) {
|
|
56
|
+
const disclosure = documentRef.createElement("details");
|
|
57
|
+
disclosure.className = "tool-result-disclosure";
|
|
58
|
+
heading = documentRef.createElement("summary");
|
|
59
|
+
heading.className = "message-heading";
|
|
60
|
+
disclosure.append(heading, body);
|
|
61
|
+
node.append(disclosure);
|
|
62
|
+
} else {
|
|
63
|
+
heading = documentRef.createElement("div");
|
|
64
|
+
heading.className = "message-heading";
|
|
65
|
+
node.append(heading, body);
|
|
66
|
+
}
|
|
67
|
+
return { node, heading, body, blocks: new Map(), role: "", final: undefined };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function updateMessageView(view, message, toolById, documentRef) {
|
|
71
|
+
let changed = false;
|
|
72
|
+
const role = knownRole(message.role);
|
|
73
|
+
if (view.role !== role) {
|
|
74
|
+
view.node.className = `message ${role}`;
|
|
75
|
+
view.role = role;
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
const heading = roleLabel(message);
|
|
79
|
+
if (view.final !== message.final || view.heading.textContent !== heading) {
|
|
80
|
+
view.heading.textContent = heading;
|
|
81
|
+
view.final = message.final;
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const retained = new Set();
|
|
86
|
+
let cursor = view.body.firstChild;
|
|
87
|
+
for (const [index, block] of (message.content ?? []).entries()) {
|
|
88
|
+
const key = `${index}:${block.type}:${block.id ?? ""}`;
|
|
89
|
+
retained.add(key);
|
|
90
|
+
let blockView = view.blocks.get(key);
|
|
91
|
+
if (!blockView) {
|
|
92
|
+
blockView = createBlockView(block, documentRef);
|
|
93
|
+
view.blocks.set(key, blockView);
|
|
94
|
+
changed = true;
|
|
95
|
+
}
|
|
96
|
+
if (updateBlockView(blockView, block, toolById.get(block.id), documentRef)) changed = true;
|
|
97
|
+
if (blockView.node !== cursor) view.body.insertBefore(blockView.node, cursor);
|
|
98
|
+
cursor = blockView.node.nextSibling;
|
|
99
|
+
}
|
|
100
|
+
for (const [key, blockView] of view.blocks) {
|
|
101
|
+
if (retained.has(key)) continue;
|
|
102
|
+
blockView.node.remove();
|
|
103
|
+
view.blocks.delete(key);
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const errorText = message.errorMessage ?? "";
|
|
108
|
+
if (errorText) {
|
|
109
|
+
if (!view.error) {
|
|
110
|
+
view.error = documentRef.createElement("p");
|
|
111
|
+
view.error.className = "message-error";
|
|
112
|
+
view.body.append(view.error);
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
if (view.error.textContent !== errorText) {
|
|
116
|
+
view.error.textContent = errorText;
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
} else if (view.error) {
|
|
120
|
+
view.error.remove();
|
|
121
|
+
view.error = undefined;
|
|
122
|
+
changed = true;
|
|
123
|
+
}
|
|
124
|
+
return changed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createBlockView(block, documentRef) {
|
|
128
|
+
if (block.type === "thinking") {
|
|
129
|
+
const node = documentRef.createElement("details");
|
|
130
|
+
node.className = "thinking";
|
|
131
|
+
const summary = documentRef.createElement("summary");
|
|
132
|
+
summary.textContent = "Thinking";
|
|
133
|
+
const text = documentRef.createElement("pre");
|
|
134
|
+
node.append(summary, text);
|
|
135
|
+
return { type: block.type, node, text, value: undefined };
|
|
136
|
+
}
|
|
137
|
+
if (block.type === "toolCall") return createToolView(documentRef);
|
|
138
|
+
const node = documentRef.createElement(block.type === "image" ? "span" : "div");
|
|
139
|
+
if (block.type === "image") node.className = "image-chip";
|
|
140
|
+
else node.className = "message-markdown";
|
|
141
|
+
return { type: block.type, node, value: undefined };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function updateBlockView(view, block, tool, documentRef) {
|
|
145
|
+
if (block.type === "text") {
|
|
146
|
+
if (view.value === block.text) return false;
|
|
147
|
+
view.value = block.text;
|
|
148
|
+
view.node.replaceChildren(renderMarkdown(block.text, documentRef));
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (block.type === "thinking") {
|
|
152
|
+
if (view.value === block.text) return false;
|
|
153
|
+
view.value = block.text;
|
|
154
|
+
view.text.textContent = block.text;
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
if (block.type === "image") {
|
|
158
|
+
const label = `Image${block.mimeType ? ` · ${block.mimeType}` : ""}`;
|
|
159
|
+
if (view.value === label) return false;
|
|
160
|
+
view.value = label;
|
|
161
|
+
view.node.textContent = label;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (block.type === "toolCall") return updateToolView(view, block, tool);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createToolView(documentRef) {
|
|
169
|
+
const node = documentRef.createElement("details");
|
|
170
|
+
node.className = "tool";
|
|
171
|
+
const summary = documentRef.createElement("summary");
|
|
172
|
+
const title = documentRef.createElement("span");
|
|
173
|
+
const command = documentRef.createElement("code");
|
|
174
|
+
command.className = "tool-command";
|
|
175
|
+
const args = documentRef.createElement("pre");
|
|
176
|
+
args.className = "tool-arguments";
|
|
177
|
+
const result = documentRef.createElement("pre");
|
|
178
|
+
result.className = "tool-result";
|
|
179
|
+
summary.append(title, command);
|
|
180
|
+
node.append(summary, args, result);
|
|
181
|
+
return { type: "toolCall", node, title, command, args, result, value: undefined };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function updateToolView(view, call, tool) {
|
|
185
|
+
const phase = toolPhaseLabel(tool);
|
|
186
|
+
const command = toolCommandPreview(tool);
|
|
187
|
+
const args = safeJson(tool?.args ?? call.arguments);
|
|
188
|
+
const result = tool?.result === undefined ? "" : safeJson(tool.result);
|
|
189
|
+
const value = JSON.stringify([call.name, phase, command, args, result, Boolean(tool?.isError)]);
|
|
190
|
+
if (view.value === value) return false;
|
|
191
|
+
view.value = value;
|
|
192
|
+
view.node.className = `tool ${tool?.isError ? "failed" : ""}`;
|
|
193
|
+
view.title.textContent = `${call.name} · ${phase}`;
|
|
194
|
+
view.command.textContent = command;
|
|
195
|
+
view.command.hidden = !command;
|
|
196
|
+
view.args.textContent = args;
|
|
197
|
+
view.result.textContent = result;
|
|
198
|
+
view.result.hidden = !result;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function roleLabel(message) {
|
|
203
|
+
if (message.role === "user") return "You";
|
|
204
|
+
if (message.role === "assistant") return message.final ? "Pi" : "Pi · Streaming";
|
|
205
|
+
if (message.role === "toolResult") {
|
|
206
|
+
return message.toolName ? `Tool · ${message.toolName}` : "Tool";
|
|
207
|
+
}
|
|
208
|
+
return message.role;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function knownRole(role) {
|
|
212
|
+
if (role === "user" || role === "assistant" || role === "toolResult") return role;
|
|
213
|
+
return "other";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safeJson(value) {
|
|
217
|
+
try {
|
|
218
|
+
return JSON.stringify(value, null, 2);
|
|
219
|
+
} catch {
|
|
220
|
+
return "Details unavailable";
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/webui.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { WebUIRuntime } from "./runtime.js";
|
|
3
|
+
|
|
4
|
+
export default function webUI(pi: ExtensionAPI): void {
|
|
5
|
+
const runtime = new WebUIRuntime(pi);
|
|
6
|
+
runtime.register();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { WebUIRuntime } from "./runtime.js";
|