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.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/bin/pipane.js +22 -0
  4. package/dist/client/assets/_node-stub_node_crypto-C_7epg3G.js +1 -0
  5. package/dist/client/assets/_node-stub_node_fs-B-VOzeXW.js +1 -0
  6. package/dist/client/assets/_node-stub_node_http-CROXaVGJ.js +1 -0
  7. package/dist/client/assets/_node-stub_node_os-TTKJha15.js +1 -0
  8. package/dist/client/assets/_node-stub_node_path-DJCJiKv7.js +1 -0
  9. package/dist/client/assets/_node-stub_stream-DDexIdn_.js +1 -0
  10. package/dist/client/assets/index-CQP8RzjO.js +45 -0
  11. package/dist/client/assets/index-CVr_JVrA.js +131 -0
  12. package/dist/client/assets/index-DQxrqu4K.js +4412 -0
  13. package/dist/client/assets/index-DuJn-Nwv.js +3 -0
  14. package/dist/client/assets/index-Pj8zaBYJ.js +1 -0
  15. package/dist/client/assets/index-yL1A-vgM.css +1 -0
  16. package/dist/client/assets/pdf.worker.min-Cpi8b8z3.mjs +28 -0
  17. package/dist/client/favicon.png +0 -0
  18. package/dist/client/index.html +118 -0
  19. package/dist/server/server/attached-session.js +209 -0
  20. package/dist/server/server/load-trace-store.js +48 -0
  21. package/dist/server/server/local-settings.js +376 -0
  22. package/dist/server/server/pi-launch.js +10 -0
  23. package/dist/server/server/pi-runtime.js +32 -0
  24. package/dist/server/server/process-pool.js +254 -0
  25. package/dist/server/server/rest-api.js +289 -0
  26. package/dist/server/server/server.js +355 -0
  27. package/dist/server/server/session-cwd.js +33 -0
  28. package/dist/server/server/session-index.js +231 -0
  29. package/dist/server/server/session-jsonl.js +260 -0
  30. package/dist/server/server/session-lifecycle.js +155 -0
  31. package/dist/server/server/ws-handler.js +808 -0
  32. package/dist/server/shared/jsonl-sync.js +141 -0
  33. package/extensions/canvas.ts +57 -0
  34. package/package.json +82 -0
  35. package/patches/@mariozechner+pi-web-ui+0.55.3.patch +3279 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * JSONL sync protocol: hash-verified text diffs.
3
+ *
4
+ * The server maintains a canonical JSON string representing the full session state.
5
+ * It sends diffs (patches) to the client, which applies them and verifies integrity
6
+ * via SHA-256 hashes.
7
+ *
8
+ * Patch format: array of operations that transform the old string into the new one.
9
+ * Each operation is { offset, deleteCount, insert } applied sequentially.
10
+ *
11
+ * This module is isomorphic — works in both Node.js and browser.
12
+ */
13
+ // ── Hashing ────────────────────────────────────────────────────────────────
14
+ /**
15
+ * Compute a hex SHA-256 hash of a string.
16
+ * Works in both Node.js (crypto) and browser (SubtleCrypto).
17
+ */
18
+ export async function computeHash(data) {
19
+ if (typeof globalThis.crypto?.subtle?.digest === "function") {
20
+ // Browser or Node 20+
21
+ const encoded = new TextEncoder().encode(data);
22
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
23
+ const hashArray = new Uint8Array(hashBuffer);
24
+ return Array.from(hashArray).map(b => b.toString(16).padStart(2, "0")).join("");
25
+ }
26
+ // Node.js fallback
27
+ const { createHash } = await import("node:crypto");
28
+ return createHash("sha256").update(data, "utf8").digest("hex");
29
+ }
30
+ // Note: computeHashSync is NOT in this shared module because it depends on
31
+ // node:crypto which is stubbed in the client Vite build. It lives in
32
+ // session-jsonl.ts (server-only).
33
+ // ── Diffing ────────────────────────────────────────────────────────────────
34
+ /**
35
+ * Compute patches to transform `oldStr` into `newStr`.
36
+ *
37
+ * Uses a simple but effective approach:
38
+ * 1. Find common prefix
39
+ * 2. Find common suffix (after prefix)
40
+ * 3. The middle part is a single replace operation
41
+ *
42
+ * This is O(n) and produces at most one patch, which is ideal for our use case
43
+ * where changes are typically appended (streaming) or localized (tool updates).
44
+ *
45
+ * For cases where the strings are identical, returns an empty array.
46
+ */
47
+ export function computePatches(oldStr, newStr) {
48
+ if (oldStr === newStr)
49
+ return [];
50
+ // Find common prefix length
51
+ const minLen = Math.min(oldStr.length, newStr.length);
52
+ let prefixLen = 0;
53
+ while (prefixLen < minLen && oldStr[prefixLen] === newStr[prefixLen]) {
54
+ prefixLen++;
55
+ }
56
+ // Find common suffix length (don't overlap with prefix)
57
+ let suffixLen = 0;
58
+ const maxSuffix = minLen - prefixLen;
59
+ while (suffixLen < maxSuffix &&
60
+ oldStr[oldStr.length - 1 - suffixLen] === newStr[newStr.length - 1 - suffixLen]) {
61
+ suffixLen++;
62
+ }
63
+ const deleteCount = oldStr.length - prefixLen - suffixLen;
64
+ const insert = newStr.slice(prefixLen, newStr.length - suffixLen);
65
+ // If nothing to delete and nothing to insert, strings must be equal (shouldn't happen)
66
+ if (deleteCount === 0 && insert.length === 0)
67
+ return [];
68
+ return [{ offset: prefixLen, deleteCount, insert }];
69
+ }
70
+ // ── Patching ───────────────────────────────────────────────────────────────
71
+ /**
72
+ * Apply patches to a string.
73
+ * Patches are applied sequentially (each operates on the result of the previous).
74
+ */
75
+ export function applyPatches(str, patches) {
76
+ let result = str;
77
+ for (const patch of patches) {
78
+ result =
79
+ result.slice(0, patch.offset) +
80
+ patch.insert +
81
+ result.slice(patch.offset + patch.deleteCount);
82
+ }
83
+ return result;
84
+ }
85
+ // ── Sync helpers ───────────────────────────────────────────────────────────
86
+ /**
87
+ * Server-side: compute a SyncOp to send to the client.
88
+ *
89
+ * If the client's baseHash doesn't match (or is empty), sends a full sync.
90
+ * Otherwise computes patches.
91
+ *
92
+ * @param oldStr - The previous string the client has (empty if first sync)
93
+ * @param newStr - The new string to sync to
94
+ * @param oldHash - Hash of oldStr (client's current hash)
95
+ * @param newHash - Pre-computed hash of newStr
96
+ * @returns The sync operation to send
97
+ */
98
+ export function computeSyncOp(oldStr, newStr, oldHash, newHash) {
99
+ if (!oldHash || oldStr.length === 0) {
100
+ return { op: "full", data: newStr, hash: newHash };
101
+ }
102
+ const patches = computePatches(oldStr, newStr);
103
+ // If the delta is larger than 80% of the full string, just send full
104
+ const deltaSize = patches.reduce((sum, p) => sum + p.insert.length + 20, 0);
105
+ if (deltaSize > newStr.length * 0.8) {
106
+ return { op: "full", data: newStr, hash: newHash };
107
+ }
108
+ return { op: "delta", patches, hash: newHash, baseHash: oldHash };
109
+ }
110
+ /**
111
+ * Client-side: apply a SyncOp to the current state.
112
+ *
113
+ * Returns the new string and hash, or null if verification failed
114
+ * (caller should request a full sync).
115
+ */
116
+ export async function applySyncOp(currentStr, currentHash, op) {
117
+ if (op.op === "full") {
118
+ // Verify the full data matches the declared hash
119
+ const actualHash = await computeHash(op.data);
120
+ if (actualHash !== op.hash) {
121
+ console.error("[jsonl-sync] Full sync hash mismatch", { expected: op.hash, actual: actualHash });
122
+ return null;
123
+ }
124
+ return { data: op.data, hash: op.hash };
125
+ }
126
+ // Delta sync
127
+ if (op.baseHash !== currentHash) {
128
+ console.warn("[jsonl-sync] Base hash mismatch, need full sync", {
129
+ expected: op.baseHash,
130
+ actual: currentHash,
131
+ });
132
+ return null;
133
+ }
134
+ const newStr = applyPatches(currentStr, op.patches);
135
+ const actualHash = await computeHash(newStr);
136
+ if (actualHash !== op.hash) {
137
+ console.error("[jsonl-sync] Post-patch hash mismatch", { expected: op.hash, actual: actualHash });
138
+ return null;
139
+ }
140
+ return { data: newStr, hash: op.hash };
141
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Canvas tool extension for pipane.
3
+ *
4
+ * Registers a `canvas` tool that the LLM can call to display markdown
5
+ * content in a side panel. Accepts either inline content or a file path.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { Type } from "@sinclair/typebox";
10
+ import { readFile } from "node:fs/promises";
11
+ import { resolve } from "node:path";
12
+
13
+ export default function (pi: ExtensionAPI) {
14
+ pi.registerTool({
15
+ name: "canvas",
16
+ label: "Canvas",
17
+ description:
18
+ "Display markdown content in a side panel for the user. Use for long-form artifacts like documents, guides, code, HTML previews, essays, reports, etc. Only one canvas is visible at a time; calling this replaces the previous. Accepts either inline markdown via `content` or a `filePath` to read from disk.",
19
+ parameters: Type.Object({
20
+ title: Type.String({ description: "Short title for the canvas panel header" }),
21
+ content: Type.Optional(
22
+ Type.String({ description: "Markdown content to display in the canvas panel" }),
23
+ ),
24
+ filePath: Type.Optional(
25
+ Type.String({ description: "Path to a markdown file to display in the canvas panel" }),
26
+ ),
27
+ }),
28
+
29
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
30
+ let markdown = params.content ?? "";
31
+
32
+ if (params.filePath) {
33
+ try {
34
+ const resolved = resolve(ctx.cwd, params.filePath);
35
+ markdown = await readFile(resolved, "utf-8");
36
+ } catch (err: any) {
37
+ return {
38
+ content: [{ type: "text" as const, text: `Error reading file: ${err.message}` }],
39
+ isError: true,
40
+ };
41
+ }
42
+ }
43
+
44
+ if (!markdown) {
45
+ return {
46
+ content: [{ type: "text" as const, text: "Error: provide either `content` or `filePath`." }],
47
+ isError: true,
48
+ };
49
+ }
50
+
51
+ return {
52
+ content: [{ type: "text" as const, text: "Canvas displayed to user." }],
53
+ details: { title: params.title, markdown },
54
+ };
55
+ },
56
+ });
57
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "pipane",
3
+ "version": "0.1.0",
4
+ "description": "A clean web interface for the pi coding agent — chat UI with real-time tool calls, streaming output, session management, and model picker",
5
+ "private": false,
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "author": "Mike Heunher",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/mike-heunher/pipane.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/mike-heunher/pipane/issues"
15
+ },
16
+ "homepage": "https://github.com/mike-heunher/pipane#readme",
17
+ "keywords": [
18
+ "pi",
19
+ "coding-agent",
20
+ "web-ui",
21
+ "chat",
22
+ "llm",
23
+ "ai",
24
+ "cli",
25
+ "developer-tools"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20.0.0"
29
+ },
30
+ "bin": {
31
+ "pipane": "bin/pipane.js"
32
+ },
33
+ "files": [
34
+ "dist/",
35
+ "bin/",
36
+ "extensions/",
37
+ "patches/",
38
+ "LICENSE",
39
+ "README.md"
40
+ ],
41
+ "scripts": {
42
+ "postinstall": "patch-package",
43
+ "dev": "./dev.sh",
44
+ "dev:server": "tsx watch src/server/server.ts",
45
+ "dev:client": "vite",
46
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && vite build && tsc -p tsconfig.server.json",
47
+ "start": "NODE_ENV=production node dist/server/server/server.js",
48
+ "prod": "./prod.sh",
49
+ "check": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "test:screenshots": "node node_modules/@playwright/test/cli.js test -c playwright.config.ts e2e/ui-screenshots.e2e.ts",
53
+ "test:screenshots:update": "node node_modules/@playwright/test/cli.js test -c playwright.config.ts e2e/ui-screenshots.e2e.ts --update-snapshots",
54
+ "bench:sessions": "tsx scripts/bench-sessions.ts",
55
+ "prepack": "npm run build"
56
+ },
57
+ "dependencies": {
58
+ "@mariozechner/mini-lit": "^0.2.0",
59
+ "@mariozechner/pi-agent-core": "0.55.3",
60
+ "@mariozechner/pi-ai": "0.55.3",
61
+ "@mariozechner/pi-coding-agent": "0.55.3",
62
+ "@mariozechner/pi-web-ui": "0.55.3",
63
+ "@tailwindcss/vite": "^4.1.17",
64
+ "express": "^5.1.0",
65
+ "lit": "^3.3.1",
66
+ "lucide": "^0.544.0",
67
+ "patch-package": "^8.0.1",
68
+ "ws": "^8.18.0"
69
+ },
70
+ "devDependencies": {
71
+ "@playwright/test": "^1.58.2",
72
+ "@types/express": "^5.0.0",
73
+ "@types/ws": "^8.5.10",
74
+ "concurrently": "^9.0.0",
75
+ "happy-dom": "^20.7.0",
76
+ "tsx": "^4.19.0",
77
+ "typescript": "^5.7.3",
78
+ "vite": "^7.1.6",
79
+ "vitest": "^3.2.4"
80
+ },
81
+ "packageManager": "npm@11.3.0"
82
+ }