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

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 (61) hide show
  1. package/package.json +5 -5
  2. package/src/App.autoclose.test.tsx +4 -3
  3. package/src/App.diff.test.tsx +5 -2
  4. package/src/App.export.test.tsx +5 -7
  5. package/src/App.plan-review.test.tsx +14 -17
  6. package/src/App.readonly.test.tsx +9 -7
  7. package/src/App.rendering.test.tsx +6 -2
  8. package/src/App.review-panel.test.tsx +19 -14
  9. package/src/App.submit-confirm.test.tsx +6 -7
  10. package/src/App.test.tsx +8 -5
  11. package/src/App.toast.test.tsx +68 -0
  12. package/src/App.tsx +154 -50
  13. package/src/App.walk.test.tsx +7 -4
  14. package/src/components/AnnotationCard.stories.tsx +1 -1
  15. package/src/components/AnnotationCard.tsx +18 -13
  16. package/src/components/CompletionOverlay.tsx +1 -1
  17. package/src/components/ConfirmCard.tsx +1 -0
  18. package/src/components/ConfirmDialog.stories.tsx +1 -1
  19. package/src/components/ConfirmDialog.tsx +1 -1
  20. package/src/components/DiffSheet.stories.tsx +21 -3
  21. package/src/components/DiffSheet.tsx +45 -19
  22. package/src/components/InboxList.stories.tsx +1 -1
  23. package/src/components/InboxList.tsx +3 -3
  24. package/src/components/KeybindsDialog.stories.tsx +31 -0
  25. package/src/components/KeybindsDialog.tsx +42 -0
  26. package/src/components/MenuBar.stories.tsx +33 -0
  27. package/src/components/MenuBar.tsx +58 -0
  28. package/src/components/PlanSheet.stories.tsx +2 -2
  29. package/src/components/PlanSheet.tsx +26 -75
  30. package/src/components/PromptDialog.stories.tsx +2 -2
  31. package/src/components/PromptDialog.tsx +1 -1
  32. package/src/components/ReviewPanel.stories.tsx +5 -17
  33. package/src/components/ReviewPanel.tsx +10 -29
  34. package/src/components/ReviewRail.stories.tsx +39 -0
  35. package/src/components/ReviewRail.tsx +62 -57
  36. package/src/components/SettingsDialog.stories.tsx +1 -1
  37. package/src/components/SettingsDialog.tsx +4 -3
  38. package/src/components/SettingsRows.stories.tsx +1 -1
  39. package/src/components/SettingsRows.tsx +1 -1
  40. package/src/components/Toast.stories.tsx +12 -0
  41. package/src/components/Toast.tsx +29 -0
  42. package/src/components/WalkWizard.stories.tsx +1 -1
  43. package/src/components/WalkWizard.tsx +1 -1
  44. package/src/components/__snapshots__/stories.test.tsx.snap +598 -566
  45. package/src/components/primitives/Button.stories.tsx +2 -2
  46. package/src/components/primitives/Dialog.stories.tsx +1 -1
  47. package/src/components/primitives/Dialog.tsx +7 -5
  48. package/src/components/primitives/Tabs.tsx +19 -38
  49. package/src/components/stories-app.tsx +1 -1
  50. package/src/components/stories.test.tsx +2 -0
  51. package/src/key-bindings.test.ts +4 -4
  52. package/src/key-bindings.ts +41 -2
  53. package/src/serve.test.ts +2 -2
  54. package/src/session-controller.share.test.ts +13 -0
  55. package/src/session-controller.ts +19 -1
  56. package/src/test-support.test.ts +55 -0
  57. package/src/test-support.ts +113 -23
  58. package/src/theme.ts +16 -16
  59. package/src/version.ts +5 -0
  60. package/src/components/ComposeBar.stories.tsx +0 -18
  61. package/src/components/ComposeBar.tsx +0 -54
@@ -14,16 +14,12 @@ import type { Annotation } from "@cueloop/schema";
14
14
  import type { Theme } from "../theme";
15
15
  import { useComponentTheme } from "./theme-context";
16
16
  import { ReviewRail, type ReviewRailHandle, type ReviewRailProps } from "./ReviewRail";
17
- import { REVIEW_COMPACT_WIDTH, reviewRowsToDivider, type ReviewPanelMode } from "../review-panel";
17
+ import { REVIEW_COMPACT_WIDTH, type ReviewPanelMode } from "../review-panel";
18
18
 
19
19
  export interface ReviewPanelProps {
20
20
  mode: ReviewPanelMode;
21
21
  /** Expanded-rail width in columns (already clamped by the app). */
22
22
  width: number;
23
- /** Rows for the divider glyph column - the height of the plan/rail row. */
24
- height: number;
25
- /** Accent the divider while a drag is live. */
26
- dragging: boolean;
27
23
  /** Arm a divider drag; the app tracks the drag itself on the container. */
28
24
  onDividerGrab: () => void;
29
25
  /** Chevron click: expanded <-> compact (never hidden). */
@@ -34,28 +30,15 @@ export interface ReviewPanelProps {
34
30
  theme?: Theme;
35
31
  }
36
32
 
37
- /** A single-column stack of `│`, accent while dragging. Grabbing arms a drag. */
38
- function ReviewDivider({
39
- dragging,
40
- rows,
41
- onGrab,
42
- theme,
43
- }: {
44
- dragging: boolean;
45
- rows: number;
46
- onGrab: () => void;
47
- theme?: Theme;
48
- }): React.ReactNode {
49
- const tokens = useComponentTheme(theme);
50
- return (
51
- <box style={{ width: 1 }} onMouseDown={onGrab}>
52
- <text fg={dragging ? tokens.accent : tokens.border}>{reviewRowsToDivider(rows)}</text>
53
- </box>
54
- );
33
+ /** An invisible, full-height grab column: the plan's right border is the seam. */
34
+ function ReviewDivider({ onGrab }: { onGrab: () => void }): React.ReactNode {
35
+ // childless: it stretches to the row height like the plan and rail beside it,
36
+ // so the three columns share one bottom edge. Invisible, but full-height grab.
37
+ return <box style={{ width: 1 }} onMouseDown={onGrab} />;
55
38
  }
56
39
 
57
40
  /** The compact strip: the card count, one kind-colored dot per card, and the
58
- * `«` chevron left-bound to match the expanded `»` gap. */
41
+ * `<` expand chevron, bottom-padded to sit at the same row as the expanded `>`. */
59
42
  function CompactRail({
60
43
  annotations,
61
44
  onExpand,
@@ -70,7 +53,7 @@ function CompactRail({
70
53
  annotation.kind === "suggestion" ? tokens.green : tokens.accent;
71
54
  return (
72
55
  <box
73
- style={{ width: REVIEW_COMPACT_WIDTH, backgroundColor: tokens.panel, flexDirection: "column", paddingTop: 1 }}
56
+ style={{ width: REVIEW_COMPACT_WIDTH, backgroundColor: tokens.panel, flexDirection: "column", paddingTop: 1, paddingBottom: 1 }}
74
57
  >
75
58
  <box style={{ alignItems: "center" }}>
76
59
  <text fg={tokens.accent}>{String(annotations.length)}</text>
@@ -85,7 +68,7 @@ function CompactRail({
85
68
  </box>
86
69
  <box style={{ flexGrow: 1 }} />
87
70
  <box style={{ paddingLeft: 1 }} onMouseUp={onExpand}>
88
- <text fg={tokens.textDim}>«</text>
71
+ <text fg={tokens.textDim}>{"<"}</text>
89
72
  </box>
90
73
  </box>
91
74
  );
@@ -94,8 +77,6 @@ function CompactRail({
94
77
  export function ReviewPanel({
95
78
  mode,
96
79
  width,
97
- height,
98
- dragging,
99
80
  onDividerGrab,
100
81
  onToggle,
101
82
  rail,
@@ -105,7 +86,7 @@ export function ReviewPanel({
105
86
  if (mode === "hidden") return null;
106
87
  return (
107
88
  <>
108
- <ReviewDivider dragging={dragging} rows={height} onGrab={onDividerGrab} theme={theme} />
89
+ <ReviewDivider onGrab={onDividerGrab} />
109
90
  {mode === "expanded" ? (
110
91
  <ReviewRail ref={railRef} {...rail} width={width} onCollapse={onToggle} theme={theme} />
111
92
  ) : (
@@ -38,6 +38,45 @@ export const AnnotationStack: Story = {
38
38
  size: { width: 40, height: 24 },
39
39
  };
40
40
 
41
+ export const MixedOwnershipStack: Story = {
42
+ render: () => (
43
+ <RailFrame>
44
+ <ReviewRail
45
+ session={fixturePlanSession({
46
+ participants: [{ id: "SHA256:collab1", provider: "ssh", name: "Priya" }],
47
+ annotations: [
48
+ {
49
+ id: "own_note",
50
+ kind: "comment",
51
+ anchor: { quote: "persists sessions", prefix: "The daemon ", suffix: " to disk" },
52
+ body: "Which daemon owns this?",
53
+ createdAt: "2026-01-01T00:00:00Z",
54
+ },
55
+ {
56
+ id: "collab_note",
57
+ kind: "suggestion",
58
+ anchor: { quote: "move the store", prefix: "", suffix: "" },
59
+ body: "move it behind one interface",
60
+ author: "SHA256:collab1",
61
+ createdAt: "2026-01-01T00:00:01Z",
62
+ },
63
+ ],
64
+ })}
65
+ authorNames={{}}
66
+ selectedId="own_note"
67
+ resolvedIds={new Set(["own_note", "collab_note"])}
68
+ railTab="review"
69
+ pendingCount={2}
70
+ cardEdit={null}
71
+ submitConfirm={null}
72
+ {...callbacks}
73
+ />
74
+ </RailFrame>
75
+ ),
76
+ expectedColors: [DARK.accent, DARK.blue],
77
+ size: { width: 40, height: 24 },
78
+ };
79
+
41
80
  export const AgentTab: Story = {
42
81
  render: () => (
43
82
  <RailFrame>
@@ -8,7 +8,7 @@
8
8
 
9
9
  import React, { forwardRef, useImperativeHandle, useRef } from "react";
10
10
  import type { ScrollBoxRenderable } from "@opentui/core";
11
- import { isAddressed, type Annotation, type ReviewSession } from "@cueloop/schema";
11
+ import { isAddressed, isAgentNote, type Annotation, type ReviewSession } from "@cueloop/schema";
12
12
  import type { Theme } from "../theme";
13
13
  import { useComponentTheme } from "./theme-context";
14
14
  import { Tabs, Tab, TabList } from "./primitives/Tabs";
@@ -46,7 +46,7 @@ export interface ReviewRailProps {
46
46
  /** Rail column width; the app derives it from the persisted review layout. */
47
47
  width?: number;
48
48
  /**
49
- * When set, renders the muted `»` chevron pinned at the rail's bottom-left,
49
+ * When set, renders the muted `>` chevron pinned at the rail's bottom-left,
50
50
  * one column from the divider. Clicking it collapses the panel to compact.
51
51
  */
52
52
  onCollapse?: () => void;
@@ -83,6 +83,7 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
83
83
  // addressed annotations leave the card list; a dim count keeps them honest
84
84
  const openAnnotations = session.annotations.filter((annotation) => !isAddressed(annotation));
85
85
  const addressedCount = session.annotations.length - openAnnotations.length;
86
+ const collaboratorsPresent = session.annotations.some((annotation) => annotation.author);
86
87
 
87
88
  useImperativeHandle(handleRef, () => ({
88
89
  revealCard: (annotationId: string): void => {
@@ -95,74 +96,78 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
95
96
  }));
96
97
 
97
98
  return (
98
- <box style={{ width, backgroundColor: tokens.panel, flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
99
+ <box style={{ width, backgroundColor: tokens.panel, flexDirection: "column", paddingLeft: 1, paddingBottom: 1 }}>
99
100
  <Tabs selectedKey={railTab} onSelectionChange={(key) => onTabChange(key as RailTab)} theme={theme}>
100
101
  <TabList>
101
- <Tab id="review">{`Review (${pendingCount})`}</Tab>
102
+ <Tab id="review">Review</Tab>
102
103
  <Tab id="agent">Agent</Tab>
103
104
  </TabList>
104
105
  </Tabs>
105
- <text> </text>
106
106
  {railTab === "agent" ? (
107
- <AgentTab session={session} theme={theme} />
107
+ <box style={{ flexGrow: 1, flexDirection: "column", paddingLeft: 2 }}>
108
+ <text> </text>
109
+ <AgentTab session={session} theme={theme} />
110
+ </box>
111
+ ) : openAnnotations.length === 0 ? (
112
+ <box style={{ flexGrow: 1, alignItems: "center", justifyContent: "center" }}>
113
+ <text fg={tokens.textDim}>{session.annotations.length === 0 ? "no annotations yet" : "all annotations addressed"}</text>
114
+ </box>
108
115
  ) : (
109
- <>
116
+ <box style={{ flexGrow: 1, flexDirection: "column" }}>
110
117
  {session.workingCopy !== undefined ? <text fg={tokens.textDim}>± plan edits → one diff</text> : null}
111
118
  {addressedCount > 0 ? <text fg={tokens.textDim}>✓ {addressedCount} addressed by revision</text> : null}
112
- {openAnnotations.length === 0 ? (
113
- <text fg={tokens.textDim}>{session.annotations.length === 0 ? "no annotations yet" : "all annotations addressed"}</text>
114
- ) : (
115
- <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
116
- {openAnnotations.map((annotation) => (
117
- <AnnotationCard
118
- key={annotation.id}
119
- id={`annotation-card-${annotation.id}`}
120
- kind={annotation.kind}
121
- quote={annotation.anchor.quote}
122
- theme={theme}
123
- saved={{
124
- body: annotation.body,
125
- isSelected: annotation.id === selectedId,
126
- isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
127
- isBlocking: annotationBlocking(annotation),
128
- authorLabel: annotation.author ? resolveDisplayName(annotation.author, session.participants, authorNames) : undefined,
129
- editing:
130
- cardEdit && cardEdit.id === annotation.id
131
- ? {
132
- text: cardEdit.text,
133
- onInput: cardEdit.onInput,
134
- onSave: cardEdit.onSave,
135
- onCancel: cardEdit.onCancel,
136
- }
137
- : null,
138
- onPress: () =>
139
- annotation.id === selectedId ? onActivateCard(annotation.id) : onSelectCard(annotation.id),
140
- }}
141
- />
142
- ))}
143
- </scrollbox>
144
- )}
145
- <box style={{ flexGrow: 1 }} />
146
- {/* the confirm card sits OUTSIDE the scrollbox: the annotation stack
147
- above scrolls while the card stays pinned to the rail bottom */}
148
- {session.status === "resolved" ? (
119
+ <scrollbox ref={scrollRef} style={{ flexGrow: 1 }} focused={false}>
120
+ {openAnnotations.map((annotation) => (
121
+ <AnnotationCard
122
+ key={annotation.id}
123
+ id={`annotation-card-${annotation.id}`}
124
+ kind={annotation.kind}
125
+ quote={annotation.anchor.quote}
126
+ theme={theme}
127
+ saved={{
128
+ body: annotation.body,
129
+ isSelected: annotation.id === selectedId,
130
+ isOrphan: resolvedIds !== null && !resolvedIds.has(annotation.id),
131
+ isBlocking: annotationBlocking(annotation),
132
+ authorLabel: annotation.author ? resolveDisplayName(annotation.author, session.participants, authorNames) : undefined,
133
+ selfLabel: !annotation.author && !isAgentNote(annotation) && collaboratorsPresent ? "me" : undefined,
134
+ editing:
135
+ cardEdit && cardEdit.id === annotation.id
136
+ ? {
137
+ text: cardEdit.text,
138
+ onInput: cardEdit.onInput,
139
+ onSave: cardEdit.onSave,
140
+ onCancel: cardEdit.onCancel,
141
+ }
142
+ : null,
143
+ onPress: () =>
144
+ annotation.id === selectedId ? onActivateCard(annotation.id) : onSelectCard(annotation.id),
145
+ }}
146
+ />
147
+ ))}
148
+ </scrollbox>
149
+ </box>
150
+ )}
151
+ {/* the confirm card keeps full width, above the footer row */}
152
+ {railTab !== "agent" && submitConfirm ? <ConfirmCard {...submitConfirm} theme={theme} /> : null}
153
+ {/* footer row: collapse chevron left-bound, the submit affordance inline
154
+ beside it, both on the rail's last row (the plan's bottom-border height) */}
155
+ <box style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
156
+ {onCollapse ? (
157
+ <box onMouseUp={onCollapse}>
158
+ <text fg={tokens.textDim}>{">"}</text>
159
+ </box>
160
+ ) : null}
161
+ {railTab !== "agent" && !submitConfirm ? (
162
+ session.status === "resolved" ? (
149
163
  <text fg={tokens.green}>resolved: {session.verdict!.kind.replace("_", " ")}</text>
150
- ) : submitConfirm ? (
151
- <ConfirmCard {...submitConfirm} theme={theme} />
152
164
  ) : (
153
165
  <Button variant="accent-text" onPress={onSubmitRequest} theme={theme}>
154
- {`Submit review (${pendingCount}) ⏎`}
166
+ Submit review
155
167
  </Button>
156
- )}
157
- </>
158
- )}
159
- {/* the collapse chevron: muted, left-bound, one column from the divider.
160
- » points right - it hands the width back toward the plan */}
161
- {onCollapse ? (
162
- <box style={{ flexDirection: "row" }} onMouseUp={onCollapse}>
163
- <text fg={tokens.textDim}>»</text>
164
- </box>
165
- ) : null}
168
+ )
169
+ ) : null}
170
+ </box>
166
171
  </box>
167
172
  );
168
173
  });
@@ -41,7 +41,7 @@ export const NavZone: Story = {
41
41
  onRowActivate={() => {}}
42
42
  />
43
43
  ),
44
- expectedColors: [DARK.accent, DARK.backdrop],
44
+ expectedColors: [DARK.accent],
45
45
  size: { width: 100, height: 30 },
46
46
  };
47
47
 
@@ -57,6 +57,7 @@ export function SettingsDialog({
57
57
  title=" Settings "
58
58
  width={Math.min(76, terminalWidth - 6)}
59
59
  height={Math.min(22, terminalHeight - 4)}
60
+ background={tokens.elevated}
60
61
  theme={theme}
61
62
  >
62
63
  <box style={{ flexDirection: "row", flexGrow: 1 }}>
@@ -67,7 +68,7 @@ export function SettingsDialog({
67
68
  return (
68
69
  <box
69
70
  key={candidate.id}
70
- style={{ backgroundColor: isActive && activeZone === "nav" ? tokens.elevated : undefined }}
71
+ style={{ backgroundColor: isActive && activeZone === "nav" ? tokens.border : undefined }}
71
72
  onMouseUp={() => onCategorySelect(candidate.id)}
72
73
  >
73
74
  <text fg={isActive ? tokens.accent : tokens.textMuted}>{`${isActive ? "› " : " "}${candidate.name}`}</text>
@@ -120,10 +121,10 @@ export function SettingsDialog({
120
121
  })}
121
122
  </box>
122
123
  </box>
123
- <box style={{ flexDirection: "row", height: 1, backgroundColor: tokens.elevated, paddingLeft: 1 }}>
124
+ <box style={{ flexDirection: "row", height: 1, paddingLeft: 1 }}>
124
125
  <text fg={tokens.textDim}>
125
126
  {activeZone === "nav"
126
- ? "j/k category · l/⏎ into settings · esc close"
127
+ ? "j/k category · l/enter into settings · esc close"
127
128
  : "j/k row · l/space change · h back · esc close"}
128
129
  </text>
129
130
  </box>
@@ -14,5 +14,5 @@ export const AllKinds: Story = {
14
14
  <TextRow label="Vault path" value="~/vaults/notes" isActive={false} onPress={() => {}} />
15
15
  </box>
16
16
  ),
17
- expectedColors: [DARK.green, DARK.accent, DARK.elevated],
17
+ expectedColors: [DARK.green, DARK.accent, DARK.border],
18
18
  };
@@ -29,7 +29,7 @@ function RowShell({ label, isActive, onPress, theme, children }: RowShellProps):
29
29
  const tokens = useComponentTheme(theme);
30
30
  return (
31
31
  <box
32
- style={{ flexDirection: "row", height: 1, backgroundColor: isActive ? tokens.elevated : undefined }}
32
+ style={{ flexDirection: "row", height: 1, backgroundColor: isActive ? tokens.border : undefined }}
33
33
  onMouseUp={onPress}
34
34
  >
35
35
  <box style={{ width: 26, paddingLeft: 1 }}>
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { DARK } from "../theme";
3
+ import type { Story, StoryMeta } from "./story";
4
+ import { Toast } from "./Toast";
5
+
6
+ export const meta: StoryMeta = { title: "Toast" };
7
+
8
+ export const ShareLink: Story = {
9
+ render: () => <Toast title="share link copied" body="ssh review-4f2a@cueloop.dev" />,
10
+ expectedColors: [DARK.accent],
11
+ size: { width: 60, height: 12 },
12
+ };
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { useTerminalDimensions } from "@opentui/react";
3
+ import type { Theme } from "../theme";
4
+ import { useComponentTheme } from "./theme-context";
5
+ import { Dialog } from "./primitives/Dialog";
6
+
7
+ export interface ToastProps {
8
+ title?: string;
9
+ body: string;
10
+ theme?: Theme;
11
+ }
12
+
13
+ export function Toast({ title, body, theme }: ToastProps): React.ReactNode {
14
+ const tokens = useComponentTheme(theme);
15
+ const { width: terminalWidth } = useTerminalDimensions();
16
+ const width = Math.min(terminalWidth - 6, Math.max(28, body.length + 4));
17
+ return (
18
+ <Dialog isOpen title={title ? ` ${title} ` : undefined} width={width} height={6} theme={theme}>
19
+ <box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 1, paddingRight: 1, paddingTop: 1 }}>
20
+ <text fg={tokens.text}>{body}</text>
21
+ <box style={{ flexGrow: 1 }} />
22
+ <box style={{ flexDirection: "row" }}>
23
+ <box style={{ flexGrow: 1 }} />
24
+ <text fg={tokens.textDim}>[esc]</text>
25
+ </box>
26
+ </box>
27
+ </Dialog>
28
+ );
29
+ }
@@ -40,7 +40,7 @@ export const FirstFile: Story = {
40
40
  />
41
41
  ),
42
42
  // the main card wears the accent border; stats carry the +/- colors
43
- expectedColors: [DARK.accent, DARK.green, DARK.red, DARK.insFg, DARK.delFg],
43
+ expectedColors: [DARK.accent, DARK.green, DARK.red, DARK.insertedForeground, DARK.deletedForeground],
44
44
  };
45
45
 
46
46
  export const WithAgentNote: Story = {
@@ -118,7 +118,7 @@ export function WalkWizard({
118
118
  <text
119
119
  key={previewIndex}
120
120
  style={{ wrapMode: "none" }}
121
- fg={previewLine.sign === "+" ? tokens.insFg : tokens.delFg}
121
+ fg={previewLine.sign === "+" ? tokens.insertedForeground : tokens.deletedForeground}
122
122
  >
123
123
  {previewLine.sign + previewLine.text}
124
124
  </text>