pitroom 0.7.0 → 0.8.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pitroom",
3
3
  "description": "A free pit crew for your expensive coding agent: the superpowers development workflow, run by cheap workers. Delegate research, fixes, tests, reviews and whole plans from Claude Code, Codex or any agent to OpenCode, Codex or Claude Code workers, with receipts that prove the savings.",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "author": {
6
6
  "name": "TahaAtas"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pitroom",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "A free pit crew for your expensive coding agent: delegate research, fixes, tests, reviews and parallel investigations to cheap worker agents running your own models, with verified answers, exact diffs and cost receipts.",
5
5
  "author": {
6
6
  "name": "TahaAtas"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Dashboard
6
+ - An edited file in a run card opens as its diff: line numbers, added and removed rows, syntax highlighting (Shiki: TypeScript, JavaScript, TSX, JSX, JSON, CSS, HTML, Python, Bash, YAML) and a copy button. Messages and tool results get their own rows with a status, and the step list has a scroller with a rail and a "Last step" button that follows new steps while you are at the bottom.
7
+ - The dashboard files grow from about 0.7 MB to 1.9 MB for the highlighter; the npm package from 422 kB to 592 kB.
8
+ - A renamed file whose path starts with `a/` or `b/` keeps its real name in the diff.
9
+
10
+ ### Fixes
11
+ - `pitroom init` asks OpenCode for its models twice: the first call can come back empty while OpenCode's service starts, which made init see no models and suggest no fallback.
12
+
3
13
  ## 0.7.0
4
14
 
5
15
  ### Codex
package/README.md CHANGED
@@ -114,7 +114,7 @@ In isolate mode nothing reaches your tree until you apply it.
114
114
  - **Zero repo pollution.** No `.pitroom/` folder, no `.gitignore` edits, no branches. Records live in `~/.local/state/pitroom`.
115
115
  - **Works with any agent.** Fourteen [Agent Skills](https://agentskills.io): the superpowers development workflow run by workers, plus delegation (`using-pitroom`, `pitroom-research`, `-crew`, `-implement`), a CLI, and a Claude Code / Codex plugin whose session-start hook loads the workflow. Claude Code, Codex, Gemini CLI, Cursor, or anything that can run a shell command.
116
116
  - **A live dashboard and a searchable history.** `pitroom dash` opens a local page of every run (the task, what the worker did step by step, its result and diff) with history and statistics, for the agent apps that show neither hooks nor a status line; `pitroom history` searches everything Pitroom ever ran. See [Dashboard](#dashboard).
117
- - **Small.** About 6,000 lines of TypeScript, a ~190 KB CLI bundle (plus the ~0.7 MB of dashboard files it serves), zero runtime dependencies.
117
+ - **Small.** About 6,500 lines of TypeScript in the CLI, a ~210 KB CLI bundle (plus the ~1.9 MB of dashboard files it serves, with syntax highlighting for ten languages), zero runtime dependencies.
118
118
 
119
119
  ### How it stays safe
120
120
 
@@ -24,7 +24,8 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
24
24
  | [Base UI](https://github.com/mui/base-ui) | MIT | Material-UI SAS (2019) |
25
25
  | [shadcn/ui](https://github.com/shadcn-ui/ui) components (`ui/src/components/ui`) | MIT | shadcn |
26
26
  | [Shadix UI](https://github.com/apix-js/shadix-ui) expandable card (`ui/src/components/expandable-card.tsx`) | MIT | gihanrangana (2025) |
27
- | [beUI](https://github.com/starc007/ui-components) agent activity (`ui/src/components/agents`, `motion`, `lib`) | MIT | Saurabh Chauhan (2026) |
27
+ | [beUI](https://github.com/starc007/ui-components) agent activity, file diff, tool result, message scroller and preview rail (`ui/src/components/agents`, `motion`, `lib`) | MIT | Saurabh Chauhan (2026) |
28
+ | [Shiki](https://github.com/shikijs/shiki) syntax highlighting | MIT | Pine Wu (2021), Anthony Fu (2023) |
28
29
  | [Motion](https://github.com/motiondivision/motion) (with framer-motion) | MIT | Motion B.V. (2024), Framer B.V. (2018) |
29
30
  | [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) | MIT | Tailwind Labs, Inc. |
30
31
  | [tw-animate-css](https://github.com/Wombosvideo/tw-animate-css) | MIT | Wombosvideo (2025) |
package/dist/pitroom.mjs CHANGED
@@ -1463,7 +1463,7 @@ function planInit(opts = {}) {
1463
1463
  const b = getBackend("opencode");
1464
1464
  let models = [];
1465
1465
  try {
1466
- models = b.listModels?.() ?? [];
1466
+ models = b.catalog ? b.catalog().models.map((m) => m.id) : b.listModels?.() ?? [];
1467
1467
  } catch {
1468
1468
  notes.push("could not list OpenCode models (is it logged in?)");
1469
1469
  }
@@ -1950,7 +1950,7 @@ function archivedRun(id) {
1950
1950
  at: s.at ?? void 0,
1951
1951
  t: s.at && Number.isFinite(start) ? Math.max(0, Math.round((s.at - start) / 1e3)) : void 0
1952
1952
  }));
1953
- return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, steps };
1953
+ return { meta: JSON.parse(r.meta_json), answer: r.answer ?? "", patch: r.patch ?? void 0, patchTruncated: r.patch?.length >= PATCH_MAX, steps };
1954
1954
  } catch {
1955
1955
  return void 0;
1956
1956
  }
@@ -2969,6 +2969,118 @@ import http from "node:http";
2969
2969
  import path16 from "node:path";
2970
2970
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2971
2971
 
2972
+ // src/core/file-diff.ts
2973
+ var PREVIEW_LINES = 300;
2974
+ function gitPath(value, prefix = true) {
2975
+ let path24 = value;
2976
+ if (value.startsWith('"') && value.endsWith('"')) {
2977
+ const bytes = [];
2978
+ const escapes = { t: " ", n: "\n", r: "\r", b: "\b", f: "\f", v: "\v" };
2979
+ for (const match of value.slice(1, -1).matchAll(/\\([0-7]{1,3}|.)|([^\\]+)/g)) {
2980
+ if (match[1] && /^[0-7]+$/.test(match[1])) bytes.push(parseInt(match[1], 8));
2981
+ else bytes.push(...new TextEncoder().encode(match[2] ?? escapes[match[1]] ?? match[1]));
2982
+ }
2983
+ path24 = new TextDecoder().decode(new Uint8Array(bytes));
2984
+ }
2985
+ return prefix ? path24.replace(/^[ab]\//, "") : path24;
2986
+ }
2987
+ function headerPath(header) {
2988
+ const quoted = header.match(/^diff --git ("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/);
2989
+ if (quoted) return gitPath(quoted[2]);
2990
+ return gitPath(header.slice(header.lastIndexOf(" b/") + 1));
2991
+ }
2992
+ function fileDiffs(patch, changes, sourceTruncated = false) {
2993
+ const files = [];
2994
+ let file;
2995
+ let oldLine = 0;
2996
+ let newLine = 0;
2997
+ let oldLeft = 0;
2998
+ let newLeft = 0;
2999
+ let inHunk = false;
3000
+ let row = 0;
3001
+ const finishHunk = () => {
3002
+ if (file && (oldLeft > 0 || newLeft > 0)) file.incomplete = true;
3003
+ oldLeft = newLeft = 0;
3004
+ inHunk = false;
3005
+ };
3006
+ const add = (line) => {
3007
+ if (!file) return;
3008
+ if (file.lines.length < PREVIEW_LINES) file.lines.push({ ...line, id: String(row) });
3009
+ else file.omittedLines++;
3010
+ row++;
3011
+ };
3012
+ for (const line of patch.split("\n")) {
3013
+ if (line.startsWith("diff --git ")) {
3014
+ finishHunk();
3015
+ file = { path: headerPath(line), status: "M", lines: [], additions: 0, deletions: 0, omittedLines: 0, binary: false, incomplete: false };
3016
+ files.push(file);
3017
+ row = 0;
3018
+ continue;
3019
+ }
3020
+ if (!file) continue;
3021
+ const hunk = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
3022
+ if (hunk) {
3023
+ finishHunk();
3024
+ oldLine = Number(hunk[1]);
3025
+ newLine = Number(hunk[3]);
3026
+ oldLeft = Number(hunk[2] ?? 1);
3027
+ newLeft = Number(hunk[4] ?? 1);
3028
+ inHunk = true;
3029
+ add({ type: "hunk", content: line });
3030
+ } else if (inHunk && line.startsWith("\")) {
3031
+ add({ type: "meta", content: line });
3032
+ } else if (inHunk && (oldLeft > 0 || newLeft > 0)) {
3033
+ if (line.startsWith("+") && newLeft > 0) {
3034
+ file.additions++;
3035
+ newLeft--;
3036
+ add({ type: "added", newLine: newLine++, content: line.slice(1) });
3037
+ } else if (line.startsWith("-") && oldLeft > 0) {
3038
+ file.deletions++;
3039
+ oldLeft--;
3040
+ add({ type: "removed", oldLine: oldLine++, content: line.slice(1) });
3041
+ } else if (line.startsWith(" ") && oldLeft > 0 && newLeft > 0) {
3042
+ oldLeft--;
3043
+ newLeft--;
3044
+ add({ type: "context", oldLine: oldLine++, newLine: newLine++, content: line.slice(1) });
3045
+ } else {
3046
+ file.incomplete = true;
3047
+ }
3048
+ } else if (!inHunk) {
3049
+ if (line.startsWith("+++ ") && line !== "+++ /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3050
+ else if (line.startsWith("--- ") && line !== "--- /dev/null") file.path = gitPath(line.slice(4).replace(/\t$/, ""));
3051
+ else if (/^(GIT binary patch|Binary files .* differ)$/.test(line)) file.binary = true;
3052
+ else if (/^(new file mode|deleted file mode|old mode|new mode|rename from|rename to|similarity index) /.test(line)) {
3053
+ if (line.startsWith("new file mode ")) file.status = "A";
3054
+ if (line.startsWith("deleted file mode ")) file.status = "D";
3055
+ if (line.startsWith("rename to ")) {
3056
+ file.path = gitPath(line.slice(10), false);
3057
+ file.status = "R";
3058
+ }
3059
+ add({ type: "meta", content: line });
3060
+ }
3061
+ }
3062
+ }
3063
+ finishHunk();
3064
+ if (sourceTruncated && file) file.incomplete = true;
3065
+ for (const f of files) {
3066
+ if (f.incomplete || f.binary) f.additions = f.deletions = void 0;
3067
+ }
3068
+ const byPath = new Map(files.map((f) => [f.path, f]));
3069
+ const result = changes.map((change) => {
3070
+ const diff = byPath.get(change.path);
3071
+ byPath.delete(change.path);
3072
+ return diff ? { ...diff, status: change.status } : {
3073
+ ...change,
3074
+ lines: [],
3075
+ omittedLines: 0,
3076
+ binary: false,
3077
+ incomplete: sourceTruncated,
3078
+ unavailable: true
3079
+ };
3080
+ });
3081
+ return [...result, ...byPath.values()];
3082
+ }
3083
+
2972
3084
  // src/core/dash-page.ts
2973
3085
  var THEME_SCRIPT = "(function(){try{var t=localStorage.getItem('pitroom-theme');var d=t?t==='dark':matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d)}catch(e){}})()";
2974
3086
  var ICON3 = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23ff6a2b'/%3E%3Cpath d='M9 8h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4zM9 16h4v4H9zm8 0h4v4h-4zm-4 4h4v4h-4zm8 0h4v4h-4z' fill='%23fff'/%3E%3C/svg%3E";
@@ -3041,10 +3153,6 @@ function dashState(opts = {}) {
3041
3153
  }
3042
3154
  var TASK_MAX = 6e3;
3043
3155
  var PATCH_LINES = 300;
3044
- function readFile(id, name) {
3045
- const f = runFile(id, name);
3046
- return fs19.existsSync(f) ? fs19.readFileSync(f, "utf8") : "";
3047
- }
3048
3156
  function runDetail(id) {
3049
3157
  if (!RUN_ID2.test(id)) return void 0;
3050
3158
  let m;
@@ -3069,7 +3177,8 @@ function runDetail(id) {
3069
3177
  const answer = isActive(m.state) ? "" : readSummary(m).replace(/^SUMMARY:\s*SPEC[^\n]*\n+/i, "").replace(/^DETAILS:[ \t]*\n+/i, "").trim();
3070
3178
  const lastSay = steps.at(-1);
3071
3179
  if (lastSay?.kind === "say" && answer && (answer.includes(lastSay.text.slice(0, 80)) || lastSay.text.includes(answer.slice(0, 80)))) steps.pop();
3072
- const patch = kept?.patch ?? readFile(id, "changes.patch");
3180
+ const fullPatch = readRunFile(id, "changes.patch");
3181
+ const patch = fullPatch ?? kept?.patch ?? "";
3073
3182
  const patchLines = patch.split("\n");
3074
3183
  const u = m.usage;
3075
3184
  return {
@@ -3080,6 +3189,7 @@ function runDetail(id) {
3080
3189
  steps,
3081
3190
  answer,
3082
3191
  changes: m.changes ?? [],
3192
+ fileDiffs: fileDiffs(patch, m.changes ?? [], fullPatch === void 0 && kept?.patchTruncated),
3083
3193
  patch: patch ? `${patchLines.slice(0, PATCH_LINES).join("\n")}${patchLines.length > PATCH_LINES ? `
3084
3194
  \u2026 ${patchLines.length - PATCH_LINES} more lines (pitroom show ${id} --patch)` : ""}` : void 0,
3085
3195
  info: {
@@ -3852,7 +3962,7 @@ function secretWarning(files, mode) {
3852
3962
  }
3853
3963
 
3854
3964
  // src/core/run.ts
3855
- var VERSION2 = true ? "0.7.0" : "0.0.0-dev";
3965
+ var VERSION2 = true ? "0.8.0" : "0.0.0-dev";
3856
3966
  function planWork(o) {
3857
3967
  if (!o.plan) return { task: o.task.trim(), tier: o.tier };
3858
3968
  if (o.continueFrom) throw new UserError("a follow-up continues its parent's task; drop --plan/--step");