pi-tau-web-server 1.0.8

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 (98) hide show
  1. package/README.md +303 -0
  2. package/bin/auth.js +96 -0
  3. package/bin/config.js +99 -0
  4. package/bin/model-utils.js +134 -0
  5. package/bin/server-main.js +1189 -0
  6. package/bin/sessions.js +492 -0
  7. package/bin/tau.js +8 -0
  8. package/bin/tree.js +248 -0
  9. package/bin/types.js +2 -0
  10. package/package.json +74 -0
  11. package/public/app-main.js +2025 -0
  12. package/public/app-types.js +1 -0
  13. package/public/app.js +1 -0
  14. package/public/command-palette.js +42 -0
  15. package/public/dialogs.js +199 -0
  16. package/public/file-browser.js +196 -0
  17. package/public/icons/apple-touch-icon.png +0 -0
  18. package/public/icons/favicon-16.png +0 -0
  19. package/public/icons/favicon-32.png +0 -0
  20. package/public/icons/tau-192.png +0 -0
  21. package/public/icons/tau-512.png +0 -0
  22. package/public/icons/tau-logo.png +0 -0
  23. package/public/icons/tau-logo.svg +13 -0
  24. package/public/icons/tau-maskable-512.png +0 -0
  25. package/public/icons/tau-new.png +0 -0
  26. package/public/index.html +264 -0
  27. package/public/launcher-panel.js +54 -0
  28. package/public/launcher.js +84 -0
  29. package/public/manifest.json +28 -0
  30. package/public/markdown.js +336 -0
  31. package/public/message-renderer.js +268 -0
  32. package/public/model-picker.js +478 -0
  33. package/public/session-sidebar.js +460 -0
  34. package/public/session-stats-card.js +123 -0
  35. package/public/state.js +81 -0
  36. package/public/style.css +3864 -0
  37. package/public/sw.js +66 -0
  38. package/public/themes.js +72 -0
  39. package/public/tool-card.js +317 -0
  40. package/public/tree-view.js +474 -0
  41. package/public/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  42. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  43. package/public/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  44. package/public/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  45. package/public/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  46. package/public/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  47. package/public/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  48. package/public/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  49. package/public/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  50. package/public/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  51. package/public/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  52. package/public/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  53. package/public/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  54. package/public/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  55. package/public/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  56. package/public/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  57. package/public/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  58. package/public/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  59. package/public/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  60. package/public/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  61. package/public/vendor/katex/katex.min.css +1 -0
  62. package/public/vendor/katex/katex.min.js +1 -0
  63. package/public/voice-input.js +74 -0
  64. package/public/websocket-client.js +156 -0
  65. package/scripts/copy-katex.mjs +32 -0
  66. package/src/pi-extension/tau-tree.ts +71 -0
  67. package/src/public/app-main.ts +2190 -0
  68. package/src/public/app-types.ts +96 -0
  69. package/src/public/app.ts +1 -0
  70. package/src/public/command-palette.ts +53 -0
  71. package/src/public/dialogs.ts +251 -0
  72. package/src/public/file-browser.ts +224 -0
  73. package/src/public/launcher-panel.ts +68 -0
  74. package/src/public/launcher.ts +101 -0
  75. package/src/public/legacy-dom.d.ts +29 -0
  76. package/src/public/markdown.ts +372 -0
  77. package/src/public/message-renderer.ts +311 -0
  78. package/src/public/model-picker.ts +500 -0
  79. package/src/public/session-sidebar.ts +522 -0
  80. package/src/public/session-stats-card.ts +176 -0
  81. package/src/public/state.ts +96 -0
  82. package/src/public/sw.ts +79 -0
  83. package/src/public/themes.ts +73 -0
  84. package/src/public/tool-card.ts +375 -0
  85. package/src/public/tree-view.ts +527 -0
  86. package/src/public/voice-input.ts +98 -0
  87. package/src/public/websocket-client.ts +165 -0
  88. package/src/server/auth.ts +88 -0
  89. package/src/server/config.ts +88 -0
  90. package/src/server/model-utils.ts +122 -0
  91. package/src/server/server-main.ts +1004 -0
  92. package/src/server/sessions.ts +481 -0
  93. package/src/server/tau.ts +9 -0
  94. package/src/server/tree.ts +288 -0
  95. package/src/server/types.ts +68 -0
  96. package/tsconfig.json +3 -0
  97. package/tsconfig.public.json +15 -0
  98. package/tsconfig.server.json +16 -0
package/bin/tree.js ADDED
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ /*
3
+ * Session-tree navigation for the backend-local `navigate_tree` RPC command.
4
+ *
5
+ * pi stores a session as an append-only JSONL tree (entries have id/parentId;
6
+ * the current position is the "leaf"). pi's native RPC protocol only exposes
7
+ * READ-ONLY tree commands (get_tree / get_entries) — there is no command that
8
+ * moves the leaf — so tau bundles a small pi extension
9
+ * (src/pi-extension/tau-tree.ts, loaded into every child via `--extension`)
10
+ * that registers a `/tau-tree-navigate <entryId>` command. The handler calls
11
+ * pi's own ctx.navigateTree() — the same API the TUI /tree uses — so the move
12
+ * happens in-process, respects other extensions' session_before_tree hooks,
13
+ * and persists itself by appending a `custom` marker entry (pi derives the
14
+ * leaf from the LAST entry on load, so a bare in-memory move would not
15
+ * survive a restart). Matched extension commands never reach the LLM and
16
+ * never append a user message, so neither the command text nor the marker is
17
+ * visible to the model.
18
+ *
19
+ * navigateTree below drives that command over RPC: it resolves the target
20
+ * from a preflight get_tree, sends the prompt, then re-fetches the tree to
21
+ * refresh session.entries and VERIFY the move — a handler error does not fail
22
+ * the prompt response (pi reports it as an `extension_error` event instead),
23
+ * so the follow-up get_tree is the real success check.
24
+ *
25
+ * Selection semantics mirror pi's /tree (docs/sessions.md "Selection
26
+ * Behavior"); the extension delegates them to ctx.navigateTree, and tau
27
+ * re-derives them here (selectNavigationTarget) only to detect no-ops, verify
28
+ * the new leaf, and hand the client its editorText — RPC-mode extensions
29
+ * cannot fill an editor:
30
+ * - user (or custom) MESSAGE entry → the leaf moves to the entry's PARENT
31
+ * and the message text goes back as editorText so the client can put it
32
+ * in the input box for edit + resubmit (root user message → reset to an
33
+ * empty conversation).
34
+ * - any other entry → the leaf moves TO that entry, no editorText.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.NAVIGATION_MARKER_TYPE = exports.NAVIGATE_COMMAND = void 0;
38
+ exports.selectNavigationTarget = selectNavigationTarget;
39
+ exports.flattenTree = flattenTree;
40
+ exports.pathFromRoot = pathFromRoot;
41
+ exports.leafDescendsFrom = leafDescendsFrom;
42
+ exports.isTreeNavigationInProgress = isTreeNavigationInProgress;
43
+ exports.navigateTree = navigateTree;
44
+ /**
45
+ * Name of the slash command registered by tau's bundled pi extension.
46
+ * Keep in sync with NAVIGATE_COMMAND in src/pi-extension/tau-tree.ts.
47
+ */
48
+ exports.NAVIGATE_COMMAND = 'tau-tree-navigate';
49
+ /**
50
+ * customType of the marker entries the extension appends to persist a leaf
51
+ * move. Keep in sync with src/pi-extension/tau-tree.ts (and the frontend
52
+ * constant in src/public/tree-view.ts, which hides markers from the tree).
53
+ */
54
+ exports.NAVIGATION_MARKER_TYPE = 'tau:navigate-tree';
55
+ function textFromContent(content) {
56
+ if (typeof content === 'string')
57
+ return content;
58
+ if (!Array.isArray(content))
59
+ return '';
60
+ return content
61
+ .filter((b) => !!b && typeof b === 'object')
62
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
63
+ .map((b) => b.text)
64
+ .join('\n');
65
+ }
66
+ /**
67
+ * Decide where the leaf should move for a navigation target, mirroring pi's
68
+ * /tree selection behavior. Pure function — exported for tests.
69
+ */
70
+ function selectNavigationTarget(entry) {
71
+ if (entry.type === 'message' && entry.message?.role === 'user') {
72
+ return { leafTargetId: entry.parentId ?? null, editorText: textFromContent(entry.message.content) };
73
+ }
74
+ if (entry.type === 'custom_message') {
75
+ return { leafTargetId: entry.parentId ?? null, editorText: textFromContent(entry.content) };
76
+ }
77
+ return { leafTargetId: entry.id };
78
+ }
79
+ /** Flatten get_tree nodes (possibly multiple roots) into an id → entry map. */
80
+ function flattenTree(nodes) {
81
+ const byId = new Map();
82
+ const stack = [...(nodes || [])];
83
+ while (stack.length) {
84
+ const node = stack.pop();
85
+ if (!node || !node.entry || typeof node.entry.id !== 'string')
86
+ continue;
87
+ byId.set(node.entry.id, node.entry);
88
+ if (node.children && node.children.length)
89
+ stack.push(...node.children);
90
+ }
91
+ return byId;
92
+ }
93
+ /** Walk root→leaf along parentId links; returns [] for a null/unknown leaf. */
94
+ function pathFromRoot(byId, leafId) {
95
+ const path = [];
96
+ const seen = new Set();
97
+ let current = leafId ? byId.get(leafId) : undefined;
98
+ while (current && !seen.has(current.id)) {
99
+ seen.add(current.id);
100
+ path.push(current);
101
+ current = current.parentId ? byId.get(current.parentId) : undefined;
102
+ }
103
+ path.reverse();
104
+ return path;
105
+ }
106
+ /** True when `leafId` is `ancestorId` or one of its descendants. */
107
+ function leafDescendsFrom(byId, leafId, ancestorId) {
108
+ const seen = new Set();
109
+ let current = leafId ? byId.get(leafId) : undefined;
110
+ while (current && !seen.has(current.id)) {
111
+ if (current.id === ancestorId)
112
+ return true;
113
+ seen.add(current.id);
114
+ current = current.parentId ? byId.get(current.parentId) : undefined;
115
+ }
116
+ return false;
117
+ }
118
+ // Session ids with a navigate_tree currently in flight. navigateTree runs a
119
+ // multi-await check→navigate→verify sequence, so without this a second
120
+ // navigation (or a prompt — see the guard in server-main's handleRpcCommand)
121
+ // could interleave with it and land its entries on the wrong branch.
122
+ const navigationsInFlight = new Set();
123
+ /** True while a navigate_tree is moving this session's leaf. */
124
+ function isTreeNavigationInProgress(sessionId) {
125
+ return navigationsInFlight.has(sessionId);
126
+ }
127
+ function assertNotStreaming(session) {
128
+ if (session.isStreaming) {
129
+ throw new Error('Cannot navigate the session tree while the agent is streaming; abort or wait for the turn to finish');
130
+ }
131
+ }
132
+ /**
133
+ * Once per child process, confirm pi actually registered tau's extension
134
+ * command. Without this check a broken install (extension file missing or
135
+ * failing to load) would make the `/tau-tree-navigate …` text fall through
136
+ * pi's command matching and be sent to the LLM as a literal user prompt.
137
+ */
138
+ async function assertNavigateCommandAvailable(session) {
139
+ if (session.navigateCommandChecked)
140
+ return;
141
+ const resp = await session.send({ type: 'get_commands' }, { timeoutMs: 15000 });
142
+ if (resp.success === false) {
143
+ throw new Error(`pi could not list its commands: ${resp.error || 'unknown error'}`);
144
+ }
145
+ const commands = (resp.data || {}).commands || [];
146
+ const found = commands.some((c) => c.name === exports.NAVIGATE_COMMAND && c.source === 'extension');
147
+ if (!found) {
148
+ throw new Error(`The pi child did not load tau's ${exports.NAVIGATE_COMMAND} extension; cannot navigate the session tree`);
149
+ }
150
+ session.navigateCommandChecked = true;
151
+ }
152
+ /**
153
+ * Full navigate_tree flow for a live session: resolve the target from a
154
+ * preflight get_tree, have the child move its leaf in-process via the bundled
155
+ * extension's /tau-tree-navigate command, re-fetch the tree to confirm the
156
+ * move, refresh session.entries to the new active path, and broadcast a fresh
157
+ * `live_session_snapshot` (plus `live_session_updated`) so every connected
158
+ * browser re-renders the conversation.
159
+ *
160
+ * The streaming guard cannot be made fully airtight from tau's side (a prompt
161
+ * whose agent_start has not yet round-tripped through the child is invisible
162
+ * here), so the flow is defensive instead: one navigation per session at a
163
+ * time, the extension re-checks idleness in-process (ctx.isIdle()) right
164
+ * before moving the leaf, prompts are refused by handleRpcCommand while a
165
+ * navigation is in flight, and if the child still turns out to have moved on
166
+ * (leaf verification fails) the clients are re-synced to the child's real
167
+ * state before the error is reported.
168
+ */
169
+ async function navigateTree(session, entryId) {
170
+ if (navigationsInFlight.has(session.id)) {
171
+ throw new Error('Another tree navigation for this session is already in progress');
172
+ }
173
+ navigationsInFlight.add(session.id);
174
+ try {
175
+ assertNotStreaming(session);
176
+ // Preflight: fetch the tree to resolve the target entry (clear error for
177
+ // unknown ids), detect no-ops, and derive editorText locally — RPC-mode
178
+ // extensions cannot fill the client's editor, and the /tau-tree-navigate
179
+ // prompt response cannot carry handler data.
180
+ let treeResp = await session.send({ type: 'get_tree' }, { timeoutMs: 15000 });
181
+ if (treeResp.success === false) {
182
+ throw new Error(`pi did not answer the pre-navigation tree check: ${treeResp.error || 'unknown error'}`);
183
+ }
184
+ let treeData = (treeResp.data || {});
185
+ let byId = flattenTree(treeData.tree);
186
+ const entry = byId.get(entryId);
187
+ if (!entry)
188
+ throw new Error(`Entry ${entryId} not found in the session tree`);
189
+ const target = selectNavigationTarget(entry);
190
+ const previousLeafId = treeData.leafId ?? null;
191
+ // Already there: pi's navigateTree no-ops when the selected entry IS the
192
+ // current leaf, and moving to the current position would be pure churn —
193
+ // skip the round-trip (but still hand back the editorText, so selecting
194
+ // the leaf user message keeps prefilling the composer).
195
+ if (entryId === previousLeafId || (target.leafTargetId !== null && target.leafTargetId === previousLeafId)) {
196
+ return target.editorText !== undefined ? { editorText: target.editorText } : {};
197
+ }
198
+ await assertNavigateCommandAvailable(session);
199
+ // A turn may have started while the preflight round-tripped; re-check
200
+ // here for a fast, precise error (the extension re-checks in-process via
201
+ // ctx.isIdle() as the authoritative guard).
202
+ assertNotStreaming(session);
203
+ session.lastExtensionError = null;
204
+ const prompted = await session.send({ type: 'prompt', message: `/${exports.NAVIGATE_COMMAND} ${entryId}` }, { timeoutMs: 30000 });
205
+ if (prompted.success === false) {
206
+ throw new Error(`pi refused the tree-navigation command: ${prompted.error || 'unknown error'}`);
207
+ }
208
+ // The prompt ack only means the handler ran — its errors surface as
209
+ // `extension_error` events, not as a failed response — so the follow-up
210
+ // get_tree is the real success check.
211
+ treeResp = await session.send({ type: 'get_tree' }, { timeoutMs: 30000 });
212
+ if (treeResp.success === false) {
213
+ // The child MAY have moved its leaf; we just cannot re-derive the
214
+ // entries. At least nudge clients so they refetch metadata instead of
215
+ // silently rendering the old branch forever.
216
+ session.manager.broadcastUpdated(session.id);
217
+ throw new Error(`pi could not report the session tree after navigating: ${treeResp.error || 'unknown error'}`);
218
+ }
219
+ treeData = (treeResp.data || {});
220
+ byId = flattenTree(treeData.tree);
221
+ const newLeafId = treeData.leafId ?? null;
222
+ // Refresh entries and broadcast BEFORE verifying the leaf: at this point
223
+ // the child is authoritative, so even when verification fails below every
224
+ // client must be re-rendered onto whatever the child actually has.
225
+ session.entries = pathFromRoot(byId, newLeafId);
226
+ session.manager.broadcast({ type: 'live_session_snapshot', sessionId: session.id, ...session.snapshot() });
227
+ session.manager.broadcastUpdated(session.id);
228
+ // On success the extension appended a marker at the navigation target and
229
+ // that marker IS the child's new leaf (no reload happens, so nothing else
230
+ // gets appended in between). Requiring the leaf to have changed AND to be
231
+ // a marker parented at the target rejects both a silent no-move (handler
232
+ // error) and a leaf that moved somewhere unexpected.
233
+ const newLeaf = newLeafId ? byId.get(newLeafId) : undefined;
234
+ const moved = newLeafId !== previousLeafId &&
235
+ newLeaf !== undefined &&
236
+ newLeaf.type === 'custom' &&
237
+ newLeaf.customType === exports.NAVIGATION_MARKER_TYPE &&
238
+ (newLeaf.parentId ?? null) === target.leafTargetId;
239
+ if (!moved) {
240
+ const detail = session.lastExtensionError ? `: ${session.lastExtensionError}` : '';
241
+ throw new Error(`pi did not move the session leaf to the expected entry${detail}`);
242
+ }
243
+ return target.editorText !== undefined ? { editorText: target.editorText } : {};
244
+ }
245
+ finally {
246
+ navigationsInFlight.delete(session.id);
247
+ }
248
+ }
package/bin/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "pi-tau-web-server",
3
+ "version": "1.0.8",
4
+ "description": "Web browser workspace for Pi — a standalone web server that manages multiple live Pi RPC sessions in parallel.",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-coding-agent",
8
+ "ai-agent",
9
+ "ai-coding-agent",
10
+ "coding-agent",
11
+ "harness",
12
+ "web-ui",
13
+ "web-server",
14
+ "rpc",
15
+ "standalone",
16
+ "multi-session"
17
+ ],
18
+ "author": {
19
+ "name": "Milan Glacier",
20
+ "email": "dev@milanglacier.com",
21
+ "url": "https://github.com/milanglacier"
22
+ },
23
+ "contributors": [
24
+ {
25
+ "name": "deflating",
26
+ "url": "https://github.com/deflating"
27
+ }
28
+ ],
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/milanglacier/pi-tau-web-server#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/milanglacier/pi-tau-web-server/issues"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/milanglacier/pi-tau-web-server.git"
37
+ },
38
+ "main": "./bin/tau.js",
39
+ "bin": {
40
+ "pi-tau-web-server": "./bin/tau.js"
41
+ },
42
+ "engines": {
43
+ "node": ">=22.19.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "files": [
50
+ "bin",
51
+ "public",
52
+ "scripts",
53
+ "src",
54
+ "tsconfig.json",
55
+ "tsconfig.server.json",
56
+ "tsconfig.public.json"
57
+ ],
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.server.json && tsc -p tsconfig.public.json && node scripts/copy-katex.mjs",
60
+ "prepare": "npm run build",
61
+ "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p tsconfig.public.json --noEmit && tsc -p tsconfig.test.json",
62
+ "test": "npm run build && node --test"
63
+ },
64
+ "dependencies": {
65
+ "ws": "^8.21.1"
66
+ },
67
+ "devDependencies": {
68
+ "@earendil-works/pi-coding-agent": "0.80.10",
69
+ "@types/node": "^26.1.1",
70
+ "@types/ws": "^8.18.1",
71
+ "katex": "^0.18.0",
72
+ "typescript": "^7.0.2"
73
+ }
74
+ }