@cueloop/client 0.1.0-alpha.17 → 0.1.0-alpha.18

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/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.17",
3
+ "version": "0.1.0-alpha.18",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@cueloop/daemon": "0.1.0-alpha.17",
11
- "@cueloop/extension-api": "0.1.0-alpha.17",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.17",
13
- "@cueloop/schema": "0.1.0-alpha.17",
10
+ "@cueloop/daemon": "0.1.0-alpha.18",
11
+ "@cueloop/extension-api": "0.1.0-alpha.18",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.18",
13
+ "@cueloop/schema": "0.1.0-alpha.18",
14
14
  "@opentui/core": "^0.5.1",
15
15
  "@opentui/keymap": "0.5.1",
16
16
  "@opentui/react": "^0.5.1",
package/src/App.tsx CHANGED
@@ -17,7 +17,7 @@ import { displayText, marksByDisplay, spanKey, startSpan, type Mark, type SpanSt
17
17
  import { noteForFile, viewedCount } from "./walk";
18
18
  import { DARK, dimmedTheme } from "./theme";
19
19
  import { DEFAULT_KEYS, loadConfig } from "./config";
20
- import { returnPaneFor } from "./herdr";
20
+ import { returnPaneFor } from "@cueloop/schema";
21
21
  import { createReviewController } from "./session-controller";
22
22
  import { reduceKey, type Intent, type KeyState } from "./keymap";
23
23
  import { KeyBindings, type HintMode } from "./key-bindings";
package/src/herdr.test.ts CHANGED
@@ -2,46 +2,17 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { focusHerdrPane, returnPaneFor } from "./herdr";
5
+ import { focusHerdrPane } from "./herdr";
6
6
 
7
7
  let dir: string;
8
- const savedEnv: Record<string, string | undefined> = {};
9
8
 
10
9
  beforeEach(() => {
11
10
  dir = mkdtempSync(join(tmpdir(), "cueloop-herdr-"));
12
- for (const k of ["HERDR_ENV", "HERDR_PANE_ID", "HERDR_BIN_PATH", "CUELOOP_RETURN_PANE"]) {
13
- savedEnv[k] = process.env[k];
14
- delete process.env[k];
15
- }
16
11
  });
17
12
  afterEach(() => {
18
- for (const [k, v] of Object.entries(savedEnv)) {
19
- if (v === undefined) delete process.env[k];
20
- else process.env[k] = v;
21
- }
22
13
  rmSync(dir, { recursive: true, force: true });
23
14
  });
24
15
 
25
- describe("returnPaneFor", () => {
26
- test("outside herdr there is never a return target", () => {
27
- expect(returnPaneFor("w1:p1")).toBeUndefined();
28
- });
29
-
30
- test("env override wins, session meta is the fallback", () => {
31
- process.env.HERDR_ENV = "1";
32
- process.env.HERDR_PANE_ID = "w1:p9";
33
- expect(returnPaneFor("w1:p1")).toBe("w1:p1");
34
- process.env.CUELOOP_RETURN_PANE = "w1:p2";
35
- expect(returnPaneFor("w1:p1")).toBe("w1:p2");
36
- });
37
-
38
- test("returning to our own pane is a no-op", () => {
39
- process.env.HERDR_ENV = "1";
40
- process.env.HERDR_PANE_ID = "w1:p1";
41
- expect(returnPaneFor("w1:p1")).toBeUndefined();
42
- });
43
- });
44
-
45
16
  describe("focusHerdrPane", () => {
46
17
  test("resolves the pane's tab and focuses it", () => {
47
18
  const log = join(dir, "calls.log");
@@ -54,15 +25,13 @@ if [ "$1" = "pane" ]; then echo '{"result":{"pane":{"tab_id":"w1:t3"}}}'; fi
54
25
  `,
55
26
  );
56
27
  Bun.spawnSync(["chmod", "+x", stub]);
57
- process.env.HERDR_BIN_PATH = stub;
58
- expect(focusHerdrPane("w1:p1")).toBe(true);
28
+ expect(focusHerdrPane(stub, "w1:p1")).toBe(true);
59
29
  const calls = readFileSync(log, "utf8").trim().split("\n");
60
30
  expect(calls[0]).toBe("pane get w1:p1");
61
31
  expect(calls[1]).toBe("tab focus w1:t3");
62
32
  });
63
33
 
64
34
  test("a missing binary never throws", () => {
65
- process.env.HERDR_BIN_PATH = join(dir, "nope");
66
- expect(focusHerdrPane("w1:p1")).toBe(false);
35
+ expect(focusHerdrPane(join(dir, "nope"), "w1:p1")).toBe(false);
67
36
  });
68
37
  });
package/src/herdr.ts CHANGED
@@ -1,33 +1,20 @@
1
1
  /**
2
- * First-class herdr return: a review opened beside an agent should hand
3
- * focus back to the agent's pane when it closes, not leave the reviewer on a
4
- * dead tab. Detection is the ambient env contract; the return target comes
5
- * from CUELOOP_RETURN_PANE (set by whoever opened this pane) or the session's
6
- * recorded herdrPane (set by the adapter hook). Everything here is
7
- * best-effort: focus failures never block closing the review.
2
+ * First-class herdr return: a review opened beside an agent hands focus back
3
+ * to the agent's pane when it closes, not leaving the reviewer on a dead tab.
4
+ * The env contract and the return-target rule live in @cueloop/schema; this
5
+ * module owns the one reviewer-side action that spawns the herdr CLI. Focus
6
+ * is best-effort - a failure never blocks closing the review.
8
7
  */
9
8
 
10
- export function insideHerdr(): boolean {
11
- return process.env.HERDR_ENV === "1";
12
- }
13
-
14
- export function returnPaneFor(sessionHerdrPane?: string): string | undefined {
15
- if (!insideHerdr()) return undefined;
16
- const pane = process.env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
17
- // returning to our own pane would be a no-op at best
18
- return pane && pane !== process.env.HERDR_PANE_ID ? pane : undefined;
19
- }
20
-
21
9
  /** Focus the tab holding the target pane. Synchronous: runs just before exit. */
22
- export function focusHerdrPane(paneId: string): boolean {
23
- const bin = process.env.HERDR_BIN_PATH ?? "herdr";
10
+ export function focusHerdrPane(binPath: string, paneId: string): boolean {
24
11
  try {
25
- const got = Bun.spawnSync([bin, "pane", "get", paneId], { stdout: "pipe", stderr: "ignore" });
12
+ const got = Bun.spawnSync([binPath, "pane", "get", paneId], { stdout: "pipe", stderr: "ignore" });
26
13
  if (got.exitCode !== 0) return false;
27
14
  const parsed = JSON.parse(got.stdout.toString()) as { result?: { pane?: { tab_id?: string } } };
28
15
  const tab = parsed.result?.pane?.tab_id;
29
16
  if (!tab) return false;
30
- return Bun.spawnSync([bin, "tab", "focus", tab], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
17
+ return Bun.spawnSync([binPath, "tab", "focus", tab], { stdout: "ignore", stderr: "ignore" }).exitCode === 0;
31
18
  } catch {
32
19
  return false;
33
20
  }
@@ -11,12 +11,14 @@ import { SystemClock, type Clock, type TimerHandle } from "@opentui/core";
11
11
  import { DaemonClient } from "@cueloop/daemon/client";
12
12
  import {
13
13
  cutBlock,
14
+ detectHerdr,
14
15
  makeAnchor,
15
16
  newAnnotationId,
16
17
  parseBlocks,
17
18
  resolveAnchor,
18
19
  restoreBlock,
19
20
  restoreLine,
21
+ returnPaneFor,
20
22
  type ReviewSession,
21
23
  type VerdictKind,
22
24
  } from "@cueloop/schema";
@@ -26,7 +28,7 @@ import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view-plan";
26
28
  import { diffRowAnchor, diffRows, type DiffRow } from "./view-diff";
27
29
  import { firstUnviewedIndex, walkFiles, type WalkFile } from "./walk";
28
30
  import { editInEditor } from "./editor";
29
- import { focusHerdrPane, returnPaneFor } from "./herdr";
31
+ import { focusHerdrPane } from "./herdr";
30
32
  import { persistAutoClose, type AutoClose, type CueloopConfig } from "./config";
31
33
 
32
34
  /**
@@ -474,8 +476,9 @@ class Controller implements ReviewController {
474
476
 
475
477
  finishReview(): void {
476
478
  this.clearCountdown();
479
+ const herdr = detectHerdr();
477
480
  const pane = returnPaneFor(this.snapshot.session?.artifact.meta.herdrPane);
478
- if (pane) focusHerdrPane(pane);
481
+ if (herdr && pane) focusHerdrPane(herdr.binPath, pane);
479
482
  this.options.onExit?.(0);
480
483
  }
481
484