@cueloop/client 0.1.0-alpha.63 → 0.1.0-alpha.65

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 (68) hide show
  1. package/package.json +7 -6
  2. package/src/App.autoclose.test.tsx +2 -2
  3. package/src/App.export.test.tsx +2 -2
  4. package/src/App.plan-review.test.tsx +76 -247
  5. package/src/App.readonly.test.tsx +80 -35
  6. package/src/App.rendering.test.tsx +5 -6
  7. package/src/App.review-panel.test.tsx +29 -32
  8. package/src/App.submit-confirm.test.tsx +27 -12
  9. package/src/App.test.tsx +73 -49
  10. package/src/App.toast.test.tsx +34 -22
  11. package/src/App.tree.test.tsx +224 -0
  12. package/src/App.tsx +134 -87
  13. package/src/app-screens.tsx +27 -1
  14. package/src/app-view-model.ts +7 -109
  15. package/src/components/AnnotationCard.tsx +2 -2
  16. package/src/components/DiffSheet.scroll.test.tsx +5 -2
  17. package/src/components/DiffSheet.tsx +1 -6
  18. package/src/components/KeybindsDialog.tsx +61 -17
  19. package/src/components/PrototypeSheet.tsx +5 -9
  20. package/src/components/ReviewPanel.stories.tsx +14 -0
  21. package/src/components/ReviewRail.stories.tsx +14 -0
  22. package/src/components/ReviewRail.tsx +15 -6
  23. package/src/components/ThreadView.selection.test.tsx +812 -0
  24. package/src/components/ThreadView.stories.tsx +26 -0
  25. package/src/components/ThreadView.tsx +1620 -0
  26. package/src/components/TreePane.stories.tsx +106 -0
  27. package/src/components/TreePane.tsx +156 -0
  28. package/src/components/__snapshots__/stories.test.tsx.snap +120 -172
  29. package/src/components/primitives/Tabs.tsx +1 -1
  30. package/src/components/story-fixtures.ts +6 -2
  31. package/src/components/story.ts +16 -4
  32. package/src/components/syntax-highlight.ts +10 -2
  33. package/src/components/terminal-pane.test.tsx +1 -1
  34. package/src/components/terminal-pane.ts +7 -1
  35. package/src/config.ts +112 -99
  36. package/src/diff-hunk-curate.ts +4 -104
  37. package/src/editor.ts +5 -1
  38. package/src/ghostty-terminal.test.ts +1 -1
  39. package/src/herdr.ts +10 -2
  40. package/src/index.ts +1 -0
  41. package/src/intent-dispatch.test.ts +235 -55
  42. package/src/intent-dispatch.ts +104 -34
  43. package/src/key-bindings.test.ts +1 -1
  44. package/src/key-bindings.ts +16 -7
  45. package/src/keymap.test.ts +9 -11
  46. package/src/keymap.ts +14 -7
  47. package/src/kitty-image.test.ts +3 -3
  48. package/src/prototype-browser.ts +7 -6
  49. package/src/serve.ts +1 -1
  50. package/src/session-controller.curate.test.ts +106 -4
  51. package/src/session-controller.share.test.ts +220 -70
  52. package/src/session-controller.tree.test.ts +228 -0
  53. package/src/session-controller.ts +370 -94
  54. package/src/share.ts +117 -17
  55. package/src/test-support.test.ts +193 -1
  56. package/src/test-support.ts +69 -1
  57. package/src/theme-presets.ts +9 -2
  58. package/src/thread-chords.test.ts +154 -0
  59. package/src/thread-chords.ts +144 -0
  60. package/src/thread-selection.test.ts +210 -0
  61. package/src/thread-selection.ts +243 -0
  62. package/src/tree-view.test.ts +128 -0
  63. package/src/tree-view.ts +148 -0
  64. package/src/view-diff.ts +1 -4
  65. package/src/view-plan.ts +35 -19
  66. package/src/components/PlanSheet.stories.tsx +0 -113
  67. package/src/components/PlanSheet.tsx +0 -422
  68. package/src/components/plan-sheet-run-style.ts +0 -90
@@ -10,13 +10,52 @@ export interface KeybindsDialogProps {
10
10
  theme?: Theme;
11
11
  }
12
12
 
13
+ /** Rows a section takes: its title, its entries, and the blank line after it. */
14
+ function sectionRows(section: CheatsheetSection): number {
15
+ return 1 + section.entries.length + 1;
16
+ }
17
+
18
+ /**
19
+ * Split the sections over `count` columns, filling each column to the
20
+ * balanced share of rows before starting the next, so a long cheatsheet
21
+ * reads top to bottom, then left to right.
22
+ */
23
+ function columnsOf(sections: CheatsheetSection[], count: number): CheatsheetSection[][] {
24
+ const total = sections.reduce((sum, section) => sum + sectionRows(section), 0);
25
+ const share = Math.ceil(total / count);
26
+ const columns: CheatsheetSection[][] = [[]];
27
+ let filled = 0;
28
+
29
+ for (const section of sections) {
30
+ const column = columns[columns.length - 1]!;
31
+
32
+ if (filled > 0 && filled + sectionRows(section) > share && columns.length < count) {
33
+ columns.push([section]);
34
+ filled = sectionRows(section);
35
+ } else {
36
+ column.push(section);
37
+ filled += sectionRows(section);
38
+ }
39
+ }
40
+
41
+ return columns;
42
+ }
43
+
44
+ const COLUMN_WIDTH = 48;
45
+
13
46
  export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.ReactNode {
14
47
  const tokens = useComponentTheme(theme);
15
48
  const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
16
- const contentRows =
17
- sections.reduce((sum, section) => sum + 1 + section.entries.length + 1, 0) + 1;
18
- const width = Math.min(52, terminalWidth - 6);
19
- const height = Math.min(terminalHeight - 4, contentRows + 3);
49
+ const totalRows = sections.reduce((sum, section) => sum + sectionRows(section), 0) + 1;
50
+ // a cheatsheet taller than the terminal folds into a second column
51
+ const columnCount =
52
+ totalRows + 3 > terminalHeight - 4 && terminalWidth >= COLUMN_WIDTH * 2 + 6 ? 2 : 1;
53
+ const columns = columnsOf(sections, columnCount);
54
+ const tallest = Math.max(
55
+ ...columns.map((column) => column.reduce((sum, section) => sum + sectionRows(section), 0)),
56
+ );
57
+ const width = Math.min(COLUMN_WIDTH * columnCount + 4, terminalWidth - 6);
58
+ const height = Math.min(terminalHeight - 4, tallest + 1 + 3);
20
59
 
21
60
  return (
22
61
  <Dialog
@@ -36,19 +75,24 @@ export function KeybindsDialog({ sections, theme }: KeybindsDialogProps): React.
36
75
  paddingTop: 1,
37
76
  }}
38
77
  >
39
- {sections.map((section) => (
40
- <box key={section.title} style={{ flexDirection: "column" }}>
41
- <text fg={tokens.accent}>{section.title}</text>
42
- {section.entries.map((entry, index) => (
43
- <text key={index}>
44
- <span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
45
- <span fg={tokens.textMuted}>{entry.label}</span>
46
- </text>
47
- ))}
48
- <text> </text>
49
- </box>
50
- ))}
51
- <box style={{ flexGrow: 1 }} />
78
+ <box style={{ flexDirection: "row", flexGrow: 1 }}>
79
+ {columns.map((column, columnIndex) => (
80
+ <box key={columnIndex} style={{ flexDirection: "column", width: COLUMN_WIDTH }}>
81
+ {column.map((section) => (
82
+ <box key={section.title} style={{ flexDirection: "column" }}>
83
+ <text fg={tokens.accent}>{section.title}</text>
84
+ {section.entries.map((entry, index) => (
85
+ <text key={index}>
86
+ <span fg={tokens.text}>{entry.keys.padEnd(12)}</span>
87
+ <span fg={tokens.textMuted}>{entry.label}</span>
88
+ </text>
89
+ ))}
90
+ <text> </text>
91
+ </box>
92
+ ))}
93
+ </box>
94
+ ))}
95
+ </box>
52
96
  <box style={{ flexDirection: "row" }}>
53
97
  <box style={{ flexGrow: 1 }} />
54
98
  <text fg={tokens.textDim}>[esc]</text>
@@ -10,6 +10,7 @@
10
10
 
11
11
  import React, { useEffect, useRef, useState } from "react";
12
12
  import type { BoxRenderable } from "@opentui/core";
13
+ import * as v from "valibot";
13
14
  import { useKeyboard, useRenderer } from "@opentui/react";
14
15
  import type { Theme } from "../theme";
15
16
  import { quickActionBody, type QuickAction } from "../config";
@@ -95,10 +96,7 @@ type CaptureRenderer = {
95
96
  * raster and fewer bytes). Without a resolution report, fall back to a fixed
96
97
  * width at 2x and derive the height from the cell aspect.
97
98
  */
98
- function captureConfig(
99
- region: CellRegion,
100
- renderer: CaptureRenderer,
101
- ): { viewport: PrototypeViewport; deviceScaleFactor: number } {
99
+ function captureConfig(region: CellRegion, renderer: CaptureRenderer) {
102
100
  const resolution = renderer.resolution;
103
101
 
104
102
  if (resolution && renderer.terminalWidth >= 1 && renderer.terminalHeight >= 1) {
@@ -174,11 +172,9 @@ function PrototypeSheetImpl({
174
172
  // harness), but the launch trigger still runs.
175
173
  useEffect(() => {
176
174
  if (!renderer) return;
177
- // writeOut is the renderer's raw output channel, ordered against its frame
178
- // bytes; it is private, so reach it through one narrow cast
179
- const rawWrite = (renderer as unknown as { writeOut?: (data: string) => void }).writeOut;
180
- const write =
181
- typeof rawWrite === "function" ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
175
+ const rendererOutput = v.safeParse(v.object({ writeOut: v.function() }), renderer);
176
+ const rawWrite = rendererOutput.success ? rendererOutput.output.writeOut : null;
177
+ const write = rawWrite ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
182
178
  const medium = resolveTransmitMedium();
183
179
  const paint = (): void => {
184
180
  if (!write) return;
@@ -7,8 +7,22 @@ import { FIXTURE_ANNOTATIONS, fixturePlanSession } from "./story-fixtures";
7
7
 
8
8
  export const meta: StoryMeta = { title: "ReviewPanel" };
9
9
 
10
+ /** An empty Tree tab: the stories here show the review tab. */
11
+ const tree = {
12
+ rows: [],
13
+ canMove: true,
14
+ canFork: true,
15
+ onSelect: () => {},
16
+ onGo: () => {},
17
+ onBranch: () => {},
18
+ onLabel: () => {},
19
+ onFork: () => {},
20
+ onForkAndShare: () => {},
21
+ };
22
+
10
23
  const rail = {
11
24
  session: fixturePlanSession(),
25
+ tree,
12
26
  authorNames: {},
13
27
  selectedId: FIXTURE_ANNOTATIONS[0]!.id,
14
28
  resolvedIds: new Set(FIXTURE_ANNOTATIONS.map((annotation) => annotation.id)),
@@ -6,8 +6,22 @@ import { FIXTURE_ANNOTATIONS, fixtureDiffSession, fixturePlanSession } from "./s
6
6
 
7
7
  export const meta: StoryMeta = { title: "ReviewRail" };
8
8
 
9
+ /** An empty Tree tab: the stories here show the review tab. */
10
+ const tree = {
11
+ rows: [],
12
+ canMove: true,
13
+ canFork: true,
14
+ onSelect: () => {},
15
+ onGo: () => {},
16
+ onBranch: () => {},
17
+ onLabel: () => {},
18
+ onFork: () => {},
19
+ onForkAndShare: () => {},
20
+ };
21
+
9
22
  const callbacks = {
10
23
  curationItems: [],
24
+ tree,
11
25
  onTabChange: () => {},
12
26
  onSelectCard: () => {},
13
27
  onActivateCard: () => {},
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The right review rail: tab strip (Review/Agent), the scrollable annotation
2
+ * The right review rail: tab strip (Review/Tree/Agent), the scrollable annotation
3
3
  * stack, and the pinned submit affordance. The confirm card sits OUTSIDE the
4
4
  * scrollbox so the stack scrolls while the card stays at the rail bottom.
5
5
  * Card reveal goes through the scrollbox's own scrollChildIntoView - no
@@ -18,11 +18,12 @@ import { Toolbar } from "./primitives/Toolbar";
18
18
  import { AnnotationCard, type AnnotationDraft } from "./AnnotationCard";
19
19
  import { ConfirmCard, type ConfirmCardProps } from "./ConfirmCard";
20
20
  import { AgentLauncher, type AgentTerminalHandle } from "./agent-launcher";
21
+ import { TreePane, type TreePaneProps } from "./TreePane";
21
22
  import { Card } from "./primitives/Card";
22
23
  import { truncateToSingleLine } from "./truncate-text";
23
24
  import { resolveDisplayName } from "../attribution";
24
25
 
25
- export type RailTab = "review" | "agent";
26
+ export type RailTab = "review" | "agent" | "tree";
26
27
 
27
28
  export interface RailCardEdit extends AnnotationDraft {
28
29
  id: string;
@@ -65,6 +66,8 @@ export interface ReviewRailProps {
65
66
  onLaunchHarness: (command: string, seedText?: string) => void;
66
67
  /** In-tab agent terminal handle (embedded path); null when detached. Lets the app route keys to it. */
67
68
  onAgentTerminal?: (handle: AgentTerminalHandle | null) => void;
69
+ /** The Tree tab: the session's history rows and the primitives that move it. */
70
+ tree: Omit<TreePaneProps, "theme">;
68
71
  /** Rail column width; the app derives it from the persisted review layout. */
69
72
  width?: number;
70
73
  /**
@@ -95,6 +98,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
95
98
  onSubmitRequest,
96
99
  onLaunchHarness,
97
100
  onAgentTerminal,
101
+ tree,
98
102
  width = 34,
99
103
  onCollapse,
100
104
  theme,
@@ -187,15 +191,20 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
187
191
  >
188
192
  <Tabs
189
193
  selectedKey={railTab}
190
- onSelectionChange={(key) => onTabChange(key as RailTab)}
194
+ onSelectionChange={(key) => {
195
+ if (key === "review" || key === "agent" || key === "tree") onTabChange(key);
196
+ }}
191
197
  theme={theme}
192
198
  >
193
199
  <TabList>
194
200
  <Tab id="review">Review</Tab>
201
+ <Tab id="tree">Tree</Tab>
195
202
  <Tab id="agent">Agent</Tab>
196
203
  </TabList>
197
204
  </Tabs>
198
- {railTab === "agent" ? (
205
+ {railTab === "tree" ? (
206
+ <TreePane {...tree} theme={theme} />
207
+ ) : railTab === "agent" ? (
199
208
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
200
209
  <AgentLauncher
201
210
  session={session}
@@ -221,7 +230,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
221
230
  </box>
222
231
  )}
223
232
  {/* the confirm card keeps full width, above the footer row */}
224
- {railTab !== "agent" && submitConfirm ? (
233
+ {railTab === "review" && submitConfirm ? (
225
234
  <ConfirmCard {...submitConfirm} theme={theme} />
226
235
  ) : null}
227
236
  {/* footer row: collapse chevron left-bound, the submit affordance inline
@@ -232,7 +241,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
232
241
  <text fg={tokens.textDim}>{">"}</text>
233
242
  </box>
234
243
  ) : null}
235
- {railTab !== "agent" && !submitConfirm ? (
244
+ {railTab === "review" && !submitConfirm ? (
236
245
  session.status === "resolved" ? (
237
246
  <text fg={tokens.green}>resolved: {session.verdict!.kind.replace("_", " ")}</text>
238
247
  ) : (