@cueloop/client 0.1.0-alpha.31 → 0.1.0-alpha.32

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.31",
3
+ "version": "0.1.0-alpha.32",
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.31",
11
- "@cueloop/extension-api": "0.1.0-alpha.31",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.31",
13
- "@cueloop/schema": "0.1.0-alpha.31",
10
+ "@cueloop/daemon": "0.1.0-alpha.32",
11
+ "@cueloop/extension-api": "0.1.0-alpha.32",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.32",
13
+ "@cueloop/schema": "0.1.0-alpha.32",
14
14
  "@opentui/core": "^0.5.2",
15
15
  "@opentui/keymap": "0.5.2",
16
16
  "@opentui/react": "^0.5.2",
@@ -10,7 +10,7 @@ import { ManualClock } from "@opentui/core/testing";
10
10
  import { DaemonServer } from "@cueloop/daemon";
11
11
  import type { ReviewSession } from "@cueloop/schema";
12
12
  import { App } from "./App";
13
- import { press, settle, waitForText } from "./test-support";
13
+ import { isolateUserConfig, press, settle, waitForText } from "./test-support";
14
14
 
15
15
  const PLAN = "# Plan\n\nShip the thing.\n";
16
16
 
@@ -18,12 +18,13 @@ let home: string;
18
18
  let server: DaemonServer;
19
19
  let session: ReviewSession;
20
20
  let configPath: string;
21
+ let restoreUserConfig: () => void;
21
22
  let clock: ManualClock;
22
23
 
23
24
  beforeEach(() => {
24
25
  home = mkdtempSync(join(tmpdir(), "cueloop-close-"));
25
26
  configPath = join(home, "config.toml");
26
- process.env.CUELOOP_CONFIG = configPath;
27
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
27
28
  server = new DaemonServer({ home, idleExitMs: 0 });
28
29
  server.start();
29
30
  session = server.core.sessionCreate({
@@ -33,7 +34,7 @@ beforeEach(() => {
33
34
  clock = new ManualClock();
34
35
  });
35
36
  afterEach(() => {
36
- delete process.env.CUELOOP_CONFIG;
37
+ restoreUserConfig();
37
38
  server.stop();
38
39
  rmSync(home, { recursive: true, force: true });
39
40
  });
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { press, waitForText } from "./test-support";
12
+ import { isolateUserConfig, press, waitForText } from "./test-support";
13
13
 
14
14
  const PATCH = `diff --git a/src/store.ts b/src/store.ts
15
15
  index 111..222 100644
@@ -23,11 +23,13 @@ index 111..222 100644
23
23
  `;
24
24
 
25
25
  let home: string;
26
+ let restoreUserConfig: () => void;
26
27
  let server: DaemonServer;
27
28
  let session: ReviewSession;
28
29
 
29
30
  beforeEach(() => {
30
31
  home = mkdtempSync(join(tmpdir(), "cueloop-diff-"));
32
+ restoreUserConfig = isolateUserConfig(home);
31
33
  server = new DaemonServer({ home, idleExitMs: 0 });
32
34
  server.start();
33
35
  session = server.core.sessionCreate({
@@ -36,6 +38,7 @@ beforeEach(() => {
36
38
  });
37
39
  });
38
40
  afterEach(() => {
41
+ restoreUserConfig();
39
42
  server.stop();
40
43
  rmSync(home, { recursive: true, force: true });
41
44
  });
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { press, waitForState, waitForText } from "./test-support";
12
+ import { isolateUserConfig, press, waitForState, waitForText } from "./test-support";
13
13
 
14
14
  const PLAN = `# Migration Plan
15
15
 
@@ -20,7 +20,7 @@ let home: string;
20
20
  let vault: string;
21
21
  let server: DaemonServer;
22
22
  let session: ReviewSession;
23
- let savedConfigEnv: string | undefined;
23
+ let restoreUserConfig: () => void;
24
24
 
25
25
  beforeEach(() => {
26
26
  home = mkdtempSync(join(tmpdir(), "cueloop-app-export-"));
@@ -28,8 +28,7 @@ beforeEach(() => {
28
28
  mkdirSync(vault);
29
29
  const configPath = join(home, "config.toml");
30
30
  writeFileSync(configPath, `[integrations.obsidian]\nvault = ${JSON.stringify(vault)}\nexportOn = "resolve"\n`);
31
- savedConfigEnv = process.env.CUELOOP_CONFIG;
32
- process.env.CUELOOP_CONFIG = configPath;
31
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
33
32
  server = new DaemonServer({ home, idleExitMs: 0 });
34
33
  server.start();
35
34
  session = server.core.sessionCreate({
@@ -38,8 +37,7 @@ beforeEach(() => {
38
37
  });
39
38
  });
40
39
  afterEach(() => {
41
- if (savedConfigEnv === undefined) delete process.env.CUELOOP_CONFIG;
42
- else process.env.CUELOOP_CONFIG = savedConfigEnv;
40
+ restoreUserConfig();
43
41
  server.stop();
44
42
  rmSync(home, { recursive: true, force: true });
45
43
  });
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { DARK as T } from "./theme";
13
- import { press, settle, typeText as type, waitForState, waitForText, waitForTextGone } from "./test-support";
13
+ import { isolateUserConfig, press, settle, typeText as type, waitForState, waitForText, waitForTextGone } from "./test-support";
14
14
 
15
15
  const PLAN = `# Migration Plan
16
16
 
@@ -27,14 +27,11 @@ The daemon persists sessions to disk atomically.
27
27
  let home: string;
28
28
  let server: DaemonServer;
29
29
  let session: ReviewSession;
30
- let priorConfig: string | undefined;
30
+ let restoreUserConfig: () => void;
31
31
 
32
32
  beforeEach(() => {
33
33
  home = mkdtempSync(join(tmpdir(), "cueloop-plan-review-"));
34
- // Isolate from the dev's real config so the rail state is the default here,
35
- // not whatever review_state they last persisted (CI runs clean already).
36
- priorConfig = process.env.CUELOOP_CONFIG;
37
- process.env.CUELOOP_CONFIG = join(home, "no-config.toml");
34
+ restoreUserConfig = isolateUserConfig(home);
38
35
  server = new DaemonServer({ home, idleExitMs: 0 });
39
36
  server.start();
40
37
  session = server.core.sessionCreate({
@@ -43,8 +40,7 @@ beforeEach(() => {
43
40
  });
44
41
  });
45
42
  afterEach(() => {
46
- if (priorConfig === undefined) delete process.env.CUELOOP_CONFIG;
47
- else process.env.CUELOOP_CONFIG = priorConfig;
43
+ restoreUserConfig();
48
44
  server.stop();
49
45
  rmSync(home, { recursive: true, force: true });
50
46
  });
@@ -250,7 +246,7 @@ describe("inline compose keeps the anchor painted", () => {
250
246
  // the whole cursor block is the anchor here; check the highlight landed
251
247
  const stored = server.core.sessionGet(session.id);
252
248
  expect(stored.annotations.length).toBe(1);
253
- expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(T.markCommentBg);
249
+ expect(backgroundsOf(setup, stored.annotations[0]!.anchor.quote.slice(0, 20))).toContain(T.markCommentBackground);
254
250
  // renderApp plus this many frame-waits grazes the 5s default on a loaded CI
255
251
  // runner, and the whole-suite publish lane has timed even 15s out; give the
256
252
  // heaviest frame-wait chain generous headroom so runner load cannot flake it.
@@ -396,7 +392,7 @@ describe("the document selects, the rail edits", () => {
396
392
  await press(setup, "enter");
397
393
  await waitForText(setup, "COMMENT · pending");
398
394
  expect(server.core.sessionGet(session.id).annotations.length).toBe(1);
399
- expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBg);
395
+ expect(backgroundsOf(setup, "persists sessions")).toContain(T.markCommentBackground);
400
396
 
401
397
  // Act
402
398
  await press(setup, "x");
@@ -404,7 +400,7 @@ describe("the document selects, the rail edits", () => {
404
400
  // Assert
405
401
  await waitForTextGone(setup, "COMMENT · pending");
406
402
  expect(server.core.sessionGet(session.id).annotations.length).toBe(0);
407
- expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBg);
403
+ expect(backgroundsOf(setup, "persists sessions")).not.toContain(T.markCommentBackground);
408
404
  });
409
405
  });
410
406
 
@@ -437,7 +433,7 @@ describe("addressed annotations leave the open list", () => {
437
433
  expect(frame).toContain("✓ 1 addressed by revision");
438
434
  expect(frame).not.toContain("settled note");
439
435
  expect(frame).toContain("Review (1)"); // the addressed card no longer counts as pending
440
- expect(backgroundsOf(setup, "The daemon")).not.toContain(T.markCommentBg); // no highlight paint
436
+ expect(backgroundsOf(setup, "The daemon")).not.toContain(T.markCommentBackground); // no highlight paint
441
437
  });
442
438
  });
443
439
 
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { press, waitForText } from "./test-support";
12
+ import { isolateUserConfig, press, waitForText } from "./test-support";
13
13
 
14
14
  const PLAN = `# Migration Plan
15
15
 
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
24
24
  `;
25
25
 
26
26
  let home: string;
27
+ let restoreUserConfig: () => void;
27
28
  let server: DaemonServer;
28
29
  let session: ReviewSession;
29
30
 
30
31
  beforeEach(() => {
31
32
  home = mkdtempSync(join(tmpdir(), "cueloop-observer-"));
33
+ restoreUserConfig = isolateUserConfig(home);
32
34
  server = new DaemonServer({ home, idleExitMs: 0 });
33
35
  server.start();
34
36
  session = server.core.sessionCreate({
@@ -37,6 +39,7 @@ beforeEach(() => {
37
39
  });
38
40
  });
39
41
  afterEach(() => {
42
+ restoreUserConfig();
40
43
  server.stop();
41
44
  rmSync(home, { recursive: true, force: true });
42
45
  });
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { waitForText } from "./test-support";
12
+ import { isolateUserConfig, waitForText } from "./test-support";
13
13
 
14
14
  const PLAN = `# Render Plan
15
15
 
@@ -30,11 +30,13 @@ Deploys block below the threshold.
30
30
  `;
31
31
 
32
32
  let home: string;
33
+ let restoreUserConfig: () => void;
33
34
  let server: DaemonServer;
34
35
  let session: ReviewSession;
35
36
 
36
37
  beforeEach(() => {
37
38
  home = mkdtempSync(join(tmpdir(), "cueloop-render-"));
39
+ restoreUserConfig = isolateUserConfig(home);
38
40
  server = new DaemonServer({ home, idleExitMs: 0 });
39
41
  server.start();
40
42
  session = server.core.sessionCreate({
@@ -43,6 +45,7 @@ beforeEach(() => {
43
45
  });
44
46
  });
45
47
  afterEach(() => {
48
+ restoreUserConfig();
46
49
  server.stop();
47
50
  rmSync(home, { recursive: true, force: true });
48
51
  });
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { loadConfig } from "./config";
13
- import { press, waitForText, waitForTextGone } from "./test-support";
13
+ import { isolateUserConfig, press, waitForText, waitForTextGone } from "./test-support";
14
14
 
15
15
  const PLAN = "# Plan\n\nShip the thing.\n";
16
16
 
@@ -18,11 +18,12 @@ let home: string;
18
18
  let server: DaemonServer;
19
19
  let session: ReviewSession;
20
20
  let configPath: string;
21
+ let restoreUserConfig: () => void;
21
22
 
22
23
  beforeEach(() => {
23
24
  home = mkdtempSync(join(tmpdir(), "cueloop-panel-"));
24
25
  configPath = join(home, "config.toml");
25
- process.env.CUELOOP_CONFIG = configPath;
26
+ restoreUserConfig = isolateUserConfig(home, "config.toml");
26
27
  server = new DaemonServer({ home, idleExitMs: 0 });
27
28
  server.start();
28
29
  session = server.core.sessionCreate({
@@ -31,7 +32,7 @@ beforeEach(() => {
31
32
  });
32
33
  });
33
34
  afterEach(() => {
34
- delete process.env.CUELOOP_CONFIG;
35
+ restoreUserConfig();
35
36
  server.stop();
36
37
  rmSync(home, { recursive: true, force: true });
37
38
  });
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import { makeAnchor, parseBlocks, type ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { press, settle, waitForText, waitForTextGone } from "./test-support";
12
+ import { isolateUserConfig, press, settle, waitForText, waitForTextGone } from "./test-support";
13
13
 
14
14
  const PLAN = `# Migration Plan
15
15
 
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
24
24
  `;
25
25
 
26
26
  let home: string;
27
+ let restoreUserConfig: () => void;
27
28
  let server: DaemonServer;
28
29
  let session: ReviewSession;
29
30
 
30
31
  beforeEach(() => {
31
32
  home = mkdtempSync(join(tmpdir(), "cueloop-confirm-"));
33
+ restoreUserConfig = isolateUserConfig(home);
32
34
  server = new DaemonServer({ home, idleExitMs: 0 });
33
35
  server.start();
34
36
  session = server.core.sessionCreate({
@@ -37,6 +39,7 @@ beforeEach(() => {
37
39
  });
38
40
  });
39
41
  afterEach(() => {
42
+ restoreUserConfig();
40
43
  server.stop();
41
44
  rmSync(home, { recursive: true, force: true });
42
45
  });
package/src/App.test.tsx CHANGED
@@ -9,7 +9,7 @@ import { testRender } from "@opentui/react/test-utils";
9
9
  import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
- import { press, typeText as type, waitForState, waitForText } from "./test-support";
12
+ import { isolateUserConfig, press, typeText as type, waitForState, waitForText } from "./test-support";
13
13
 
14
14
  const PLAN = `# Migration Plan
15
15
 
@@ -24,11 +24,13 @@ The daemon persists sessions to disk atomically.
24
24
  `;
25
25
 
26
26
  let home: string;
27
+ let restoreUserConfig: () => void;
27
28
  let server: DaemonServer;
28
29
  let session: ReviewSession;
29
30
 
30
31
  beforeEach(() => {
31
32
  home = mkdtempSync(join(tmpdir(), "cueloop-app-"));
33
+ restoreUserConfig = isolateUserConfig(home);
32
34
  server = new DaemonServer({ home, idleExitMs: 0 });
33
35
  server.start();
34
36
  session = server.core.sessionCreate({
@@ -37,6 +39,7 @@ beforeEach(() => {
37
39
  });
38
40
  });
39
41
  afterEach(() => {
42
+ restoreUserConfig();
40
43
  server.stop();
41
44
  rmSync(home, { recursive: true, force: true });
42
45
  });
package/src/App.tsx CHANGED
@@ -513,7 +513,7 @@ export function App({ home, sessionId, readOnly = false, onExit, clock, openClie
513
513
  return (
514
514
  <ThemeProvider theme={theme}>
515
515
  <box
516
- style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg }}
516
+ style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.background }}
517
517
  onMouseDrag={(event: MouseEvent) => {
518
518
  if (!dividerDragging || reviewMode !== "expanded") return;
519
519
  const next = widthFromMouseColumn(event.x, terminalWidth);
@@ -10,7 +10,7 @@ import { DaemonServer } from "@cueloop/daemon";
10
10
  import type { ReviewSession } from "@cueloop/schema";
11
11
  import { App } from "./App";
12
12
  import { DARK } from "./theme";
13
- import { press, waitForState, waitForText, waitForTextGone } from "./test-support";
13
+ import { isolateUserConfig, press, waitForState, waitForText, waitForTextGone } from "./test-support";
14
14
 
15
15
  const PATCH = `diff --git a/src/a.ts b/src/a.ts
16
16
  index 111..222 100644
@@ -38,11 +38,13 @@ index 555..666 100644
38
38
  `;
39
39
 
40
40
  let home: string;
41
+ let restoreUserConfig: () => void;
41
42
  let server: DaemonServer;
42
43
  let session: ReviewSession;
43
44
 
44
45
  beforeEach(() => {
45
46
  home = mkdtempSync(join(tmpdir(), "cueloop-walk-"));
47
+ restoreUserConfig = isolateUserConfig(home);
46
48
  server = new DaemonServer({ home, idleExitMs: 0 });
47
49
  server.start();
48
50
  session = server.core.sessionCreate({
@@ -51,6 +53,7 @@ beforeEach(() => {
51
53
  });
52
54
  });
53
55
  afterEach(() => {
56
+ restoreUserConfig();
54
57
  server.stop();
55
58
  rmSync(home, { recursive: true, force: true });
56
59
  });
@@ -236,7 +239,7 @@ describe("the guided walk", () => {
236
239
  // Assert
237
240
  // before the walk the sheet's added line wears the insertion color
238
241
  await waitForText(setup, "+new line");
239
- expect(foregroundsOf(setup, "+new line")).toContain(DARK.insFg);
242
+ expect(foregroundsOf(setup, "+new line")).toContain(DARK.insertedForeground);
240
243
 
241
244
  // Act
242
245
  await press(setup, "w");
@@ -246,7 +249,7 @@ describe("the guided walk", () => {
246
249
  // dimmed: the sheet line drops to the dim token...
247
250
  expect(foregroundsOf(setup, "+new line")).toEqual([DARK.textDim]);
248
251
  // ...while the wizard preview keeps the insertion color
249
- expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insFg);
252
+ expect(foregroundsOf(setup, "+export const b = 3;")).toContain(DARK.insertedForeground);
250
253
  });
251
254
 
252
255
  test("walking a resolved review answers read-only", async () => {
@@ -27,7 +27,7 @@ export function CompletionOverlay({ verdict, completion, status, returnsTo, them
27
27
  style={{
28
28
  width: "100%",
29
29
  height: "100%",
30
- backgroundColor: tokens.bg,
30
+ backgroundColor: tokens.background,
31
31
  flexDirection: "column",
32
32
  justifyContent: "center",
33
33
  alignItems: "center",
@@ -16,6 +16,6 @@ export const DeletePlan: Story = {
16
16
  {...callbacks}
17
17
  />
18
18
  ),
19
- expectedColors: [DARK.backdrop, DARK.elevated, DARK.accent],
19
+ expectedColors: [DARK.accent],
20
20
  size: { width: 64, height: 16 },
21
21
  };
@@ -8,13 +8,13 @@ export const meta: StoryMeta = { title: "DiffSheet" };
8
8
 
9
9
  export const SignedRows: Story = {
10
10
  render: () => <DiffSheet rows={fixtureDiffRows()} cursor={0} annotations={[]} />,
11
- expectedColors: [DARK.insFg, DARK.delFg],
11
+ expectedColors: [DARK.insertedForeground, DARK.deletedForeground],
12
12
  size: { width: 90, height: 16 },
13
13
  };
14
14
 
15
15
  export const CursorOnAddedLine: Story = {
16
16
  render: () => <DiffSheet rows={fixtureDiffRows()} cursor={4} annotations={[]} />,
17
- expectedColors: [DARK.cursorBg],
17
+ expectedColors: [DARK.cursorBackground],
18
18
  size: { width: 90, height: 16 },
19
19
  };
20
20
 
@@ -39,6 +39,6 @@ export const AnnotatedLine: Story = {
39
39
  ]}
40
40
  />
41
41
  ),
42
- expectedColors: [DARK.markCommentBg],
42
+ expectedColors: [DARK.markCommentBackground],
43
43
  size: { width: 90, height: 16 },
44
44
  };
@@ -100,10 +100,10 @@ function DiffChunk({
100
100
  // the renderable's config parser requires the gutter key to be present
101
101
  const colors = new Map<number, { gutter: string | undefined; content: string }>();
102
102
  if (segment.annotation) {
103
- colors.set(segment.rows.length - 1, { gutter: undefined, content: tokens.markCommentBg });
103
+ colors.set(segment.rows.length - 1, { gutter: undefined, content: tokens.markCommentBackground });
104
104
  }
105
105
  if (cursorInChunk >= 0 && cursorInChunk < segment.rows.length) {
106
- colors.set(cursorInChunk, { gutter: tokens.cursorBg, content: tokens.cursorBg });
106
+ colors.set(cursorInChunk, { gutter: tokens.cursorBackground, content: tokens.cursorBackground });
107
107
  }
108
108
  return colors;
109
109
  }, [segment, cursorInChunk, tokens]);
@@ -124,7 +124,7 @@ function DiffChunk({
124
124
  <line-number
125
125
  ref={gutterRef}
126
126
  fg={tokens.textDim}
127
- bg={tokens.bg}
127
+ bg={tokens.background}
128
128
  minWidth={4}
129
129
  paddingRight={1}
130
130
  lineNumbers={lineNumbers}
@@ -133,14 +133,14 @@ function DiffChunk({
133
133
  <text style={{ wrapMode: "none" }} selectable={false}>
134
134
  {segment.rows.map((row, lineIndex) => {
135
135
  const sign = row.kind === "add" ? "+" : row.kind === "del" ? "-" : " ";
136
- const foreground = row.kind === "add" ? tokens.insFg : row.kind === "del" ? tokens.delFg : tokens.textMuted;
136
+ const foreground = row.kind === "add" ? tokens.insertedForeground : row.kind === "del" ? tokens.deletedForeground : tokens.textMuted;
137
137
  const isCursorRow = lineIndex === cursorInChunk;
138
138
  const isAnnotatedRow = segment.annotation !== null && lineIndex === segment.rows.length - 1;
139
139
  return (
140
140
  <span
141
141
  key={lineIndex}
142
142
  fg={foreground}
143
- bg={isCursorRow ? tokens.cursorBg : isAnnotatedRow ? tokens.markCommentBg : undefined}
143
+ bg={isCursorRow ? tokens.cursorBackground : isAnnotatedRow ? tokens.markCommentBackground : undefined}
144
144
  >
145
145
  {(lineIndex > 0 ? "\n" : "") + sign + rowLine(row)}
146
146
  </span>
@@ -215,13 +215,13 @@ export function DiffSheet({ rows, cursor, annotations, focusedAnnotationId, them
215
215
  const isCursor = segment.rowIndex === cursor;
216
216
  if (segment.row.kind === "file") {
217
217
  return (
218
- <text key={segmentIndex} fg={tokens.text} bg={isCursor ? tokens.cursorBg : tokens.panel}>
218
+ <text key={segmentIndex} fg={tokens.text} bg={isCursor ? tokens.cursorBackground : tokens.panel}>
219
219
  {isCursor ? "▎" : " "}■ {rowLine(segment.row)}
220
220
  </text>
221
221
  );
222
222
  }
223
223
  return (
224
- <text key={segmentIndex} fg={tokens.blue} bg={isCursor ? tokens.cursorBg : undefined}>
224
+ <text key={segmentIndex} fg={tokens.blue} bg={isCursor ? tokens.cursorBackground : undefined}>
225
225
  {isCursor ? "▎" : " "}
226
226
  {rowLine(segment.row)}
227
227
  </text>
@@ -8,7 +8,7 @@ export const meta: StoryMeta = { title: "InboxList" };
8
8
 
9
9
  export const TwoPending: Story = {
10
10
  render: () => <InboxList inbox={[fixturePlanSession(), fixtureDiffSession()]} cursor={0} />,
11
- expectedColors: [DARK.accent, DARK.cursorBg],
11
+ expectedColors: [DARK.accent, DARK.cursorBackground],
12
12
  size: { width: 80, height: 12 },
13
13
  };
14
14
 
@@ -23,7 +23,7 @@ export interface InboxListProps {
23
23
  export function InboxList({ inbox, cursor, onRequestDelete, theme }: InboxListProps): React.ReactNode {
24
24
  const tokens = useComponentTheme(theme);
25
25
  return (
26
- <box style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: tokens.bg, padding: 1 }}>
26
+ <box style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: tokens.background, padding: 1 }}>
27
27
  <text fg={tokens.accent}>cueloop · inbox ({inbox.length} pending)</text>
28
28
  <text> </text>
29
29
  {inbox.length === 0 ? (
@@ -33,7 +33,7 @@ export function InboxList({ inbox, cursor, onRequestDelete, theme }: InboxListPr
33
33
  const selected = index === cursor;
34
34
  const title = session.artifact.meta.title ?? session.id;
35
35
  return (
36
- <box key={session.id} style={{ flexDirection: "row", backgroundColor: selected ? tokens.cursorBg : undefined }}>
36
+ <box key={session.id} style={{ flexDirection: "row", backgroundColor: selected ? tokens.cursorBackground : undefined }}>
37
37
  <text fg={selected ? tokens.text : tokens.textMuted}>
38
38
  {selected ? "▸ " : " "}
39
39
  {title} · {session.workspace.branch} · {session.artifact.type}
@@ -21,7 +21,7 @@ export const AnnotatedPlan: Story = {
21
21
  {...callbacks}
22
22
  />
23
23
  ),
24
- expectedColors: [DARK.markCommentBg, DARK.markSuggestionBg, DARK.cursorBg],
24
+ expectedColors: [DARK.markCommentBackground, DARK.markSuggestionBackground, DARK.cursorBackground],
25
25
  size: { width: 90, height: 28 },
26
26
  };
27
27
 
@@ -193,7 +193,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
193
193
  <span fg={tokens.textDim}>{marker(block)}</span>
194
194
  </text>
195
195
  <text
196
- bg={isCursor ? tokens.cursorBg : undefined}
196
+ bg={isCursor ? tokens.cursorBackground : undefined}
197
197
  selectable
198
198
  selectionBg={tokens.accent}
199
199
  selectionFg={tokens.accentInk}
@@ -228,7 +228,7 @@ export const PlanSheet = forwardRef<PlanSheetHandle, PlanSheetProps>(function Pl
228
228
  theme={theme}
229
229
  />
230
230
  {editOrphanCount > 0 ? (
231
- <box style={{ height: 1, backgroundColor: tokens.markCommentBg, paddingLeft: 2 }}>
231
+ <box style={{ height: 1, backgroundColor: tokens.markCommentBackground, paddingLeft: 2 }}>
232
232
  <text fg={tokens.red}>
233
233
  {editOrphanCount} annotation{editOrphanCount === 1 ? "" : "s"} no longer match - the passage was removed.
234
234
  </text>
@@ -312,13 +312,13 @@ function runStyle(run: StyleRun, block: DisplayBlock, tokens: Theme): { fg?: str
312
312
  const struck = block.type === "del";
313
313
  switch (run.role) {
314
314
  case "ins":
315
- return { fg: tokens.insFg };
315
+ return { fg: tokens.insertedForeground };
316
316
  case "del":
317
- return { fg: tokens.delFg };
317
+ return { fg: tokens.deletedForeground };
318
318
  case "mark-comment":
319
- return { fg: tokens.text, bg: tokens.markCommentBg };
319
+ return { fg: tokens.text, bg: tokens.markCommentBackground };
320
320
  case "mark-suggestion":
321
- return { fg: tokens.text, bg: tokens.markSuggestionBg };
321
+ return { fg: tokens.text, bg: tokens.markSuggestionBackground };
322
322
  case "mark-focus":
323
323
  return { fg: tokens.accentInk, bg: tokens.accent };
324
324
  case "kspan":
@@ -16,7 +16,7 @@ export const RenameAuthor: Story = {
16
16
  onInput={() => {}}
17
17
  />
18
18
  ),
19
- expectedColors: [DARK.backdrop, DARK.elevated, DARK.accent],
19
+ expectedColors: [DARK.elevated, DARK.accent],
20
20
  size: { width: 64, height: 16 },
21
21
  };
22
22
 
@@ -31,6 +31,6 @@ export const NameYourself: Story = {
31
31
  onInput={() => {}}
32
32
  />
33
33
  ),
34
- expectedColors: [DARK.backdrop, DARK.elevated],
34
+ expectedColors: [DARK.elevated],
35
35
  size: { width: 64, height: 16 },
36
36
  };
@@ -27,7 +27,7 @@ const panelCallbacks = { onDividerGrab: () => {}, onToggle: () => {} };
27
27
  /** The plan area to the left of the divider, so the layout reads in context. */
28
28
  function PanelFrame({ children }: { children: React.ReactNode }): React.ReactNode {
29
29
  return (
30
- <box style={{ flexDirection: "row", width: "100%", height: "100%", backgroundColor: DARK.bg }}>
30
+ <box style={{ flexDirection: "row", width: "100%", height: "100%", backgroundColor: DARK.background }}>
31
31
  <box style={{ flexGrow: 1, paddingLeft: 1 }}>
32
32
  <text fg={DARK.textMuted}>The plan takes the width the panel hands back.</text>
33
33
  </box>
@@ -41,7 +41,7 @@ export const NavZone: Story = {
41
41
  onRowActivate={() => {}}
42
42
  />
43
43
  ),
44
- expectedColors: [DARK.accent, DARK.backdrop],
44
+ expectedColors: [DARK.accent],
45
45
  size: { width: 100, height: 30 },
46
46
  };
47
47
 
@@ -40,7 +40,7 @@ export const FirstFile: Story = {
40
40
  />
41
41
  ),
42
42
  // the main card wears the accent border; stats carry the +/- colors
43
- expectedColors: [DARK.accent, DARK.green, DARK.red, DARK.insFg, DARK.delFg],
43
+ expectedColors: [DARK.accent, DARK.green, DARK.red, DARK.insertedForeground, DARK.deletedForeground],
44
44
  };
45
45
 
46
46
  export const WithAgentNote: Story = {
@@ -118,7 +118,7 @@ export function WalkWizard({
118
118
  <text
119
119
  key={previewIndex}
120
120
  style={{ wrapMode: "none" }}
121
- fg={previewLine.sign === "+" ? tokens.insFg : tokens.delFg}
121
+ fg={previewLine.sign === "+" ? tokens.insertedForeground : tokens.deletedForeground}
122
122
  >
123
123
  {previewLine.sign + previewLine.text}
124
124
  </text>
@@ -11,7 +11,7 @@ export const Open: Story = {
11
11
  <text>dialog body content</text>
12
12
  </Dialog>
13
13
  ),
14
- expectedColors: [DARK.accent, DARK.backdrop],
14
+ expectedColors: [DARK.accent],
15
15
  size: { width: 60, height: 16 },
16
16
  };
17
17
 
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Centered modal over a dimmed backdrop. The dialog fills the terminal with
3
- * the backdrop token and centers a bordered panel; content composes inside.
2
+ * Centered modal: a full-screen layer (backdrop token, transparent by
3
+ * default) centers a bordered, unpainted panel over the session - the frame
4
+ * delineates the dialog, the content behind stays visible.
4
5
  * `isOpen` keeps the mount decision with the caller-visible prop vocabulary.
5
6
  */
6
7
 
@@ -41,8 +42,6 @@ export function Dialog({ isOpen, title, width, height, theme, children }: Dialog
41
42
  border: true,
42
43
  borderStyle: FRAME_BORDER_STYLE,
43
44
  borderColor: tokens.accent,
44
- /* Floating surface: painted so dialog text never mixes with the page behind. */
45
- backgroundColor: tokens.elevated,
46
45
  flexDirection: "column",
47
46
  }}
48
47
  title={title}
@@ -38,7 +38,7 @@ function StoriesApp({ stories, onExit }: { stories: LoadedStory[]; onExit: () =>
38
38
 
39
39
  return (
40
40
  <ThemeProvider theme={DARK}>
41
- <box style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: DARK.bg }}>
41
+ <box style={{ flexDirection: "column", width: "100%", height: "100%", backgroundColor: DARK.background }}>
42
42
  <box style={{ height: 1, backgroundColor: DARK.panel, paddingLeft: 1 }}>
43
43
  <text fg={DARK.text}>
44
44
  <span fg={DARK.accent}>cueloop stories</span>
@@ -9,6 +9,7 @@ import { describe, expect, test } from "bun:test";
9
9
  import { testRender } from "@opentui/react/test-utils";
10
10
  import { RGBA } from "@opentui/core";
11
11
  import { componentFilesMissingStories, loadStories } from "./story";
12
+ import { allowEventLoopUpdates } from "../test-support";
12
13
 
13
14
  const stories = await loadStories();
14
15
 
@@ -35,6 +36,7 @@ describe("stories catalog", () => {
35
36
 
36
37
  // Act
37
38
  const setup = await testRender(story.render(), size);
39
+ allowEventLoopUpdates();
38
40
  await setup.waitForVisualIdle();
39
41
  const frame = setup.captureCharFrame();
40
42
 
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { loadConfig } from "./config";
6
+ import { isolateUserConfig } from "./test-support";
7
+
8
+ describe("isolateUserConfig", () => {
9
+ test("loadConfig sees defaults while isolated and the env is restored after", () => {
10
+ // Arrange
11
+ const home = mkdtempSync(join(tmpdir(), "cueloop-isolate-"));
12
+ const userConfigPath = join(home, "config.toml");
13
+ writeFileSync(userConfigPath, '[ui]\nreview_state = "compact"\n');
14
+ const priorEnv = process.env.CUELOOP_CONFIG;
15
+ process.env.CUELOOP_CONFIG = userConfigPath;
16
+
17
+ try {
18
+ // Act
19
+ const restoreUserConfig = isolateUserConfig(home);
20
+ const isolated = loadConfig({ repoRoot: home });
21
+ restoreUserConfig();
22
+ const restored = loadConfig({ repoRoot: home });
23
+
24
+ // Assert
25
+ expect(isolated.ui.reviewState).toBe("expanded");
26
+ expect(process.env.CUELOOP_CONFIG).toBe(userConfigPath);
27
+ expect(restored.ui.reviewState).toBe("compact");
28
+ } finally {
29
+ if (priorEnv === undefined) delete process.env.CUELOOP_CONFIG;
30
+ else process.env.CUELOOP_CONFIG = priorEnv;
31
+ rmSync(home, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("restore removes the variable when nothing was set before", () => {
36
+ // Arrange
37
+ const home = mkdtempSync(join(tmpdir(), "cueloop-isolate-"));
38
+ const priorEnv = process.env.CUELOOP_CONFIG;
39
+ delete process.env.CUELOOP_CONFIG;
40
+
41
+ try {
42
+ // Act
43
+ const restoreUserConfig = isolateUserConfig(home);
44
+ const pointedAt = process.env.CUELOOP_CONFIG ?? "";
45
+ restoreUserConfig();
46
+
47
+ // Assert
48
+ expect(pointedAt).toBe(join(home, "no-config.toml"));
49
+ expect(process.env.CUELOOP_CONFIG).toBeUndefined();
50
+ } finally {
51
+ if (priorEnv !== undefined) process.env.CUELOOP_CONFIG = priorEnv;
52
+ rmSync(home, { recursive: true, force: true });
53
+ }
54
+ });
55
+ });
@@ -6,16 +6,40 @@
6
6
  * generous pass budgets.
7
7
  */
8
8
 
9
+ import { join } from "node:path";
9
10
  import type { TestRendererSetup } from "@opentui/core/testing";
10
11
 
11
12
  /** Pass budget for waits that include daemon or subprocess round-trips. */
12
13
  export const WAIT_PASSES = { maxPasses: 400 };
13
14
 
15
+ /**
16
+ * Isolate the user config: point CUELOOP_CONFIG into the test home so
17
+ * loadConfig never reads the developer's real ~/.config/cueloop (a persisted
18
+ * review_state would change what char frames render locally while CI stays
19
+ * clean). The default file name does not exist, so loadConfig returns
20
+ * defaults; pass a file name for suites that write and assert a config of
21
+ * their own. Call in beforeEach; invoke the returned restore in afterEach.
22
+ */
23
+ export function isolateUserConfig(home: string, fileName = "no-config.toml"): () => void {
24
+ const priorUserConfig = process.env.CUELOOP_CONFIG;
25
+ process.env.CUELOOP_CONFIG = join(home, fileName);
26
+ return () => {
27
+ if (priorUserConfig === undefined) delete process.env.CUELOOP_CONFIG;
28
+ else process.env.CUELOOP_CONFIG = priorUserConfig;
29
+ };
30
+ }
31
+
32
+ /** The harness drives the real event loop by design; keep React's act warning off. */
33
+ export function allowEventLoopUpdates(): void {
34
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = false;
35
+ }
36
+
14
37
  /**
15
38
  * One macrotask yield lets the input parser and the React scheduler run,
16
39
  * then a render pass commits the result before the visual-idle wait.
17
40
  */
18
41
  export async function settle(setup: TestRendererSetup): Promise<void> {
42
+ allowEventLoopUpdates();
19
43
  await new Promise((resolve) => setTimeout(resolve, 0));
20
44
  await setup.renderOnce();
21
45
  await setup.waitForVisualIdle();
@@ -50,6 +74,7 @@ async function waitForFramePredicate(
50
74
  setup: TestRendererSetup,
51
75
  predicate: (frame: string) => boolean,
52
76
  ): Promise<string> {
77
+ allowEventLoopUpdates();
53
78
  const deadline = Date.now() + WAIT_DEADLINE_MS;
54
79
  for (;;) {
55
80
  try {
@@ -64,6 +89,7 @@ async function waitForFramePredicate(
64
89
 
65
90
  /** Wait until a state predicate holds (daemon round-trips included). */
66
91
  export async function waitForState(setup: TestRendererSetup, predicate: () => boolean): Promise<void> {
92
+ allowEventLoopUpdates();
67
93
  const deadline = Date.now() + WAIT_DEADLINE_MS;
68
94
  for (;;) {
69
95
  try {
package/src/theme.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export interface Theme {
9
- bg: string;
9
+ background: string;
10
10
  panel: string;
11
11
  elevated: string;
12
12
  border: string;
@@ -18,12 +18,12 @@ export interface Theme {
18
18
  green: string;
19
19
  red: string;
20
20
  blue: string;
21
- cursorBg: string;
22
- markCommentBg: string;
23
- markSuggestionBg: string;
24
- insFg: string;
25
- delFg: string;
26
- /** Dimmed layer painted behind centered dialogs. */
21
+ cursorBackground: string;
22
+ markCommentBackground: string;
23
+ markSuggestionBackground: string;
24
+ insertedForeground: string;
25
+ deletedForeground: string;
26
+ /** Layer behind centered dialogs; transparent keeps the session visible. */
27
27
  backdrop: string;
28
28
  }
29
29
 
@@ -41,14 +41,14 @@ export function dimmedTheme(theme: Theme): Theme {
41
41
  green: theme.textDim,
42
42
  red: theme.textDim,
43
43
  blue: theme.textDim,
44
- insFg: theme.textDim,
45
- delFg: theme.textDim,
44
+ insertedForeground: theme.textDim,
45
+ deletedForeground: theme.textDim,
46
46
  };
47
47
  }
48
48
 
49
49
  export const DARK: Theme = {
50
50
  /* Unpainted so the terminal background shows through; override in [theme]. */
51
- bg: "transparent",
51
+ background: "transparent",
52
52
  panel: "transparent",
53
53
  elevated: "#23232f",
54
54
  border: "#3a3a4a",
@@ -60,10 +60,10 @@ export const DARK: Theme = {
60
60
  green: "#62d96b",
61
61
  red: "#ff6b6b",
62
62
  blue: "#84a6e8",
63
- cursorBg: "#33262a",
64
- markCommentBg: "#3d2a2e",
65
- markSuggestionBg: "#20351f",
66
- insFg: "#62d96b",
67
- delFg: "#ff6b6b",
68
- backdrop: "#14141b",
63
+ cursorBackground: "#33262a",
64
+ markCommentBackground: "#3d2a2e",
65
+ markSuggestionBackground: "#20351f",
66
+ insertedForeground: "#62d96b",
67
+ deletedForeground: "#ff6b6b",
68
+ backdrop: "transparent",
69
69
  }