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

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
@@ -22,7 +22,7 @@ import {
22
22
  } from "@cueloop/schema";
23
23
  import { Registry, type Exporter } from "@cueloop/extension-api";
24
24
  import { createObsidianExtension, shouldExport, type ObsidianConfig } from "@cueloop/integration-obsidian";
25
- import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view";
25
+ import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view-plan";
26
26
  import { diffRowAnchor, diffRows, type DiffRow } from "./view-diff";
27
27
  import { firstUnviewedIndex, walkFiles, type WalkFile } from "./walk";
28
28
  import { editInEditor } from "./editor";
@@ -81,8 +81,8 @@ export interface ReviewController {
81
81
  connect(): void;
82
82
  close(): void;
83
83
  /** Loaded config parts the controller acts on: auto-close and exporters. */
84
- applyConfig(cfg: CueloopConfig): void;
85
- setStatus(msg: string): void;
84
+ applyConfig(config: CueloopConfig): void;
85
+ setStatus(message: string): void;
86
86
  /** Derived projections, cached per session identity. */
87
87
  display(): DisplayBlock[];
88
88
  rows(): DiffRow[];
@@ -92,14 +92,14 @@ export interface ReviewController {
92
92
  /** Open a session from the inbox. */
93
93
  open(id: string): void;
94
94
  /** Cut the block under the cursor, or restore a cut one. */
95
- cut(dispIdx: number): void;
95
+ cut(displayIndex: number): void;
96
96
  /** The $EDITOR hand-off on the working copy. */
97
97
  edit(): void;
98
98
  /**
99
99
  * Anchor and store an annotation; both plan and diff anchor constructions.
100
100
  * Returns the minted annotation id so the view can select the new card.
101
101
  */
102
- annotate(kind: "comment" | "suggestion", dispIdx: number, start: number, end: number, body: string): string | undefined;
102
+ annotate(kind: "comment" | "suggestion", displayIndex: number, start: number, end: number, body: string): string | undefined;
103
103
  /** Rewrite a stored annotation's body in place (the rail-card edit). */
104
104
  updateAnnotation(id: string, body: string): void;
105
105
  removeAnnotation(id: string): void;
@@ -120,15 +120,15 @@ export interface ReviewController {
120
120
  optInAutoClose(): void;
121
121
  }
122
122
 
123
- export function createReviewController(opts: ReviewControllerOptions): ReviewController {
124
- return new Controller(opts);
123
+ export function createReviewController(options: ReviewControllerOptions): ReviewController {
124
+ return new Controller(options);
125
125
  }
126
126
 
127
127
  class Controller implements ReviewController {
128
128
  readonly readOnly: boolean;
129
129
  private client: DaemonClient | null = null;
130
130
  private closed = false;
131
- private snap: ControllerSnapshot = {
131
+ private snapshot: ControllerSnapshot = {
132
132
  session: null,
133
133
  inbox: null,
134
134
  status: "",
@@ -151,9 +151,9 @@ class Controller implements ReviewController {
151
151
  files: [],
152
152
  };
153
153
 
154
- constructor(private readonly opts: ReviewControllerOptions) {
155
- this.readOnly = opts.readOnly ?? false;
156
- this.clock = opts.clock ?? new SystemClock();
154
+ constructor(private readonly options: ReviewControllerOptions) {
155
+ this.readOnly = options.readOnly ?? false;
156
+ this.clock = options.clock ?? new SystemClock();
157
157
  }
158
158
 
159
159
  subscribe = (listener: () => void): (() => void) => {
@@ -161,28 +161,28 @@ class Controller implements ReviewController {
161
161
  return () => this.listeners.delete(listener);
162
162
  };
163
163
 
164
- getSnapshot = (): ControllerSnapshot => this.snap;
164
+ getSnapshot = (): ControllerSnapshot => this.snapshot;
165
165
 
166
166
  private update(patch: Partial<ControllerSnapshot>): void {
167
- this.snap = { ...this.snap, ...patch };
168
- for (const l of this.listeners) l();
167
+ this.snapshot = { ...this.snapshot, ...patch };
168
+ for (const listener of this.listeners) listener();
169
169
  }
170
170
 
171
171
  connect(): void {
172
172
  void (async () => {
173
173
  try {
174
- const client = await DaemonClient.connect({ home: this.opts.home, autostart: true });
174
+ const client = await DaemonClient.connect({ home: this.options.home, autostart: true });
175
175
  if (this.closed) return void client.close();
176
176
  this.client = client;
177
- client.onEvent((e) => {
177
+ client.onEvent((event) => {
178
178
  // 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();
179
+ const session = this.snapshot.session;
180
+ if (session && event.sessionId === session.id) void this.refreshSession(session.id);
181
+ if (!this.options.sessionId) void this.refreshInbox();
182
182
  });
183
183
  await client.subscribe();
184
- if (this.opts.sessionId) {
185
- this.update({ session: await client.sessionGet(this.opts.sessionId) });
184
+ if (this.options.sessionId) {
185
+ this.update({ session: await client.sessionGet(this.options.sessionId) });
186
186
  } else {
187
187
  this.update({ inbox: await client.sessionList({ status: "pending" }) });
188
188
  }
@@ -203,28 +203,28 @@ class Controller implements ReviewController {
203
203
  this.countdown = undefined;
204
204
  }
205
205
 
206
- applyConfig(cfg: CueloopConfig): void {
207
- this.autoClose = cfg.ui.autoClose;
208
- this.obsidian = cfg.integrations.obsidian;
206
+ applyConfig(config: CueloopConfig): void {
207
+ this.autoClose = config.ui.autoClose;
208
+ this.obsidian = config.integrations.obsidian;
209
209
  // bundled integrations register through the public extension API
210
210
  const registry = new Registry();
211
- void registry.load("obsidian", createObsidianExtension(cfg.integrations.obsidian)).then((rec) => {
212
- this.exporters = rec.exporters;
211
+ void registry.load("obsidian", createObsidianExtension(config.integrations.obsidian)).then((record) => {
212
+ this.exporters = record.exporters;
213
213
  });
214
214
  }
215
215
 
216
- setStatus(msg: string): void {
217
- this.update({ status: msg });
216
+ setStatus(message: string): void {
217
+ this.update({ status: message });
218
218
  }
219
219
 
220
220
  // ── derived projections ─────────────────────
221
221
  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) : [];
222
+ const session = this.snapshot.session;
223
+ if (this.derivedFor === session) return;
224
+ this.derivedFor = session;
225
+ const rows = session && session.artifact.type === "diff" ? diffRows(session.artifact.content) : [];
226
226
  this.derived = {
227
- display: s ? buildDisplay(s.artifact.content, s.workingCopy) : [],
227
+ display: session ? buildDisplay(session.artifact.content, session.workingCopy) : [],
228
228
  rows,
229
229
  files: walkFiles(rows),
230
230
  };
@@ -246,8 +246,8 @@ class Controller implements ReviewController {
246
246
  }
247
247
 
248
248
  working(): string {
249
- const s = this.snap.session;
250
- return s ? (s.workingCopy ?? s.artifact.content) : "";
249
+ const session = this.snapshot.session;
250
+ return session ? (session.workingCopy ?? session.artifact.content) : "";
251
251
  }
252
252
 
253
253
  // Refreshes race the connection teardown: an event can arrive while close()
@@ -270,41 +270,41 @@ class Controller implements ReviewController {
270
270
  }
271
271
 
272
272
  /** 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)),
273
+ private apply(mutation: Promise<ReviewSession>): void {
274
+ mutation.then((session) => this.update({ session })).catch((error: unknown) =>
275
+ this.setStatus(String(error instanceof Error ? error.message : error)),
276
276
  );
277
277
  }
278
278
 
279
279
  // ── verbs ───────────────────────────────────
280
280
  open(id: string): void {
281
281
  this.locallyViewed.clear();
282
- const cached = this.snap.inbox?.find((s) => s.id === id);
282
+ const cached = this.snapshot.inbox?.find((candidate) => candidate.id === id);
283
283
  if (cached) this.update({ session: cached });
284
284
  else void this.refreshSession(id);
285
285
  }
286
286
 
287
- cut(dispIdx: number): void {
288
- const session = this.snap.session;
287
+ cut(displayIndex: number): void {
288
+ const session = this.snapshot.session;
289
289
  if (!session || session.status === "resolved") return;
290
- const d = this.display()[dispIdx];
291
- if (!d) return;
290
+ const block = this.display()[displayIndex];
291
+ if (!block) return;
292
292
  const working = this.working();
293
- if (d.type === "del") {
294
- const line = restoreLine(nextWorkBlock(this.display(), dispIdx), working.split("\n").length);
293
+ if (block.type === "del") {
294
+ const line = restoreLine(nextWorkBlock(this.display(), displayIndex), working.split("\n").length);
295
295
  // restoreBlock returns undefined when the block structure round-trips
296
296
  // to the submitted revision - the working copy is gone
297
- const restored = restoreBlock(session.artifact.content, working, d.base!, line);
297
+ const restored = restoreBlock(session.artifact.content, working, block.base!, line);
298
298
  this.setWorkingCopy(restored);
299
299
  this.setStatus("cut restored");
300
- } else if (d.work) {
301
- this.setWorkingCopy(cutBlock(working, d.work));
300
+ } else if (block.work) {
301
+ this.setWorkingCopy(cutBlock(working, block.work));
302
302
  this.setStatus("block cut - it serializes into the diff");
303
303
  }
304
304
  }
305
305
 
306
306
  edit(): void {
307
- const session = this.snap.session;
307
+ const session = this.snapshot.session;
308
308
  if (!session || session.status === "resolved") return;
309
309
  try {
310
310
  const result = editInEditor(this.working(), "plan.md");
@@ -334,21 +334,21 @@ class Controller implements ReviewController {
334
334
 
335
335
  annotate(
336
336
  kind: "comment" | "suggestion",
337
- dispIdx: number,
337
+ displayIndex: number,
338
338
  start: number,
339
339
  end: number,
340
340
  body: string,
341
341
  ): string | undefined {
342
- const session = this.snap.session;
342
+ const session = this.snapshot.session;
343
343
  if (!session) return undefined;
344
344
  let anchor;
345
345
  if (session.artifact.type === "diff") {
346
- anchor = { ...diffRowAnchor(this.rows(), dispIdx), blockIndex: dispIdx };
346
+ anchor = { ...diffRowAnchor(this.rows(), displayIndex), blockIndex: displayIndex };
347
347
  } else {
348
348
  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);
349
+ const workBlocks = display.filter((entry) => entry.work).map((entry) => entry.work!);
350
+ const workBlockIndex = display.slice(0, displayIndex + 1).filter((entry) => entry.work).length - 1;
351
+ anchor = makeAnchor(workBlocks, workBlockIndex, start, end);
352
352
  }
353
353
  const annotationId = newAnnotationId();
354
354
  this.apply(
@@ -364,7 +364,7 @@ class Controller implements ReviewController {
364
364
  }
365
365
 
366
366
  updateAnnotation(id: string, body: string): void {
367
- const session = this.snap.session;
367
+ const session = this.snapshot.session;
368
368
  if (!session) return;
369
369
  const existing = session.annotations.find((annotation) => annotation.id === id);
370
370
  if (!existing) return;
@@ -381,14 +381,14 @@ class Controller implements ReviewController {
381
381
  }
382
382
 
383
383
  removeAnnotation(id: string): void {
384
- const session = this.snap.session;
384
+ const session = this.snapshot.session;
385
385
  if (!session) return;
386
386
  this.apply(this.client!.sessionRemoveAnnotation(session.id, id));
387
387
  this.setStatus("annotation deleted");
388
388
  }
389
389
 
390
390
  setWorkingCopy(content: string | undefined): void {
391
- const session = this.snap.session;
391
+ const session = this.snapshot.session;
392
392
  if (!session) return;
393
393
  this.apply(this.client!.sessionSetWorkingCopy(session.id, content));
394
394
  }
@@ -399,11 +399,11 @@ class Controller implements ReviewController {
399
399
  private locallyViewed = new Set<string>();
400
400
 
401
401
  private viewedSet(): Set<string> {
402
- return new Set([...(this.snap.session?.viewedPaths ?? []), ...this.locallyViewed]);
402
+ return new Set([...(this.snapshot.session?.viewedPaths ?? []), ...this.locallyViewed]);
403
403
  }
404
404
 
405
405
  walkStart(): void {
406
- const session = this.snap.session;
406
+ const session = this.snapshot.session;
407
407
  if (!session || session.artifact.type !== "diff") return;
408
408
  const files = this.files();
409
409
  if (files.length === 0) return this.setStatus("nothing to walk - the diff is empty");
@@ -412,8 +412,8 @@ class Controller implements ReviewController {
412
412
  }
413
413
 
414
414
  walkForward(): void {
415
- const walk = this.snap.walk;
416
- const session = this.snap.session;
415
+ const walk = this.snapshot.walk;
416
+ const session = this.snapshot.session;
417
417
  if (!walk || !session) return;
418
418
  const files = this.files();
419
419
  const current = files[walk.index];
@@ -428,17 +428,17 @@ class Controller implements ReviewController {
428
428
  }
429
429
 
430
430
  walkBack(): void {
431
- const walk = this.snap.walk;
431
+ const walk = this.snapshot.walk;
432
432
  if (!walk) return;
433
433
  this.update({ walk: { index: Math.max(0, walk.index - 1) } });
434
434
  }
435
435
 
436
436
  walkLeave(): void {
437
- if (this.snap.walk) this.update({ walk: null });
437
+ if (this.snapshot.walk) this.update({ walk: null });
438
438
  }
439
439
 
440
440
  submit(verdict: VerdictKind, summary: string): void {
441
- const session = this.snap.session;
441
+ const session = this.snapshot.session;
442
442
  if (!session) return;
443
443
  this.walkLeave();
444
444
  this.client!.sessionResolve(session.id, verdict, summary)
@@ -450,8 +450,8 @@ class Controller implements ReviewController {
450
450
  const obsidian = this.obsidian;
451
451
  const exporter = this.exporters.get("obsidian");
452
452
  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"}`);
453
+ void exporter(resolved).then((exportResult) => {
454
+ this.setStatus(exportResult.success && exportResult.path ? `exported to ${exportResult.path}` : `export failed: ${exportResult.error ?? "unknown"}`);
455
455
  });
456
456
  }
457
457
  // Hand the reviewer back to the agent. The default is a visible
@@ -462,7 +462,7 @@ class Controller implements ReviewController {
462
462
  else if (typeof delay === "number") this.startCounting(delay);
463
463
  else this.startCounting(DEFAULT_AUTO_CLOSE);
464
464
  })
465
- .catch((e: unknown) => this.setStatus(String(e instanceof Error ? e.message : e)));
465
+ .catch((error: unknown) => this.setStatus(String(error instanceof Error ? error.message : error)));
466
466
  }
467
467
 
468
468
  // ── completion hand-back ────────────────────
@@ -474,9 +474,9 @@ class Controller implements ReviewController {
474
474
 
475
475
  finishReview(): void {
476
476
  this.clearCountdown();
477
- const pane = returnPaneFor(this.snap.session?.artifact.meta.herdrPane);
477
+ const pane = returnPaneFor(this.snapshot.session?.artifact.meta.herdrPane);
478
478
  if (pane) focusHerdrPane(pane);
479
- this.opts.onExit?.(0);
479
+ this.options.onExit?.(0);
480
480
  }
481
481
 
482
482
  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