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
|
@@ -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
|
+
}
|