@plannotator/pi-extension 0.27.5 → 0.27.7

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.
@@ -161,7 +161,7 @@ export interface ParsedCallDiffWorkerResult {
161
161
  diagnostics: CallFlowDiagnostic[];
162
162
  }
163
163
 
164
- const MAX_TREES = 100;
164
+ const MAX_TREES = 2_000;
165
165
  const MAX_NODES = 5_000;
166
166
  const MAX_TREE_DEPTH = 32;
167
167
  const MAX_DIAGNOSTICS = 100;
@@ -218,9 +218,16 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
218
218
  if (!isRecord(value.result) || !Array.isArray(value.result.trees)) {
219
219
  throw new Error("CallDiff worker response is missing its diff trees.");
220
220
  }
221
- if (value.result.trees.length > MAX_TREES) {
222
- throw new Error("CallDiff result exceeded Plannotator's tree limits.");
223
- }
221
+ // A large-but-valid result (many small entry trees — common in Swift, TS,
222
+ // and other per-function languages) degrades instead of failing: keep the
223
+ // first MAX_TREES trees and say so in diagnostics (#1351). The caps that
224
+ // guard against genuinely unbounded worker output (total nodes, depth, raw
225
+ // length) still throw below.
226
+ const totalTreeCount = value.result.trees.length;
227
+ const truncatedTrees = totalTreeCount > MAX_TREES;
228
+ const boundedTreeCandidates = truncatedTrees
229
+ ? value.result.trees.slice(0, MAX_TREES)
230
+ : value.result.trees;
224
231
 
225
232
  let nodeCount = 0;
226
233
  const parseNode = (candidate: unknown, depth: number): CallFlowNode => {
@@ -254,7 +261,7 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
254
261
  };
255
262
 
256
263
  const raw = boundedRaw(value.result.ascii);
257
- const trees = value.result.trees.map((candidate): CallFlowTree => {
264
+ const trees = boundedTreeCandidates.map((candidate): CallFlowTree => {
258
265
  if (!isRecord(candidate)) throw new Error("CallDiff worker returned an invalid tree.");
259
266
  const entry = boundedString(candidate.entry, "tree entry");
260
267
  const tree = parseNode(candidate.tree, 0);
@@ -287,6 +294,12 @@ export function parseCallDiffWorkerResult(value: unknown): ParsedCallDiffWorkerR
287
294
  });
288
295
  }
289
296
  }
297
+ if (truncatedTrees) {
298
+ diagnostics.push({
299
+ level: "warning",
300
+ message: `Showing the first ${MAX_TREES.toLocaleString("en-US")} of ${totalTreeCount.toLocaleString("en-US")} call trees; the rest were truncated to bound the result.`,
301
+ });
302
+ }
290
303
 
291
304
  return {
292
305
  version: boundedString(value.version, "version", 100),
@@ -46,7 +46,8 @@ export type PromptRuntime =
46
46
  | "copilot-cli"
47
47
  | "pi"
48
48
  | "codex"
49
- | "gemini-cli";
49
+ | "gemini-cli"
50
+ | "oh-my-pi";
50
51
 
51
52
  interface PromptSectionConfig {
52
53
  [key: string]: string | Partial<Record<PromptRuntime, PromptSectionOverrides>> | undefined;
@@ -9,6 +9,7 @@ import {
9
9
  type JjEvoLogEntry,
10
10
  JJ_TRUNK_REVSET,
11
11
  jjLineBaseRevset,
12
+ parseRemoteBookmark,
12
13
  validateFilePath,
13
14
  } from "./review-core.ts";
14
15
 
@@ -27,6 +28,31 @@ export interface ReviewJjRuntime {
27
28
  ) => Promise<GitCommandResult>;
28
29
  }
29
30
 
31
+ // `reachable(@, mutable())` is JJ's definition of the stack being worked on.
32
+ // Its root parents are where that line diverged from immutable history.
33
+ //
34
+ // `latest(..., 1)` is what keeps the query single-record. A criss-cross history
35
+ // can leave several fork points, and the parser below reads one record only, so
36
+ // the tie-break belongs in the revset where it is deliberate and testable
37
+ // rather than in a silent "first row wins" slice. It also matters for
38
+ // correctness: bookmark preference (remote before local) is only meaningful
39
+ // within one commit, so a multi-row answer could otherwise pick a remote
40
+ // bookmark from one commit over a local bookmark on a nearer one.
41
+ const JJ_LINE_BASE_REVSET = "latest(fork_point(roots(reachable(@, mutable()))-), 1)";
42
+
43
+ // `jj git push --change` mints bookmarks under `git.push-bookmark-prefix`
44
+ // (default `push-`). They name one change, not a line of work, so they are
45
+ // never a useful review base. They reach the fork point in practice: a
46
+ // colleague's pushed change bookmark arrives as an untracked remote bookmark,
47
+ // which makes its commit immutable and therefore a candidate base, and the
48
+ // reviewer would be told they are comparing against `push-vmopwunwxopv@origin`.
49
+ const JJ_GENERATED_PUSH_BOOKMARK_PREFIX = "push-";
50
+
51
+ // `commit_id` renders in full, so an all-zero id is the virtual root commit.
52
+ // A repo with no immutable history forks there, and a 40-zero hash means
53
+ // nothing to a reviewer, so keep the `trunk()` sentinel for that case.
54
+ const JJ_ROOT_COMMIT_ID = /^0+$/;
55
+
30
56
  export async function detectJjWorkspace(
31
57
  runtime: ReviewJjRuntime,
32
58
  cwd?: string,
@@ -369,13 +395,35 @@ export async function selectDefaultJjCompareTarget(
369
395
  "log",
370
396
  "--no-graph",
371
397
  "-r",
372
- JJ_TRUNK_REVSET,
398
+ JJ_LINE_BASE_REVSET,
373
399
  "-T",
374
- "json(bookmarks)",
400
+ 'json(bookmarks) ++ "\\t" ++ commit_id ++ "\\n"',
375
401
  ], { cwd });
402
+ // Every unresolvable case falls back to `trunk()`, which is what this
403
+ // returned before the line-of-work base was inferred at all. The only live
404
+ // caller is `getJjContext`, which runs on the review startup path with no
405
+ // handler above it, so throwing here does not report a problem: it aborts
406
+ // `plannotator review` with a stack trace before the server is built. That
407
+ // also covers a `jj` too old for `fork_point`/`reachable`, where the revset
408
+ // itself fails and the previous default is still perfectly serviceable.
376
409
  if (result.exitCode !== 0) return JJ_TRUNK_REVSET;
377
410
 
378
- return parseJjResolvedBookmarks(result.stdout)[0] ?? JJ_TRUNK_REVSET;
411
+ const [record] = splitJjTemplateRecords(result.stdout);
412
+ if (!record) return JJ_TRUNK_REVSET;
413
+
414
+ const fields = splitJjTemplateFields(record);
415
+ const bookmark = parseJjResolvedBookmarks(fields?.[0] ?? record)
416
+ .find((name) => !isGeneratedPushBookmark(name));
417
+ if (bookmark) return bookmark;
418
+
419
+ const commitId = fields?.[1]?.trim();
420
+ if (commitId && !JJ_ROOT_COMMIT_ID.test(commitId)) return commitId;
421
+ return JJ_TRUNK_REVSET;
422
+ }
423
+
424
+ function isGeneratedPushBookmark(target: string): boolean {
425
+ const name = parseRemoteBookmark(target)?.name ?? target;
426
+ return name.startsWith(JJ_GENERATED_PUSH_BOOKMARK_PREFIX);
379
427
  }
380
428
 
381
429
  function parseJjResolvedBookmarks(value: string): string[] {
@@ -0,0 +1,133 @@
1
+ // @generated — DO NOT EDIT. Source: packages/shared/live-probe.ts
2
+ /**
3
+ * Shared live-app eligibility probe for annotate URL targets.
4
+ *
5
+ * One implementation of the "does this loopback URL front a live dev app?"
6
+ * decision, used by the Bun CLI's annotate resolution
7
+ * (apps/hook/server/annotate-resolution.ts) and the Pi extension's annotate
8
+ * command (vendored to generated/). Both hosts must probe identically: same
9
+ * 3s timeout, same `accept: text/html` request, same "any status < 500"
10
+ * gate, same final-response redirect rule — a drifting copy here would make
11
+ * the same URL open live on one agent and static on another.
12
+ *
13
+ * Pure Web-platform code: global fetch + AbortSignal.timeout, no Bun or
14
+ * node:http APIs.
15
+ */
16
+
17
+ import { isLoopbackHostname } from "./live-proxy-core.ts";
18
+
19
+ export const LIVE_PROBE_TIMEOUT_MS = 3000;
20
+
21
+ export interface LiveAppCandidate {
22
+ parsed: URL | null;
23
+ /** The URL parsed and its hostname is loopback (localhost / ::1 / 127/8). */
24
+ loopback: boolean;
25
+ }
26
+
27
+ /** Parse an annotate URL target and classify its live-mode candidacy. */
28
+ export function classifyLiveAppCandidate(url: string): LiveAppCandidate {
29
+ let parsed: URL | null = null;
30
+ try {
31
+ parsed = new URL(url);
32
+ } catch {
33
+ parsed = null;
34
+ }
35
+ return { parsed, loopback: parsed !== null && isLoopbackHostname(parsed.hostname) };
36
+ }
37
+
38
+ export interface LiveAppProbeResult {
39
+ liveEligible: boolean;
40
+ /** Fetch-level failure message, when the probe never completed. */
41
+ probeError: string | null;
42
+ /** Final URL when the probe redirected off the target's loopback origin. */
43
+ probeRedirectedTo: string | null;
44
+ }
45
+
46
+ /**
47
+ * Probe a loopback http URL for live eligibility. Live eligibility is judged
48
+ * on the FINAL response after redirects: a loopback URL that 302s off its
49
+ * own origin (auth portal, tunnel splash page, another local service) must
50
+ * not open a live session whose iframe immediately navigates off the proxy;
51
+ * same-server redirects (/ to /login) stay live-eligible. Any status < 500
52
+ * with an HTML content type qualifies, so a login page or SPA 404 still
53
+ * opens live.
54
+ */
55
+ export async function probeLiveAppTarget(
56
+ targetUrl: string,
57
+ parsedUrl: URL,
58
+ fetchImpl: typeof fetch = fetch,
59
+ ): Promise<LiveAppProbeResult> {
60
+ let liveEligible = false;
61
+ let probeError: string | null = null;
62
+ let probeRedirectedTo: string | null = null;
63
+ try {
64
+ const probe = await fetchImpl(targetUrl, {
65
+ headers: { accept: "text/html" },
66
+ redirect: "follow",
67
+ signal: AbortSignal.timeout(LIVE_PROBE_TIMEOUT_MS),
68
+ });
69
+ const contentType = probe.headers.get("content-type") ?? "";
70
+ let finalOnOrigin = false;
71
+ try {
72
+ const finalUrl = new URL(probe.url || targetUrl);
73
+ finalOnOrigin =
74
+ finalUrl.protocol === "http:"
75
+ && isLoopbackHostname(finalUrl.hostname)
76
+ && (finalUrl.port || "80") === (parsedUrl.port || "80");
77
+ if (!finalOnOrigin) probeRedirectedTo = probe.url;
78
+ } catch {
79
+ finalOnOrigin = false;
80
+ }
81
+ liveEligible = probe.status < 500 && contentType.includes("text/html") && finalOnOrigin;
82
+ } catch (err) {
83
+ probeError = err instanceof Error ? err.message : String(err);
84
+ }
85
+ return { liveEligible, probeError, probeRedirectedTo };
86
+ }
87
+
88
+ // --- Shared user-facing messages ---------------------------------------------
89
+ // Wording is shared so a Pi user and a CLI user read the SAME explanation for
90
+ // the same condition; hosts differ only in the channel (stderr vs notify).
91
+
92
+ /** --app given with a file/folder target. */
93
+ export const LIVE_APP_REQUIRES_URL_MESSAGE =
94
+ "--app requires a URL target (a running local app, e.g. http://localhost:5173)";
95
+
96
+ /** --app given with a non-loopback URL. */
97
+ export const LIVE_APP_REQUIRES_LOOPBACK_MESSAGE = "--app requires a localhost/loopback URL";
98
+
99
+ /** --app given with an https URL (the live proxy is http-only). */
100
+ export const LIVE_APP_REQUIRES_HTTP_MESSAGE =
101
+ "--app requires an http:// URL (the live app proxy does not support https upstreams)";
102
+
103
+ /** Live sessions are hard-off in remote mode; no override env var exists. */
104
+ export const LIVE_APP_REMOTE_MESSAGE =
105
+ "Live app annotation is unavailable in remote mode (PLANNOTATOR_REMOTE). Run locally, or use --static to annotate a converted snapshot of the page.";
106
+
107
+ /** The --app forced-live failure, keyed on what the probe found. */
108
+ export function buildForceAppFailureMessage(
109
+ targetUrl: string,
110
+ probe: LiveAppProbeResult,
111
+ ): string {
112
+ if (probe.probeError !== null) {
113
+ return `--app: could not reach ${targetUrl}: ${probe.probeError}`;
114
+ }
115
+ if (probe.probeRedirectedTo !== null) {
116
+ return `--app: ${targetUrl} redirected off its loopback origin (${probe.probeRedirectedTo}); live mode requires the app to serve HTML from the probed origin`;
117
+ }
118
+ return `--app: ${targetUrl} did not return an HTML page`;
119
+ }
120
+
121
+ /**
122
+ * Fallback notice when a live-eligible-looking URL probed as unreachable and
123
+ * the session degrades to static conversion. A dev server still starting up
124
+ * probes as unreachable, and a silent downgrade to static conversion reads
125
+ * as a broken live session, so the notice names the condition and the
126
+ * retry-with---app escape hatch.
127
+ */
128
+ export function buildLiveProbeFallbackNotice(targetUrl: string, probeError: string): string {
129
+ return (
130
+ `Live probe failed for ${targetUrl} (${probeError}); using static conversion. `
131
+ + `If a dev server is still starting up, retry with --app to force live mode.`
132
+ );
133
+ }