@cueloop/client 0.1.0-alpha.16 → 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/src/serve.test.ts CHANGED
@@ -38,7 +38,7 @@ afterEach(async () => {
38
38
  });
39
39
 
40
40
  /** Connect, open a PTY shell, resolve with the bytes seen until `until` matches. */
41
- function sshCapture(port: number, until: (s: string) => boolean, timeoutMs = 8000): Promise<string> {
41
+ function sshCapture(port: number, until: (frame: string) => boolean, timeoutMs = 8000): Promise<string> {
42
42
  return new Promise((resolve, reject) => {
43
43
  const conn = new Client();
44
44
  let out = "";
@@ -55,8 +55,8 @@ function sshCapture(port: number, until: (s: string) => boolean, timeoutMs = 800
55
55
  .on("ready", () => {
56
56
  conn.shell({ term: "xterm-256color", cols: 100, rows: 30 }, (err, stream) => {
57
57
  if (err) return finish(err);
58
- stream.on("data", (d: Buffer) => {
59
- out += d.toString("utf8");
58
+ stream.on("data", (chunk: Buffer) => {
59
+ out += chunk.toString("utf8");
60
60
  if (until(out)) finish();
61
61
  });
62
62
  stream.on("close", () => finish(new Error(`stream closed early; captured:\n${out}`)));
@@ -78,7 +78,7 @@ describe("cueloop serve", () => {
78
78
  });
79
79
 
80
80
  test("an anonymous ssh client gets the observer TUI for the session", async () => {
81
- const bytes = await sshCapture(handle.port, (s) => s.includes("Rollout Plan") && s.includes("observer"));
81
+ const bytes = await sshCapture(handle.port, (frame) => frame.includes("Rollout Plan") && frame.includes("observer"));
82
82
  expect(bytes).toContain("cueloop");
83
83
  expect(bytes).toContain("Rollout Plan");
84
84
  // observer chrome, not the controller's mutating hint bar
@@ -87,8 +87,8 @@ describe("cueloop serve", () => {
87
87
 
88
88
  test("two observers can watch at once", async () => {
89
89
  const [a, b] = await Promise.all([
90
- sshCapture(handle.port, (s) => s.includes("Rollout Plan")),
91
- sshCapture(handle.port, (s) => s.includes("Rollout Plan")),
90
+ sshCapture(handle.port, (frame) => frame.includes("Rollout Plan")),
91
+ sshCapture(handle.port, (frame) => frame.includes("Rollout Plan")),
92
92
  ]);
93
93
  expect(a).toContain("Rollout Plan");
94
94
  expect(b).toContain("Rollout Plan");
package/src/serve.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * SSH-served TUI (#22 share path): `cueloop serve` lets teammates join a
2
+ * SSH-served TUI: `cueloop serve` lets teammates join a
3
3
  * review session over plain ssh. Every SSH connection renders <App> in
4
4
  * observer mode (readOnly) against the same local daemon; the one writable
5
5
  * controller stays the local owner's own `cueloop` TUI.
@@ -44,8 +44,8 @@ export interface ServeHandle {
44
44
  close(): Promise<void>;
45
45
  }
46
46
 
47
- export async function serveClient(opts: ServeOptions = {}): Promise<ServeHandle> {
48
- const home = opts.home ?? cueloopHome();
47
+ export async function serveClient(options: ServeOptions = {}): Promise<ServeHandle> {
48
+ const home = options.home ?? cueloopHome();
49
49
  const sshDir = join(home, "ssh");
50
50
  mkdirSync(sshDir, { recursive: true, mode: 0o700 });
51
51
 
@@ -53,15 +53,15 @@ export async function serveClient(opts: ServeOptions = {}): Promise<ServeHandle>
53
53
  // password-less by design; see the trust model in the module comment
54
54
  auth: "open",
55
55
  hostKey: { path: join(sshDir, "host_key") },
56
- startupBanner: opts.banner ?? true,
56
+ startupBanner: options.banner ?? true,
57
57
  idleTimeout: "2h",
58
- onError: opts.onError,
58
+ onError: options.onError,
59
59
  }).serve((session) => {
60
60
  const root = createRoot(session.renderer);
61
61
  root.render(
62
62
  React.createElement(App, {
63
63
  home,
64
- sessionId: opts.sessionId,
64
+ sessionId: options.sessionId,
65
65
  readOnly: true,
66
66
  // q disconnects only this observer, never the server
67
67
  onExit: () => session.end(),
@@ -70,6 +70,6 @@ export async function serveClient(opts: ServeOptions = {}): Promise<ServeHandle>
70
70
  session.onClose(() => root.unmount());
71
71
  });
72
72
 
73
- const info = await server.listen(opts.port ?? 2222, opts.host ?? "127.0.0.1");
73
+ const info = await server.listen(options.port ?? 2222, options.host ?? "127.0.0.1");
74
74
  return { ...info, close: () => server.close() };
75
75
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The review-session controller (#70): every daemon round-trip and mutation
2
+ * The review-session controller: every daemon round-trip and mutation
3
3
  * verb behind one React-free object. It owns connect/autostart/subscribe,
4
4
  * the session/inbox/status/error snapshot, optimistic apply, the mutation
5
5
  * verbs (cut/edit/annotate/submit/...), and the post-submit completion
@@ -11,22 +11,24 @@ 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";
23
25
  import { Registry, type Exporter } from "@cueloop/extension-api";
24
26
  import { createObsidianExtension, shouldExport, type ObsidianConfig } from "@cueloop/integration-obsidian";
25
- import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view";
27
+ 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
  /**
@@ -81,8 +83,8 @@ export interface ReviewController {
81
83
  connect(): void;
82
84
  close(): void;
83
85
  /** Loaded config parts the controller acts on: auto-close and exporters. */
84
- applyConfig(cfg: CueloopConfig): void;
85
- setStatus(msg: string): void;
86
+ applyConfig(config: CueloopConfig): void;
87
+ setStatus(message: string): void;
86
88
  /** Derived projections, cached per session identity. */
87
89
  display(): DisplayBlock[];
88
90
  rows(): DiffRow[];
@@ -92,14 +94,14 @@ export interface ReviewController {
92
94
  /** Open a session from the inbox. */
93
95
  open(id: string): void;
94
96
  /** Cut the block under the cursor, or restore a cut one. */
95
- cut(dispIdx: number): void;
97
+ cut(displayIndex: number): void;
96
98
  /** The $EDITOR hand-off on the working copy. */
97
99
  edit(): void;
98
100
  /**
99
101
  * Anchor and store an annotation; both plan and diff anchor constructions.
100
102
  * Returns the minted annotation id so the view can select the new card.
101
103
  */
102
- annotate(kind: "comment" | "suggestion", dispIdx: number, start: number, end: number, body: string): string | undefined;
104
+ annotate(kind: "comment" | "suggestion", displayIndex: number, start: number, end: number, body: string): string | undefined;
103
105
  /** Rewrite a stored annotation's body in place (the rail-card edit). */
104
106
  updateAnnotation(id: string, body: string): void;
105
107
  removeAnnotation(id: string): void;
@@ -120,15 +122,15 @@ export interface ReviewController {
120
122
  optInAutoClose(): void;
121
123
  }
122
124
 
123
- export function createReviewController(opts: ReviewControllerOptions): ReviewController {
124
- return new Controller(opts);
125
+ export function createReviewController(options: ReviewControllerOptions): ReviewController {
126
+ return new Controller(options);
125
127
  }
126
128
 
127
129
  class Controller implements ReviewController {
128
130
  readonly readOnly: boolean;
129
131
  private client: DaemonClient | null = null;
130
132
  private closed = false;
131
- private snap: ControllerSnapshot = {
133
+ private snapshot: ControllerSnapshot = {
132
134
  session: null,
133
135
  inbox: null,
134
136
  status: "",
@@ -151,9 +153,9 @@ class Controller implements ReviewController {
151
153
  files: [],
152
154
  };
153
155
 
154
- constructor(private readonly opts: ReviewControllerOptions) {
155
- this.readOnly = opts.readOnly ?? false;
156
- this.clock = opts.clock ?? new SystemClock();
156
+ constructor(private readonly options: ReviewControllerOptions) {
157
+ this.readOnly = options.readOnly ?? false;
158
+ this.clock = options.clock ?? new SystemClock();
157
159
  }
158
160
 
159
161
  subscribe = (listener: () => void): (() => void) => {
@@ -161,28 +163,28 @@ class Controller implements ReviewController {
161
163
  return () => this.listeners.delete(listener);
162
164
  };
163
165
 
164
- getSnapshot = (): ControllerSnapshot => this.snap;
166
+ getSnapshot = (): ControllerSnapshot => this.snapshot;
165
167
 
166
168
  private update(patch: Partial<ControllerSnapshot>): void {
167
- this.snap = { ...this.snap, ...patch };
168
- for (const l of this.listeners) l();
169
+ this.snapshot = { ...this.snapshot, ...patch };
170
+ for (const listener of this.listeners) listener();
169
171
  }
170
172
 
171
173
  connect(): void {
172
174
  void (async () => {
173
175
  try {
174
- const client = await DaemonClient.connect({ home: this.opts.home, autostart: true });
176
+ const client = await DaemonClient.connect({ home: this.options.home, autostart: true });
175
177
  if (this.closed) return void client.close();
176
178
  this.client = client;
177
- client.onEvent((e) => {
179
+ client.onEvent((event) => {
178
180
  // another controller/observer changed state: re-fetch
179
- const s = this.snap.session;
180
- if (s && e.sessionId === s.id) void this.refreshSession(s.id);
181
- if (!this.opts.sessionId) void this.refreshInbox();
181
+ const session = this.snapshot.session;
182
+ if (session && event.sessionId === session.id) void this.refreshSession(session.id);
183
+ if (!this.options.sessionId) void this.refreshInbox();
182
184
  });
183
185
  await client.subscribe();
184
- if (this.opts.sessionId) {
185
- this.update({ session: await client.sessionGet(this.opts.sessionId) });
186
+ if (this.options.sessionId) {
187
+ this.update({ session: await client.sessionGet(this.options.sessionId) });
186
188
  } else {
187
189
  this.update({ inbox: await client.sessionList({ status: "pending" }) });
188
190
  }
@@ -203,28 +205,28 @@ class Controller implements ReviewController {
203
205
  this.countdown = undefined;
204
206
  }
205
207
 
206
- applyConfig(cfg: CueloopConfig): void {
207
- this.autoClose = cfg.ui.autoClose;
208
- this.obsidian = cfg.integrations.obsidian;
208
+ applyConfig(config: CueloopConfig): void {
209
+ this.autoClose = config.ui.autoClose;
210
+ this.obsidian = config.integrations.obsidian;
209
211
  // bundled integrations register through the public extension API
210
212
  const registry = new Registry();
211
- void registry.load("obsidian", createObsidianExtension(cfg.integrations.obsidian)).then((rec) => {
212
- this.exporters = rec.exporters;
213
+ void registry.load("obsidian", createObsidianExtension(config.integrations.obsidian)).then((record) => {
214
+ this.exporters = record.exporters;
213
215
  });
214
216
  }
215
217
 
216
- setStatus(msg: string): void {
217
- this.update({ status: msg });
218
+ setStatus(message: string): void {
219
+ this.update({ status: message });
218
220
  }
219
221
 
220
222
  // ── derived projections ─────────────────────
221
223
  private ensureDerived(): void {
222
- const s = this.snap.session;
223
- if (this.derivedFor === s) return;
224
- this.derivedFor = s;
225
- const rows = s && s.artifact.type === "diff" ? diffRows(s.artifact.content) : [];
224
+ const session = this.snapshot.session;
225
+ if (this.derivedFor === session) return;
226
+ this.derivedFor = session;
227
+ const rows = session && session.artifact.type === "diff" ? diffRows(session.artifact.content) : [];
226
228
  this.derived = {
227
- display: s ? buildDisplay(s.artifact.content, s.workingCopy) : [],
229
+ display: session ? buildDisplay(session.artifact.content, session.workingCopy) : [],
228
230
  rows,
229
231
  files: walkFiles(rows),
230
232
  };
@@ -246,8 +248,8 @@ class Controller implements ReviewController {
246
248
  }
247
249
 
248
250
  working(): string {
249
- const s = this.snap.session;
250
- return s ? (s.workingCopy ?? s.artifact.content) : "";
251
+ const session = this.snapshot.session;
252
+ return session ? (session.workingCopy ?? session.artifact.content) : "";
251
253
  }
252
254
 
253
255
  // Refreshes race the connection teardown: an event can arrive while close()
@@ -270,41 +272,41 @@ class Controller implements ReviewController {
270
272
  }
271
273
 
272
274
  /** Optimistic apply: the daemon response is the next session snapshot. */
273
- private apply(p: Promise<ReviewSession>): void {
274
- p.then((session) => this.update({ session })).catch((e: unknown) =>
275
- this.setStatus(String(e instanceof Error ? e.message : e)),
275
+ private apply(mutation: Promise<ReviewSession>): void {
276
+ mutation.then((session) => this.update({ session })).catch((error: unknown) =>
277
+ this.setStatus(String(error instanceof Error ? error.message : error)),
276
278
  );
277
279
  }
278
280
 
279
281
  // ── verbs ───────────────────────────────────
280
282
  open(id: string): void {
281
283
  this.locallyViewed.clear();
282
- const cached = this.snap.inbox?.find((s) => s.id === id);
284
+ const cached = this.snapshot.inbox?.find((candidate) => candidate.id === id);
283
285
  if (cached) this.update({ session: cached });
284
286
  else void this.refreshSession(id);
285
287
  }
286
288
 
287
- cut(dispIdx: number): void {
288
- const session = this.snap.session;
289
+ cut(displayIndex: number): void {
290
+ const session = this.snapshot.session;
289
291
  if (!session || session.status === "resolved") return;
290
- const d = this.display()[dispIdx];
291
- if (!d) return;
292
+ const block = this.display()[displayIndex];
293
+ if (!block) return;
292
294
  const working = this.working();
293
- if (d.type === "del") {
294
- const line = restoreLine(nextWorkBlock(this.display(), dispIdx), working.split("\n").length);
295
+ if (block.type === "del") {
296
+ const line = restoreLine(nextWorkBlock(this.display(), displayIndex), working.split("\n").length);
295
297
  // restoreBlock returns undefined when the block structure round-trips
296
298
  // to the submitted revision - the working copy is gone
297
- const restored = restoreBlock(session.artifact.content, working, d.base!, line);
299
+ const restored = restoreBlock(session.artifact.content, working, block.base!, line);
298
300
  this.setWorkingCopy(restored);
299
301
  this.setStatus("cut restored");
300
- } else if (d.work) {
301
- this.setWorkingCopy(cutBlock(working, d.work));
302
+ } else if (block.work) {
303
+ this.setWorkingCopy(cutBlock(working, block.work));
302
304
  this.setStatus("block cut - it serializes into the diff");
303
305
  }
304
306
  }
305
307
 
306
308
  edit(): void {
307
- const session = this.snap.session;
309
+ const session = this.snapshot.session;
308
310
  if (!session || session.status === "resolved") return;
309
311
  try {
310
312
  const result = editInEditor(this.working(), "plan.md");
@@ -334,21 +336,21 @@ class Controller implements ReviewController {
334
336
 
335
337
  annotate(
336
338
  kind: "comment" | "suggestion",
337
- dispIdx: number,
339
+ displayIndex: number,
338
340
  start: number,
339
341
  end: number,
340
342
  body: string,
341
343
  ): string | undefined {
342
- const session = this.snap.session;
344
+ const session = this.snapshot.session;
343
345
  if (!session) return undefined;
344
346
  let anchor;
345
347
  if (session.artifact.type === "diff") {
346
- anchor = { ...diffRowAnchor(this.rows(), dispIdx), blockIndex: dispIdx };
348
+ anchor = { ...diffRowAnchor(this.rows(), displayIndex), blockIndex: displayIndex };
347
349
  } else {
348
350
  const display = this.display();
349
- const workBlocks = display.filter((d) => d.work).map((d) => d.work!);
350
- const workIdx = display.slice(0, dispIdx + 1).filter((d) => d.work).length - 1;
351
- anchor = makeAnchor(workBlocks, workIdx, start, end);
351
+ const workBlocks = display.filter((entry) => entry.work).map((entry) => entry.work!);
352
+ const workBlockIndex = display.slice(0, displayIndex + 1).filter((entry) => entry.work).length - 1;
353
+ anchor = makeAnchor(workBlocks, workBlockIndex, start, end);
352
354
  }
353
355
  const annotationId = newAnnotationId();
354
356
  this.apply(
@@ -364,7 +366,7 @@ class Controller implements ReviewController {
364
366
  }
365
367
 
366
368
  updateAnnotation(id: string, body: string): void {
367
- const session = this.snap.session;
369
+ const session = this.snapshot.session;
368
370
  if (!session) return;
369
371
  const existing = session.annotations.find((annotation) => annotation.id === id);
370
372
  if (!existing) return;
@@ -381,14 +383,14 @@ class Controller implements ReviewController {
381
383
  }
382
384
 
383
385
  removeAnnotation(id: string): void {
384
- const session = this.snap.session;
386
+ const session = this.snapshot.session;
385
387
  if (!session) return;
386
388
  this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
387
389
  this.setStatus("annotation deleted");
388
390
  }
389
391
 
390
392
  setWorkingCopy(content: string | undefined): void {
391
- const session = this.snap.session;
393
+ const session = this.snapshot.session;
392
394
  if (!session) return;
393
395
  this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
394
396
  }
@@ -399,11 +401,11 @@ class Controller implements ReviewController {
399
401
  private locallyViewed = new Set<string>();
400
402
 
401
403
  private viewedSet(): Set<string> {
402
- return new Set([...(this.snap.session?.viewedPaths ?? []), ...this.locallyViewed]);
404
+ return new Set([...(this.snapshot.session?.viewedPaths ?? []), ...this.locallyViewed]);
403
405
  }
404
406
 
405
407
  walkStart(): void {
406
- const session = this.snap.session;
408
+ const session = this.snapshot.session;
407
409
  if (!session || session.artifact.type !== "diff") return;
408
410
  const files = this.files();
409
411
  if (files.length === 0) return this.setStatus("nothing to walk - the diff is empty");
@@ -412,8 +414,8 @@ class Controller implements ReviewController {
412
414
  }
413
415
 
414
416
  walkForward(): void {
415
- const walk = this.snap.walk;
416
- const session = this.snap.session;
417
+ const walk = this.snapshot.walk;
418
+ const session = this.snapshot.session;
417
419
  if (!walk || !session) return;
418
420
  const files = this.files();
419
421
  const current = files[walk.index];
@@ -428,17 +430,17 @@ class Controller implements ReviewController {
428
430
  }
429
431
 
430
432
  walkBack(): void {
431
- const walk = this.snap.walk;
433
+ const walk = this.snapshot.walk;
432
434
  if (!walk) return;
433
435
  this.update({ walk: { index: Math.max(0, walk.index - 1) } });
434
436
  }
435
437
 
436
438
  walkLeave(): void {
437
- if (this.snap.walk) this.update({ walk: null });
439
+ if (this.snapshot.walk) this.update({ walk: null });
438
440
  }
439
441
 
440
442
  submit(verdict: VerdictKind, summary: string): void {
441
- const session = this.snap.session;
443
+ const session = this.snapshot.session;
442
444
  if (!session) return;
443
445
  this.walkLeave();
444
446
  this.client!.sessionResolve(session.id, verdict, summary)
@@ -450,8 +452,8 @@ class Controller implements ReviewController {
450
452
  const obsidian = this.obsidian;
451
453
  const exporter = this.exporters.get("obsidian");
452
454
  if (obsidian && exporter && shouldExport(obsidian.exportOn, verdict)) {
453
- void exporter(resolved).then((r) => {
454
- this.setStatus(r.success && r.path ? `exported to ${r.path}` : `export failed: ${r.error ?? "unknown"}`);
455
+ void exporter(resolved).then((exportResult) => {
456
+ this.setStatus(exportResult.success && exportResult.path ? `exported to ${exportResult.path}` : `export failed: ${exportResult.error ?? "unknown"}`);
455
457
  });
456
458
  }
457
459
  // Hand the reviewer back to the agent. The default is a visible
@@ -462,7 +464,7 @@ class Controller implements ReviewController {
462
464
  else if (typeof delay === "number") this.startCounting(delay);
463
465
  else this.startCounting(DEFAULT_AUTO_CLOSE);
464
466
  })
465
- .catch((e: unknown) => this.setStatus(String(e instanceof Error ? e.message : e)));
467
+ .catch((error: unknown) => this.setStatus(String(error instanceof Error ? error.message : error)));
466
468
  }
467
469
 
468
470
  // ── completion hand-back ────────────────────
@@ -474,9 +476,10 @@ class Controller implements ReviewController {
474
476
 
475
477
  finishReview(): void {
476
478
  this.clearCountdown();
477
- const pane = returnPaneFor(this.snap.session?.artifact.meta.herdrPane);
478
- if (pane) focusHerdrPane(pane);
479
- this.opts.onExit?.(0);
479
+ const herdr = detectHerdr();
480
+ const pane = returnPaneFor(this.snapshot.session?.artifact.meta.herdrPane);
481
+ if (herdr && pane) focusHerdrPane(herdr.binPath, pane);
482
+ this.options.onExit?.(0);
480
483
  }
481
484
 
482
485
  dismissCompletion(): void {
package/src/theme.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Semantic theme tokens (#22). Values carry over from the design-system
2
+ * Semantic theme tokens. Values carry over from the design-system
3
3
  * prototypes (tokens.css). One built-in dark theme for now; the theme
4
4
  * registry (palette-derived themes, terminal theme, user overrides) layers
5
5
  * on top of these slots without touching consumers.
@@ -22,22 +22,22 @@ index 111..222 100644
22
22
  describe("diffRows", () => {
23
23
  test("flattens files, hunks, and signed lines with line numbers", () => {
24
24
  const rows = diffRows(PATCH);
25
- expect(rows[0]).toMatchObject({ t: "file", file: "src/store.ts" });
26
- expect(rows[1]!.t).toBe("hunk");
27
- const del = rows.find((r) => r.t === "del")!;
25
+ expect(rows[0]).toMatchObject({ kind: "file", file: "src/store.ts" });
26
+ expect(rows[1]!.kind).toBe("hunk");
27
+ const del = rows.find((row) => row.kind === "del")!;
28
28
  expect(del.text).toContain("private items = [];");
29
29
  expect(del.oldLine).toBe(3);
30
- const adds = rows.filter((r) => r.t === "add");
30
+ const adds = rows.filter((row) => row.kind === "add");
31
31
  expect(adds[0]!.text).toContain("new Map()");
32
32
  expect(adds[0]!.newLine).toBe(3);
33
33
  expect(adds[1]!.newLine).toBe(4);
34
34
  // two hunks
35
- expect(rows.filter((r) => r.t === "hunk").length).toBe(2);
35
+ expect(rows.filter((row) => row.kind === "hunk").length).toBe(2);
36
36
  });
37
37
 
38
38
  test("context lines carry both line numbers", () => {
39
39
  const rows = diffRows(PATCH);
40
- const ctx = rows.find((r) => r.t === "ctx")!;
40
+ const ctx = rows.find((row) => row.kind === "ctx")!;
41
41
  expect(ctx.oldLine).toBe(1);
42
42
  expect(ctx.newLine).toBe(1);
43
43
  });
@@ -46,8 +46,8 @@ describe("diffRows", () => {
46
46
  describe("diffRowAnchor", () => {
47
47
  test("quote is the line, neighbors are the context selectors", () => {
48
48
  const rows = diffRows(PATCH);
49
- const idx = rows.findIndex((r) => r.text.includes("new Map()"));
50
- const anchor = diffRowAnchor(rows, idx);
49
+ const rowIndex = rows.findIndex((row) => row.text.includes("new Map()"));
50
+ const anchor = diffRowAnchor(rows, rowIndex);
51
51
  expect(anchor.quote).toContain("new Map()");
52
52
  expect(anchor.prefix.length).toBeGreaterThan(0);
53
53
  expect(anchor.suffix.length).toBeGreaterThan(0);
package/src/view-diff.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Diff artifact projection (#18 slice 3): flatten @pierre/diffs' parsed
2
+ * Diff artifact projection: flatten @pierre/diffs' parsed
3
3
  * patch model into render rows. Line-anchored annotations use the same
4
4
  * quote-primary anchors as plans: quote = the line content, prefix/suffix =
5
5
  * the neighbor lines - so the whole anchor/feedback pipeline is shared.
@@ -7,10 +7,10 @@
7
7
 
8
8
  import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
9
9
 
10
- export type DiffRowType = "file" | "hunk" | "ctx" | "add" | "del";
10
+ export type DiffRowKind = "file" | "hunk" | "ctx" | "add" | "del";
11
11
 
12
12
  export interface DiffRow {
13
- t: DiffRowType;
13
+ kind: DiffRowKind;
14
14
  text: string;
15
15
  file: string;
16
16
  oldLine?: number;
@@ -22,35 +22,35 @@ export function diffRows(patchText: string): DiffRow[] {
22
22
  const patches = parsePatchFiles(patchText);
23
23
  for (const patch of patches) {
24
24
  for (const file of patch.files) {
25
- rows.push({ t: "file", text: fileLabel(file), file: file.name });
25
+ rows.push({ kind: "file", text: fileLabel(file), file: file.name });
26
26
  for (const hunk of file.hunks) {
27
- rows.push({ t: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
27
+ rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
28
28
  let oldLine = hunk.deletionStart;
29
29
  let newLine = hunk.additionStart;
30
- for (const seg of hunk.hunkContent) {
31
- if (seg.type === "context") {
32
- for (let i = 0; i < seg.lines; i++) {
30
+ for (const segment of hunk.hunkContent) {
31
+ if (segment.type === "context") {
32
+ for (let i = 0; i < segment.lines; i++) {
33
33
  rows.push({
34
- t: "ctx",
35
- text: file.additionLines[seg.additionLineIndex + i] ?? "",
34
+ kind: "ctx",
35
+ text: file.additionLines[segment.additionLineIndex + i] ?? "",
36
36
  file: file.name,
37
37
  oldLine: oldLine++,
38
38
  newLine: newLine++,
39
39
  });
40
40
  }
41
41
  } else {
42
- for (let i = 0; i < seg.deletions; i++) {
42
+ for (let i = 0; i < segment.deletions; i++) {
43
43
  rows.push({
44
- t: "del",
45
- text: file.deletionLines[seg.deletionLineIndex + i] ?? "",
44
+ kind: "del",
45
+ text: file.deletionLines[segment.deletionLineIndex + i] ?? "",
46
46
  file: file.name,
47
47
  oldLine: oldLine++,
48
48
  });
49
49
  }
50
- for (let i = 0; i < seg.additions; i++) {
50
+ for (let i = 0; i < segment.additions; i++) {
51
51
  rows.push({
52
- t: "add",
53
- text: file.additionLines[seg.additionLineIndex + i] ?? "",
52
+ kind: "add",
53
+ text: file.additionLines[segment.additionLineIndex + i] ?? "",
54
54
  file: file.name,
55
55
  newLine: newLine++,
56
56
  });
@@ -68,14 +68,14 @@ function fileLabel(file: FileDiffMetadata): string {
68
68
  }
69
69
 
70
70
  /** Quote-primary anchor for a diff row: neighbors as context selectors. */
71
- export function diffRowAnchor(rows: DiffRow[], idx: number): { quote: string; prefix: string; suffix: string } {
72
- const row = rows[idx]!;
73
- const prev = rows[idx - 1];
74
- const next = rows[idx + 1];
71
+ export function diffRowAnchor(rows: DiffRow[], rowIndex: number): { quote: string; prefix: string; suffix: string } {
72
+ const row = rows[rowIndex]!;
73
+ const prev = rows[rowIndex - 1];
74
+ const next = rows[rowIndex + 1];
75
75
  return {
76
76
  quote: row.text,
77
- prefix: prev && (prev.t === "ctx" || prev.t === "add" || prev.t === "del") ? prev.text.slice(-24) : "",
78
- suffix: next && (next.t === "ctx" || next.t === "add" || next.t === "del") ? next.text.slice(0, 24) : "",
77
+ prefix: prev && (prev.kind === "ctx" || prev.kind === "add" || prev.kind === "del") ? prev.text.slice(-24) : "",
78
+ suffix: next && (next.kind === "ctx" || next.kind === "add" || next.kind === "del") ? next.text.slice(0, 24) : "",
79
79
  };
80
80
  }
81
81