@cueloop/client 0.1.0-alpha.59 → 0.1.0-alpha.60

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.
Files changed (98) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +7 -0
  3. package/src/App.diff.test.tsx +11 -0
  4. package/src/App.export.test.tsx +5 -0
  5. package/src/App.plan-review.test.tsx +42 -0
  6. package/src/App.prototype.test.tsx +11 -0
  7. package/src/App.readonly.test.tsx +10 -0
  8. package/src/App.rendering.test.tsx +4 -0
  9. package/src/App.review-panel.test.tsx +7 -0
  10. package/src/App.submit-confirm.test.tsx +11 -0
  11. package/src/App.test.tsx +13 -0
  12. package/src/App.toast.test.tsx +2 -0
  13. package/src/App.tsx +27 -0
  14. package/src/App.walk.test.tsx +10 -0
  15. package/src/app-key-state.ts +1 -0
  16. package/src/app-screens.tsx +4 -0
  17. package/src/app-view-model.ts +21 -0
  18. package/src/attribution.ts +4 -0
  19. package/src/clipboard.ts +3 -0
  20. package/src/components/AnnotationCard.tsx +9 -0
  21. package/src/components/Breadcrumb.tsx +1 -0
  22. package/src/components/CodeBlock.tsx +1 -0
  23. package/src/components/CompletionOverlay.tsx +1 -0
  24. package/src/components/ConfirmCard.tsx +2 -0
  25. package/src/components/ConfirmDialog.tsx +2 -0
  26. package/src/components/DiffSheet.scroll.test.tsx +12 -0
  27. package/src/components/DiffSheet.syntax.test.tsx +3 -0
  28. package/src/components/DiffSheet.tsx +19 -0
  29. package/src/components/InboxList.tsx +2 -0
  30. package/src/components/KeybindsDialog.tsx +1 -0
  31. package/src/components/MarkerPopover.tsx +1 -0
  32. package/src/components/MenuBar.tsx +1 -0
  33. package/src/components/PlanSheet.tsx +26 -0
  34. package/src/components/PromptDialog.tsx +2 -0
  35. package/src/components/PrototypeSheet.tsx +27 -0
  36. package/src/components/ReviewPanel.tsx +2 -0
  37. package/src/components/ReviewRail.tsx +1 -0
  38. package/src/components/SettingsDialog.tsx +6 -0
  39. package/src/components/SettingsRows.tsx +4 -0
  40. package/src/components/Toast.tsx +1 -0
  41. package/src/components/agent-launcher.test.tsx +1 -0
  42. package/src/components/agent-launcher.tsx +2 -0
  43. package/src/components/diff-sheet-layout.test.ts +1 -0
  44. package/src/components/diff-sheet-layout.ts +17 -0
  45. package/src/components/plan-sheet-run-style.ts +4 -0
  46. package/src/components/primitives/Button.tsx +1 -0
  47. package/src/components/primitives/Card.tsx +1 -0
  48. package/src/components/primitives/Dialog.tsx +2 -0
  49. package/src/components/primitives/StatusBar.tsx +1 -0
  50. package/src/components/primitives/Tabs.tsx +4 -0
  51. package/src/components/quick-actions-editor.tsx +2 -0
  52. package/src/components/stories-app.tsx +1 -0
  53. package/src/components/stories.test.tsx +4 -0
  54. package/src/components/story.ts +6 -0
  55. package/src/components/syntax-highlight.ts +10 -0
  56. package/src/components/terminal-pane.ts +12 -0
  57. package/src/components/theme-context.tsx +1 -0
  58. package/src/components/truncate-text.ts +1 -0
  59. package/src/config.test.ts +18 -0
  60. package/src/config.ts +27 -0
  61. package/src/diff-hunk-curate.ts +20 -0
  62. package/src/diff-intraline.test.ts +1 -0
  63. package/src/diff-intraline.ts +14 -0
  64. package/src/diff-syntax.ts +22 -0
  65. package/src/editor.test.ts +6 -0
  66. package/src/editor.ts +12 -0
  67. package/src/ghostty-terminal.test.ts +2 -0
  68. package/src/ghostty-terminal.ts +8 -0
  69. package/src/herdr.test.ts +2 -0
  70. package/src/herdr.ts +3 -0
  71. package/src/integrations.ts +2 -0
  72. package/src/intent-dispatch.test.ts +6 -0
  73. package/src/intent-dispatch.ts +37 -0
  74. package/src/key-bindings.test.ts +4 -0
  75. package/src/key-bindings.ts +22 -0
  76. package/src/keymap.test.ts +13 -0
  77. package/src/keymap.ts +43 -0
  78. package/src/kitty-image.ts +9 -0
  79. package/src/prototype-browser.test.ts +1 -0
  80. package/src/prototype-browser.ts +22 -0
  81. package/src/pty.test.ts +3 -0
  82. package/src/pty.ts +15 -0
  83. package/src/run.ts +1 -0
  84. package/src/serve.test.ts +1 -0
  85. package/src/serve.ts +3 -0
  86. package/src/session-controller.curate.test.ts +7 -0
  87. package/src/session-controller.share.test.ts +11 -0
  88. package/src/session-controller.ts +79 -0
  89. package/src/share.ts +7 -0
  90. package/src/test-support.test.ts +5 -0
  91. package/src/test-support.ts +10 -0
  92. package/src/theme-presets.ts +1 -0
  93. package/src/use-settings-dialog.tsx +12 -0
  94. package/src/view-diff.test.ts +3 -0
  95. package/src/view-diff.ts +5 -0
  96. package/src/view-plan.test.ts +7 -0
  97. package/src/view-plan.ts +59 -0
  98. package/src/walk.ts +4 -0
package/src/view-diff.ts CHANGED
@@ -20,6 +20,7 @@ export interface DiffRow {
20
20
  export function diffRows(patchText: string): DiffRow[] {
21
21
  const rows: DiffRow[] = [];
22
22
  const patches = parsePatchFiles(patchText);
23
+
23
24
  for (const patch of patches) {
24
25
  for (const file of patch.files) {
25
26
  rows.push({ kind: "file", text: fileLabel(file), file: file.name });
@@ -27,6 +28,7 @@ export function diffRows(patchText: string): DiffRow[] {
27
28
  rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
28
29
  let oldLine = hunk.deletionStart;
29
30
  let newLine = hunk.additionStart;
31
+
30
32
  for (const segment of hunk.hunkContent) {
31
33
  if (segment.type === "context") {
32
34
  for (let lineOffset = 0; lineOffset < segment.lines; lineOffset++) {
@@ -60,6 +62,7 @@ export function diffRows(patchText: string): DiffRow[] {
60
62
  }
61
63
  }
62
64
  }
65
+
63
66
  return rows;
64
67
  }
65
68
 
@@ -77,6 +80,7 @@ export function diffRowAnchor(
77
80
  const row = rows[rowIndex]!;
78
81
  const prev = rows[rowIndex - 1];
79
82
  const next = rows[rowIndex + 1];
83
+
80
84
  return {
81
85
  quote: row.text,
82
86
  prefix:
@@ -93,5 +97,6 @@ export function diffRowAnchor(
93
97
  /** Location label for the rail and feedback: file:newLine (or old for del). */
94
98
  export function diffRowLocation(row: DiffRow): string {
95
99
  const line = row.newLine ?? row.oldLine;
100
+
96
101
  return line !== undefined ? `${row.file}:${line}` : row.file;
97
102
  }
@@ -45,6 +45,7 @@ describe("buildDisplay reconciliation", () => {
45
45
 
46
46
  // Assert
47
47
  const mod = display.find((block) => block.type === "mod")!;
48
+
48
49
  expect(mod.base!.text).toContain("persists");
49
50
  expect(mod.work!.text).toContain("writes");
50
51
  });
@@ -92,6 +93,7 @@ describe("blockRuns + overlayMarks", () => {
92
93
  .filter((run) => run.role !== "del")
93
94
  .map((run) => run.text)
94
95
  .join("");
96
+
95
97
  expect(oldSide).toBe("plain old text here");
96
98
  expect(newSide).toBe("plain new text here");
97
99
  });
@@ -109,6 +111,7 @@ describe("blockRuns + overlayMarks", () => {
109
111
  .filter((run) => run.role === "mark-comment")
110
112
  .map((run) => run.text)
111
113
  .join("");
114
+
112
115
  expect(markedText).toBe("words");
113
116
  // del run text untouched
114
117
  expect(marked.some((run) => run.role === "del" && run.text === "old")).toBe(true);
@@ -226,6 +229,7 @@ describe("marksByDisplay", () => {
226
229
 
227
230
  // Assert
228
231
  const [displayIndex, marks] = [...map.entries()][0]!;
232
+
229
233
  expect(displayText(display[displayIndex]!)).toContain("atomically");
230
234
  expect(marks[0]!.annotationId).toBe("a1");
231
235
  });
@@ -270,6 +274,7 @@ describe("rendered/work offset mapping", () => {
270
274
 
271
275
  // Assert
272
276
  const renderedText = runs.map((run) => run.text).join("");
277
+
273
278
  expect(renderedText.slice(rendered, rendered + "words".length)).toBe("words");
274
279
  // reading a rendered selection over "words" recovers the work range
275
280
  expect(workRangeForRendered(runs, rendered, rendered + "words".length)).toEqual({
@@ -315,6 +320,7 @@ describe("rendered/work offset mapping", () => {
315
320
 
316
321
  // Assert
317
322
  const renderedText = runs.map((run) => run.text).join("");
323
+
318
324
  expect(renderedText.slice(renderedStart, renderedEnd + 1)).toBe("very");
319
325
  // exact hits behave like renderedOffsetFor
320
326
  expect(renderedOffsetAtOrAfter(runs, 0)).toBe(0);
@@ -326,6 +332,7 @@ describe("rendered/work offset mapping", () => {
326
332
 
327
333
  describe("span mode", () => {
328
334
  const text = "one two three four";
335
+
329
336
  test("v selects the first word; l grows; w slides; $ to end", () => {
330
337
  // Act
331
338
  let span = startSpan(0, text)!;
package/src/view-plan.ts CHANGED
@@ -43,14 +43,17 @@ function similar(baseBlock: Block, workBlock: Block): boolean {
43
43
  const baseWords = words(baseBlock.text);
44
44
  const workWords = words(workBlock.text);
45
45
  let intersection = 0;
46
+
46
47
  for (const word of baseWords) if (workWords.has(word)) intersection++;
47
48
  const union = baseWords.size + workWords.size - intersection;
49
+
48
50
  return union === 0 ? true : intersection / union >= 0.3;
49
51
  }
50
52
 
51
53
  /** Reconcile base blocks vs working blocks into the display list. */
52
54
  export function buildDisplay(baseContent: string, workingContent?: string): DisplayBlock[] {
53
55
  const baseBlocks = parseBlocks(baseContent);
56
+
54
57
  if (workingContent === undefined) {
55
58
  const display: DisplayBlock[] = baseBlocks.map((block) => ({
56
59
  type: "same",
@@ -58,7 +61,9 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
58
61
  work: block,
59
62
  base: block,
60
63
  }));
64
+
61
65
  numberOrderedListItems(display);
66
+
62
67
  return display;
63
68
  }
64
69
  const workBlocks = parseBlocks(workingContent);
@@ -70,8 +75,10 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
70
75
  );
71
76
  const display: DisplayBlock[] = [];
72
77
  let opIndex = 0;
78
+
73
79
  while (opIndex < ops.length) {
74
80
  const op = ops[opIndex]!;
81
+
75
82
  if (op.kind === "ctx") {
76
83
  display.push({ type: "same", kind: op.newValue!.kind, work: op.newValue, base: op.oldValue });
77
84
  opIndex++;
@@ -79,14 +86,17 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
79
86
  }
80
87
  const deletedBlocks: Block[] = [];
81
88
  const addedBlocks: Block[] = [];
89
+
82
90
  while (opIndex < ops.length && ops[opIndex]!.kind !== "ctx") {
83
91
  const changeOp = ops[opIndex]!;
92
+
84
93
  if (changeOp.kind === "del") deletedBlocks.push(changeOp.oldValue!);
85
94
  else addedBlocks.push(changeOp.newValue!);
86
95
  opIndex++;
87
96
  }
88
97
  let deletedIndex = 0;
89
98
  let addedIndex = 0;
99
+
90
100
  while (deletedIndex < deletedBlocks.length && addedIndex < addedBlocks.length) {
91
101
  if (similar(deletedBlocks[deletedIndex]!, addedBlocks[addedIndex]!)) {
92
102
  display.push({
@@ -129,11 +139,13 @@ export function buildDisplay(baseContent: string, workingContent?: string): Disp
129
139
  }
130
140
  }
131
141
  numberOrderedListItems(display);
142
+
132
143
  return display;
133
144
  }
134
145
 
135
146
  function numberOrderedListItems(display: DisplayBlock[]): void {
136
147
  let runLength = 0;
148
+
137
149
  for (const block of display) {
138
150
  if (block.kind === "oli") block.orderedItemNumber = ++runLength;
139
151
  else runLength = 0;
@@ -188,14 +200,18 @@ export function marksByDisplay(
188
200
  .filter((entry) => entry.block.work !== undefined);
189
201
  const workBlocks = workEntries.map((entry) => entry.block.work!);
190
202
  const marksByIndex = new Map<number, Mark[]>();
203
+
191
204
  for (const annotation of annotations) {
192
205
  // an addressed annotation keeps its record but paints no highlight
193
206
  if (isAddressed(annotation)) continue;
194
207
  const resolved = resolveAnchor(annotation.anchor, workBlocks);
208
+
195
209
  if (!resolved) continue;
196
210
  const entry = workEntries[resolved.blockIndex];
211
+
197
212
  if (!entry) continue;
198
213
  const marks = marksByIndex.get(entry.displayIndex) ?? [];
214
+
199
215
  marks.push({
200
216
  start: resolved.start,
201
217
  end: resolved.end,
@@ -204,16 +220,19 @@ export function marksByDisplay(
204
220
  });
205
221
  marksByIndex.set(entry.displayIndex, marks);
206
222
  }
223
+
207
224
  return marksByIndex;
208
225
  }
209
226
 
210
227
  /** Base runs for a display block: plain text, or word-diff for mod blocks. */
211
228
  export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
212
229
  const literal = block.kind === "code" || block.kind === "hr";
230
+
213
231
  if (block.type === "mod" && markup) {
214
232
  const changes = wordLevelChanges(block.base!.text, block.work!.text);
215
233
  const runs: StyleRun[] = [];
216
234
  let workOffset = 0;
235
+
217
236
  for (const change of changes) {
218
237
  if (change.kind === "removed") {
219
238
  runs.push({ text: change.text, role: "del", start: null });
@@ -225,19 +244,23 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
225
244
  const segment = literal
226
245
  ? [{ text: change.text, role: "plain" as const, start: workOffset }]
227
246
  : inlineStyleRuns(change.text, workOffset);
247
+
228
248
  for (const run of segment) {
229
249
  runs.push(change.kind === "added" ? { ...run, role: "ins" } : run);
230
250
  }
231
251
  workOffset += change.text.length;
232
252
  }
253
+
233
254
  return runs;
234
255
  }
235
256
  const text = displayText(block);
257
+
236
258
  // Prose carries inline markup; code fences and rules are literal. Concealed
237
259
  // markers are dropped, so every rendered cell still maps to a work offset.
238
260
  if (markup && !literal) {
239
261
  return inlineStyleRuns(text, 0);
240
262
  }
263
+
241
264
  return [{ text, role: "plain", start: 0 }];
242
265
  }
243
266
 
@@ -248,6 +271,7 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
248
271
  export function safeLinkHref(href: string | undefined): string | undefined {
249
272
  if (href === undefined) return undefined;
250
273
  if (!/^(https?:|mailto:)/i.test(href)) return undefined;
274
+
251
275
  return /^[\x21-\x7e]+$/.test(href) ? href : undefined;
252
276
  }
253
277
 
@@ -258,6 +282,7 @@ export function safeLinkHref(href: string | undefined): string | undefined {
258
282
  */
259
283
  export function inlineStyleRuns(text: string, base: number): StyleRun[] {
260
284
  const runs: StyleRun[] = [];
285
+
261
286
  for (const run of inlineRuns(text)) {
262
287
  if (run.role === "marker" || run.start === null) continue;
263
288
  runs.push({
@@ -267,6 +292,7 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
267
292
  ...(run.href !== undefined ? { href: run.href } : {}),
268
293
  });
269
294
  }
295
+
270
296
  return runs;
271
297
  }
272
298
 
@@ -274,15 +300,18 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
274
300
  export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
275
301
  if (!marks.length) return runs;
276
302
  const splitRuns: StyleRun[] = [];
303
+
277
304
  for (const run of runs) {
278
305
  if (run.start === null) {
279
306
  splitRuns.push(run);
280
307
  continue;
281
308
  }
282
309
  const bounds = new Set<number>([0, run.text.length]);
310
+
283
311
  for (const mark of marks) {
284
312
  const boundStart = Math.max(0, mark.start - run.start);
285
313
  const boundEnd = Math.min(run.text.length, mark.end - run.start);
314
+
286
315
  if (boundStart < boundEnd) {
287
316
  bounds.add(boundStart);
288
317
  bounds.add(boundEnd);
@@ -290,14 +319,17 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
290
319
  }
291
320
  const runStart = run.start;
292
321
  const boundaries = [...bounds].sort((left, right) => left - right);
322
+
293
323
  for (let boundaryIndex = 0; boundaryIndex < boundaries.length - 1; boundaryIndex++) {
294
324
  const sliceStart = boundaries[boundaryIndex]!;
295
325
  const sliceEnd = boundaries[boundaryIndex + 1]!;
326
+
296
327
  if (sliceStart >= sliceEnd) continue;
297
328
  const absoluteStart = runStart + sliceStart;
298
329
  const mark = marks.find(
299
330
  (candidate) => candidate.start <= absoluteStart && runStart + sliceEnd <= candidate.end,
300
331
  );
332
+
301
333
  splitRuns.push({
302
334
  text: run.text.slice(sliceStart, sliceEnd),
303
335
  // a mark never overrides ins/del emphasis; it rides on plain text
@@ -307,6 +339,7 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
307
339
  });
308
340
  }
309
341
  }
342
+
310
343
  return splitRuns;
311
344
  }
312
345
 
@@ -319,12 +352,14 @@ export function overlayMarks(runs: StyleRun[], marks: Mark[]): StyleRun[] {
319
352
  */
320
353
  export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number | null {
321
354
  let rendered = 0;
355
+
322
356
  for (const run of runs) {
323
357
  if (run.start !== null && workOffset >= run.start && workOffset < run.start + run.text.length) {
324
358
  return rendered + (workOffset - run.start);
325
359
  }
326
360
  rendered += run.text.length;
327
361
  }
362
+
328
363
  return null;
329
364
  }
330
365
 
@@ -336,12 +371,14 @@ export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number
336
371
  */
337
372
  export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): number | null {
338
373
  let rendered = 0;
374
+
339
375
  for (const run of runs) {
340
376
  if (run.start !== null && run.start + run.text.length > workOffset) {
341
377
  return rendered + Math.max(0, workOffset - run.start);
342
378
  }
343
379
  rendered += run.text.length;
344
380
  }
381
+
345
382
  return null;
346
383
  }
347
384
 
@@ -352,12 +389,14 @@ export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): n
352
389
  export function renderedOffsetAtOrBefore(runs: StyleRun[], workOffset: number): number | null {
353
390
  let rendered = 0;
354
391
  let best: number | null = null;
392
+
355
393
  for (const run of runs) {
356
394
  if (run.start !== null && run.start <= workOffset) {
357
395
  best = rendered + Math.min(workOffset - run.start, run.text.length - 1);
358
396
  }
359
397
  rendered += run.text.length;
360
398
  }
399
+
361
400
  return best;
362
401
  }
363
402
 
@@ -373,22 +412,27 @@ export function workRangeForRendered(
373
412
  ): { start: number; end: number } | null {
374
413
  let rendered = 0;
375
414
  let range: { start: number; end: number } | null = null;
415
+
376
416
  for (const run of runs) {
377
417
  const runRenderedStart = rendered;
378
418
  const runRenderedEnd = rendered + run.text.length;
419
+
379
420
  rendered = runRenderedEnd;
380
421
  if (run.start === null) continue;
381
422
  const overlapStart = Math.max(renderedStart, runRenderedStart);
382
423
  const overlapEnd = Math.min(renderedEnd, runRenderedEnd);
424
+
383
425
  if (overlapEnd <= overlapStart) continue;
384
426
  const workStart = run.start + (overlapStart - runRenderedStart);
385
427
  const workEnd = run.start + (overlapEnd - runRenderedStart);
428
+
386
429
  if (!range) range = { start: workStart, end: workEnd };
387
430
  else {
388
431
  range.start = Math.min(range.start, workStart);
389
432
  range.end = Math.max(range.end, workEnd);
390
433
  }
391
434
  }
435
+
392
436
  return range;
393
437
  }
394
438
 
@@ -400,10 +444,12 @@ export function revisionDelta(
400
444
  const ops = lcsDiff(previousContent.split("\n"), nextContent.split("\n"));
401
445
  let added = 0;
402
446
  let removed = 0;
447
+
403
448
  for (const op of ops) {
404
449
  if (op.kind === "add") added++;
405
450
  else if (op.kind === "del") removed++;
406
451
  }
452
+
407
453
  return { added, removed };
408
454
  }
409
455
 
@@ -421,14 +467,18 @@ export function wordRanges(text: string): [number, number][] {
421
467
  const ranges: [number, number][] = [];
422
468
  const wordPattern = /\S+/g;
423
469
  let match: RegExpExecArray | null;
470
+
424
471
  while ((match = wordPattern.exec(text)))
425
472
  ranges.push([match.index, match.index + match[0]!.length]);
473
+
426
474
  return ranges;
427
475
  }
428
476
 
429
477
  export function startSpan(displayIndex: number, text: string): SpanState | null {
430
478
  const words = wordRanges(text);
479
+
431
480
  if (!words.length) return null;
481
+
432
482
  return { displayIndex, wordIndex: 0, wordEnd: 0, start: words[0]![0], end: words[0]![1] };
433
483
  }
434
484
 
@@ -445,11 +495,14 @@ export function spanFromRange(
445
495
  ): SpanState | null {
446
496
  const words = wordRanges(text);
447
497
  const wordIndex = words.findIndex((word) => word[1] > start);
498
+
448
499
  if (wordIndex === -1) return null;
449
500
  let wordEnd = wordIndex;
501
+
450
502
  for (let index = wordIndex; index < words.length && words[index]![0] < end; index++) {
451
503
  wordEnd = index;
452
504
  }
505
+
453
506
  return {
454
507
  displayIndex,
455
508
  wordIndex,
@@ -463,14 +516,17 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
463
516
  const words = wordRanges(text);
464
517
  const nextSpan = { ...span };
465
518
  const width = nextSpan.wordEnd - nextSpan.wordIndex;
519
+
466
520
  if (key === "l") nextSpan.wordEnd = Math.min(words.length - 1, nextSpan.wordEnd + 1);
467
521
  else if (key === "h") nextSpan.wordEnd = Math.max(nextSpan.wordIndex, nextSpan.wordEnd - 1);
468
522
  else if (key === "w") {
469
523
  const slidTo = Math.min(words.length - 1 - width, nextSpan.wordIndex + 1);
524
+
470
525
  nextSpan.wordIndex = slidTo;
471
526
  nextSpan.wordEnd = slidTo + width;
472
527
  } else if (key === "b") {
473
528
  const slidTo = Math.max(0, nextSpan.wordIndex - 1);
529
+
474
530
  nextSpan.wordIndex = slidTo;
475
531
  nextSpan.wordEnd = slidTo + width;
476
532
  } else if (key === "$") nextSpan.wordEnd = words.length - 1;
@@ -480,6 +536,7 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
480
536
  } else return span;
481
537
  nextSpan.start = words[nextSpan.wordIndex]![0];
482
538
  nextSpan.end = words[nextSpan.wordEnd]![1];
539
+
483
540
  return nextSpan;
484
541
  }
485
542
 
@@ -489,7 +546,9 @@ export function spanKey(span: SpanState, key: string, text: string): SpanState {
489
546
  export function nextWorkBlock(display: DisplayBlock[], displayIndex: number): Block | undefined {
490
547
  for (let candidateIndex = displayIndex + 1; candidateIndex < display.length; candidateIndex++) {
491
548
  const workBlock = display[candidateIndex]!.work;
549
+
492
550
  if (workBlock) return workBlock;
493
551
  }
552
+
494
553
  return undefined;
495
554
  }
package/src/walk.ts CHANGED
@@ -24,8 +24,10 @@ export const WALK_PREVIEW_ROWS = 5;
24
24
  export function walkFiles(rows: DiffRow[]): WalkFile[] {
25
25
  const files: WalkFile[] = [];
26
26
  const byPath = new Map<string, WalkFile>();
27
+
27
28
  for (const row of rows) {
28
29
  let file = byPath.get(row.file);
30
+
29
31
  if (!file) {
30
32
  file = { path: row.file, added: 0, removed: 0, preview: [] };
31
33
  byPath.set(row.file, file);
@@ -41,6 +43,7 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
41
43
  });
42
44
  }
43
45
  }
46
+
44
47
  return files;
45
48
  }
46
49
 
@@ -51,6 +54,7 @@ export function walkFiles(rows: DiffRow[]): WalkFile[] {
51
54
  */
52
55
  export function firstUnviewedIndex(files: WalkFile[], viewedPaths: ReadonlySet<string>): number {
53
56
  const index = files.findIndex((file) => !viewedPaths.has(file.path));
57
+
54
58
  return index === -1 ? files.length : index;
55
59
  }
56
60