@larose/pi-web 0.3.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.
Files changed (43) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +50 -0
  3. package/THIRD_PARTY_LICENSES.md +40 -0
  4. package/dist/client/home.js +1619 -0
  5. package/dist/client/session.js +3703 -0
  6. package/dist/server/api.js +485 -0
  7. package/dist/server/cli.js +51 -0
  8. package/dist/server/directory-browser.js +104 -0
  9. package/dist/server/errors.js +10 -0
  10. package/dist/server/event-buffer.js +40 -0
  11. package/dist/server/extension-ui.js +245 -0
  12. package/dist/server/git-workspaces.js +559 -0
  13. package/dist/server/runtime-registry.js +703 -0
  14. package/dist/server/server.js +190 -0
  15. package/dist/server/session-repository.js +374 -0
  16. package/package.json +46 -0
  17. package/public/home.html +139 -0
  18. package/public/session.html +144 -0
  19. package/public/styles.css +2463 -0
  20. package/screenshots/home.png +0 -0
  21. package/screenshots/session.png +0 -0
  22. package/src/client/display-title.ts +36 -0
  23. package/src/client/event-stream.ts +194 -0
  24. package/src/client/home.ts +1575 -0
  25. package/src/client/markdown.ts +98 -0
  26. package/src/client/message-queue.ts +67 -0
  27. package/src/client/path-combobox.ts +271 -0
  28. package/src/client/session.ts +2174 -0
  29. package/src/client/shared.ts +99 -0
  30. package/src/client/slash-completion.ts +184 -0
  31. package/src/client/transcript-activity.ts +188 -0
  32. package/src/client/usage-format.ts +156 -0
  33. package/src/client/workspace-browser.ts +36 -0
  34. package/src/server/api.ts +652 -0
  35. package/src/server/cli.ts +63 -0
  36. package/src/server/directory-browser.ts +137 -0
  37. package/src/server/errors.ts +11 -0
  38. package/src/server/event-buffer.ts +59 -0
  39. package/src/server/extension-ui.ts +359 -0
  40. package/src/server/git-workspaces.ts +750 -0
  41. package/src/server/runtime-registry.ts +943 -0
  42. package/src/server/server.ts +248 -0
  43. package/src/server/session-repository.ts +488 -0
@@ -0,0 +1,245 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Theme } from "@earendil-works/pi-coding-agent";
3
+ // The browser supplies its own colors. Extensions still need a complete Theme
4
+ // object for formatting status and widget text, so keep those operations plain.
5
+ class PlainTextTheme extends Theme {
6
+ constructor() {
7
+ super({ text: "", muted: "", thinkingXhigh: "", searchMatchText: "" }, { selectedBg: "" }, "truecolor");
8
+ }
9
+ fg(...[, text]) {
10
+ return text;
11
+ }
12
+ bg(...[, text]) {
13
+ return text;
14
+ }
15
+ bold(text) {
16
+ return text;
17
+ }
18
+ italic(text) {
19
+ return text;
20
+ }
21
+ underline(text) {
22
+ return text;
23
+ }
24
+ inverse(text) {
25
+ return text;
26
+ }
27
+ strikethrough(text) {
28
+ return text;
29
+ }
30
+ getFgAnsi() {
31
+ return "";
32
+ }
33
+ getBgAnsi() {
34
+ return "";
35
+ }
36
+ getThinkingBorderColor() {
37
+ return (text) => text;
38
+ }
39
+ getBashModeBorderColor() {
40
+ return (text) => text;
41
+ }
42
+ }
43
+ const plainTextTheme = new PlainTextTheme();
44
+ export class WebExtensionUI {
45
+ pending = new Map();
46
+ statuses = new Map();
47
+ widgets = new Map();
48
+ publisher;
49
+ queuedEvents = [];
50
+ available = true;
51
+ context = {
52
+ select: (title, options, opts) => this.request({ method: "select", title, options: [...options] }, undefined, (response) => ("value" in response ? response.value : undefined), opts),
53
+ confirm: (title, message, opts) => this.request({ method: "confirm", title, message }, false, (response) => ("confirmed" in response ? response.confirmed : false), opts),
54
+ input: (title, placeholder, opts) => this.request({ method: "input", title, ...(placeholder === undefined ? {} : { placeholder }) }, undefined, (response) => ("value" in response ? response.value : undefined), opts),
55
+ editor: (title, prefill) => this.request({ method: "editor", title, ...(prefill === undefined ? {} : { prefill }) }, undefined, (response) => "value" in response ? response.value : undefined),
56
+ notify: (message, type) => this.emit({
57
+ type: "extension_ui_request",
58
+ id: randomUUID(),
59
+ method: "notify",
60
+ message,
61
+ ...(type === undefined ? {} : { notifyType: type }),
62
+ }),
63
+ onTerminalInput: () => () => undefined,
64
+ setStatus: (key, text) => {
65
+ if (text === undefined) {
66
+ this.statuses.delete(key);
67
+ }
68
+ else {
69
+ this.statuses.set(key, text);
70
+ }
71
+ this.emit({
72
+ type: "extension_ui_request",
73
+ id: randomUUID(),
74
+ method: "setStatus",
75
+ statusKey: key,
76
+ ...(text === undefined ? {} : { statusText: text }),
77
+ });
78
+ },
79
+ setWorkingMessage: () => undefined,
80
+ setWorkingVisible: () => undefined,
81
+ setWorkingIndicator: () => undefined,
82
+ setHiddenThinkingLabel: () => undefined,
83
+ setWidget: (key, content, options) => {
84
+ if (content !== undefined && !Array.isArray(content)) {
85
+ return;
86
+ }
87
+ const placement = options?.placement ?? "aboveEditor";
88
+ if (content === undefined) {
89
+ this.widgets.delete(key);
90
+ }
91
+ else {
92
+ this.widgets.set(key, { lines: [...content], placement });
93
+ }
94
+ this.emit({
95
+ type: "extension_ui_request",
96
+ id: randomUUID(),
97
+ method: "setWidget",
98
+ widgetKey: key,
99
+ ...(content === undefined ? {} : { widgetLines: [...content] }),
100
+ ...(options?.placement === undefined ? {} : { widgetPlacement: options.placement }),
101
+ });
102
+ },
103
+ setFooter: () => undefined,
104
+ setHeader: () => undefined,
105
+ setTitle: (title) => this.emit({
106
+ type: "extension_ui_request",
107
+ id: randomUUID(),
108
+ method: "setTitle",
109
+ title,
110
+ }),
111
+ custom: async () => undefined,
112
+ pasteToEditor: (text) => this.emit({
113
+ type: "extension_ui_request",
114
+ id: randomUUID(),
115
+ method: "set_editor_text",
116
+ text,
117
+ }),
118
+ setEditorText: (text) => this.emit({
119
+ type: "extension_ui_request",
120
+ id: randomUUID(),
121
+ method: "set_editor_text",
122
+ text,
123
+ }),
124
+ getEditorText: () => "",
125
+ addAutocompleteProvider: () => undefined,
126
+ setEditorComponent: () => undefined,
127
+ getEditorComponent: () => undefined,
128
+ get theme() {
129
+ return plainTextTheme;
130
+ },
131
+ getAllThemes: () => [],
132
+ getTheme: () => undefined,
133
+ setTheme: () => ({ success: false, error: "Theme switching is not supported in Pi Web" }),
134
+ getToolsExpanded: () => false,
135
+ setToolsExpanded: () => undefined,
136
+ };
137
+ setPublisher(publisher) {
138
+ this.publisher = publisher;
139
+ const queued = this.queuedEvents;
140
+ this.queuedEvents = [];
141
+ for (const event of queued) {
142
+ publisher(event);
143
+ }
144
+ return () => {
145
+ if (this.publisher === publisher) {
146
+ this.publisher = undefined;
147
+ }
148
+ };
149
+ }
150
+ snapshot() {
151
+ return {
152
+ pending: [...this.pending.values()].map(({ request }) => ({ ...request })),
153
+ statuses: [...this.statuses].map(([key, text]) => ({ key, text })),
154
+ widgets: [...this.widgets].map(([key, widget]) => ({
155
+ key,
156
+ lines: [...widget.lines],
157
+ placement: widget.placement,
158
+ })),
159
+ };
160
+ }
161
+ respond(response) {
162
+ const dialog = this.pending.get(response.id);
163
+ if (!dialog) {
164
+ return false;
165
+ }
166
+ this.settle(response.id, dialog.parse(response));
167
+ return true;
168
+ }
169
+ cancelPending() {
170
+ for (const [id, dialog] of [...this.pending]) {
171
+ this.settle(id, dialog.defaultValue);
172
+ }
173
+ }
174
+ reset() {
175
+ this.cancelPending();
176
+ this.statuses.clear();
177
+ this.widgets.clear();
178
+ this.emit({ type: "extension_ui_reset" });
179
+ }
180
+ dispose() {
181
+ if (!this.available) {
182
+ return;
183
+ }
184
+ this.reset();
185
+ this.available = false;
186
+ this.publisher = undefined;
187
+ this.queuedEvents = [];
188
+ }
189
+ request(request, defaultValue, parse, options) {
190
+ if (!this.available || options?.signal?.aborted) {
191
+ return Promise.resolve(defaultValue);
192
+ }
193
+ const id = randomUUID();
194
+ const fullRequest = {
195
+ id,
196
+ ...request,
197
+ ...(options?.timeout === undefined ? {} : { timeout: options.timeout }),
198
+ };
199
+ return new Promise((resolve) => {
200
+ const pending = {
201
+ request: fullRequest,
202
+ defaultValue,
203
+ parse,
204
+ resolve,
205
+ ...(options?.signal === undefined ? {} : { signal: options.signal }),
206
+ };
207
+ if (options?.timeout !== undefined) {
208
+ pending.timeout = setTimeout(() => this.settle(id, defaultValue), options.timeout);
209
+ pending.timeout.unref();
210
+ }
211
+ if (options?.signal) {
212
+ pending.abort = () => this.settle(id, defaultValue);
213
+ options.signal.addEventListener("abort", pending.abort, { once: true });
214
+ }
215
+ this.pending.set(id, pending);
216
+ this.emit({ type: "extension_ui_request", ...fullRequest });
217
+ });
218
+ }
219
+ settle(id, value) {
220
+ const dialog = this.pending.get(id);
221
+ if (!dialog) {
222
+ return;
223
+ }
224
+ this.pending.delete(id);
225
+ if (dialog.timeout) {
226
+ clearTimeout(dialog.timeout);
227
+ }
228
+ if (dialog.signal && dialog.abort) {
229
+ dialog.signal.removeEventListener("abort", dialog.abort);
230
+ }
231
+ dialog.resolve(value);
232
+ this.emit({ type: "extension_ui_closed", id });
233
+ }
234
+ emit(event) {
235
+ if (!this.available) {
236
+ return;
237
+ }
238
+ if (this.publisher) {
239
+ this.publisher(event);
240
+ }
241
+ else {
242
+ this.queuedEvents.push(event);
243
+ }
244
+ }
245
+ }