@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/pty.test.ts CHANGED
@@ -11,6 +11,7 @@ function runOnPty(file: string, args: string[]): Promise<{ output: string; exitC
11
11
  const pty = spawn(file, args, { name: "xterm-256color", cols: 80, rows: 24 });
12
12
  let output = "";
13
13
  const timeout = setTimeout(() => reject(new Error("pty child never exited")), 5000);
14
+
14
15
  pty.onData((chunk) => {
15
16
  output += chunk;
16
17
  });
@@ -35,6 +36,7 @@ describe("pty", () => {
35
36
  // Arrange - `cat` echoes stdin back through the tty until it closes
36
37
  const pty = spawn("cat", [], { name: "xterm-256color", cols: 80, rows: 24 });
37
38
  let output = "";
39
+
38
40
  pty.onData((chunk) => {
39
41
  output += chunk;
40
42
  });
@@ -63,6 +65,7 @@ describe("pty", () => {
63
65
  });
64
66
  let collected = "";
65
67
  const timeout = setTimeout(() => reject(new Error("no exit")), 5000);
68
+
66
69
  pty.onData((chunk) => {
67
70
  collected += chunk;
68
71
  });
package/src/pty.ts CHANGED
@@ -68,14 +68,17 @@ function nativeLibraryPath(): string | null {
68
68
  const suffix = process.platform === "darwin" ? "dylib" : "so";
69
69
  const dir = `${process.platform}-${process.arch}`;
70
70
  const path = join(import.meta.dir, "..", "native", dir, `libcuelooppty.${suffix}`);
71
+
71
72
  return existsSync(path) ? path : null;
72
73
  }
73
74
 
74
75
  /** One shared load of the pty shim; null when no dylib ships for the platform. */
75
76
  let ptyLib: PtyLib | null | undefined;
77
+
76
78
  function library(): PtyLib | null {
77
79
  if (ptyLib !== undefined) return ptyLib;
78
80
  const path = nativeLibraryPath();
81
+
79
82
  if (!path) return (ptyLib = null);
80
83
  try {
81
84
  ptyLib = dlopen(path, PTY_SYMBOLS).symbols;
@@ -83,6 +86,7 @@ function library(): PtyLib | null {
83
86
  console.error(`cueloop: failed to load ${path}:`, error);
84
87
  ptyLib = null;
85
88
  }
89
+
86
90
  return ptyLib;
87
91
  }
88
92
 
@@ -95,9 +99,11 @@ class EventEmitter<T> {
95
99
  private listeners: ((value: T) => void)[] = [];
96
100
  readonly event = (listener: (value: T) => void): Disposable => {
97
101
  this.listeners.push(listener);
102
+
98
103
  return {
99
104
  dispose: () => {
100
105
  const index = this.listeners.indexOf(listener);
106
+
101
107
  if (index !== -1) this.listeners.splice(index, 1);
102
108
  },
103
109
  };
@@ -138,6 +144,7 @@ class Pty implements IPty {
138
144
 
139
145
  const argvPacked = packTokens([file, ...args]);
140
146
  const envPacked = packTokens(env);
147
+
141
148
  this.handle = lib.cueloop_pty_spawn(
142
149
  ptr(argvPacked),
143
150
  Buffer.from(`${cwd}\0`, "utf8"),
@@ -170,6 +177,7 @@ class Pty implements IPty {
170
177
  write(data: string): void {
171
178
  if (this.closing) return;
172
179
  const buffer = Buffer.from(data, "utf8");
180
+
173
181
  this.lib.cueloop_pty_write(this.handle, ptr(buffer), buffer.length);
174
182
  }
175
183
 
@@ -192,18 +200,23 @@ class Pty implements IPty {
192
200
  if (this.reading) return;
193
201
  this.reading = true;
194
202
  const buffer = Buffer.allocUnsafe(READ_BUFFER_BYTES);
203
+
195
204
  while (!this.closing) {
196
205
  const count = this.lib.cueloop_pty_read(this.handle, ptr(buffer), buffer.length);
206
+
197
207
  if (count > 0) {
198
208
  // Stream mode buffers a multibyte char split across reads (box-drawing etc.).
199
209
  const text = this.decoder.decode(buffer.subarray(0, count), { stream: true });
210
+
200
211
  if (text) this.dataEvent.fire(text);
201
212
  } else if (count === CHILD_EXITED || count < 0) {
202
213
  // Both the drained-exit sentinel and a hard read error end the session:
203
214
  // flush the decoder, reap for the code, close, and fire the exit once.
204
215
  const tail = this.decoder.decode();
216
+
205
217
  if (tail) this.dataEvent.fire(tail);
206
218
  const exitCode = this.lib.cueloop_pty_get_exit_code(this.handle);
219
+
207
220
  this.lib.cueloop_pty_close(this.handle);
208
221
  this.closing = true;
209
222
  this.exitEvent.fire({ exitCode });
@@ -217,6 +230,8 @@ class Pty implements IPty {
217
230
  /** Spawn `file` with `args` on a fresh PTY. Throws when no shim ships (gate on ptyAvailable). */
218
231
  export function spawn(file: string, args: string[], options: PtyForkOptions = {}): IPty {
219
232
  const lib = library();
233
+
220
234
  if (!lib) throw new Error("cueloop: no pty shim for this platform");
235
+
221
236
  return new Pty(lib, file, args, options);
222
237
  }
package/src/run.ts CHANGED
@@ -19,6 +19,7 @@ export async function runClient(options: RunClientOptions): Promise<number> {
19
19
  const renderer = await createCliRenderer({ enableMouseMovement: true });
20
20
  const appearance =
21
21
  (await renderer.waitForThemeMode(THEME_QUERY_TIMEOUT_MS).catch(() => null)) ?? "dark";
22
+
22
23
  return new Promise<number>((resolve) => {
23
24
  createRoot(renderer).render(
24
25
  React.createElement(App, {
package/src/serve.test.ts CHANGED
@@ -56,6 +56,7 @@ function sshCapture(
56
56
  else resolve(out);
57
57
  };
58
58
  const timer = setTimeout(() => finish(new Error(`timed out; captured:\n${out}`)), timeoutMs);
59
+
59
60
  conn
60
61
  .on("ready", () => {
61
62
  conn.shell({ term: "xterm-256color", cols: 100, rows: 30 }, (err, stream) => {
package/src/serve.ts CHANGED
@@ -50,6 +50,7 @@ export interface ServeHandle {
50
50
  export async function serveClient(options: ServeOptions = {}): Promise<ServeHandle> {
51
51
  const home = options.home ?? cueloopHome();
52
52
  const sshDir = join(home, "ssh");
53
+
53
54
  mkdirSync(sshDir, { recursive: true, mode: 0o700 });
54
55
 
55
56
  const server = createServer({
@@ -65,6 +66,7 @@ export async function serveClient(options: ServeOptions = {}): Promise<ServeHand
65
66
  const appearance =
66
67
  (await session.renderer.waitForThemeMode(THEME_QUERY_TIMEOUT_MS).catch(() => null)) ?? "dark";
67
68
  const root = createRoot(session.renderer);
69
+
68
70
  root.render(
69
71
  React.createElement(App, {
70
72
  home,
@@ -79,5 +81,6 @@ export async function serveClient(options: ServeOptions = {}): Promise<ServeHand
79
81
  });
80
82
 
81
83
  const info = await server.listen(options.port ?? 2222, options.host ?? "127.0.0.1");
84
+
82
85
  return { ...info, close: () => server.close() };
83
86
  }
@@ -46,6 +46,7 @@ function fakeClient(session: ReviewSession, sink: { workingCopy?: string }): Ses
46
46
  sessionList: async () => [session],
47
47
  sessionSetWorkingCopy: mock(async (_id: string, content: string | undefined) => {
48
48
  sink.workingCopy = content;
49
+
49
50
  return { ...session, workingCopy: content };
50
51
  }),
51
52
  close: () => {},
@@ -61,8 +62,10 @@ async function connected(session: ReviewSession) {
61
62
  sessionId: session.id,
62
63
  openClient: async () => client,
63
64
  });
65
+
64
66
  controller.connect();
65
67
  await tick();
68
+
66
69
  return { controller, client, sink };
67
70
  }
68
71
 
@@ -132,6 +135,7 @@ describe("diff hunk curation", () => {
132
135
 
133
136
  // Assert - one item: diff source, revealing the change's row, both lines previewed
134
137
  const items = controller.curationItems();
138
+
135
139
  expect(items.length).toBe(1);
136
140
  expect(items[0]!.source).toBe("diff");
137
141
  expect(items[0]!.id).toBe("diff:src/store.ts#0#1");
@@ -152,6 +156,7 @@ describe("diff hunk curation", () => {
152
156
 
153
157
  // Assert - the whole hunk, id diff:path#hunk#hunk
154
158
  const items = controller.curationItems();
159
+
155
160
  expect(items.length).toBe(1);
156
161
  expect(items[0]!.source).toBe("diff");
157
162
  expect(items[0]!.id).toBe("diff:src/store.ts#0#hunk");
@@ -160,6 +165,7 @@ describe("diff hunk curation", () => {
160
165
  test("restoreCuration drops the diff removal and reverts the working copy", async () => {
161
166
  // Arrange - one change rejected
162
167
  const { controller, sink } = await connected(diffSession(FILES));
168
+
163
169
  controller.toggleRejectChange(4);
164
170
  await tick();
165
171
  const [item] = controller.curationItems();
@@ -178,6 +184,7 @@ describe("diff hunk curation", () => {
178
184
  test("restoreCuration ignores an unknown id", async () => {
179
185
  // Arrange - one change rejected
180
186
  const { controller } = await connected(diffSession(FILES));
187
+
181
188
  controller.toggleRejectChange(4);
182
189
  await tick();
183
190
 
@@ -11,6 +11,7 @@ const pullShare = mock(async () => remote);
11
11
  const pushShare = mock(
12
12
  async (_shareId: string, _annotations: Array<Omit<Annotation, "createdAt">>) => {},
13
13
  );
14
+
14
15
  mock.module("./share", () => ({
15
16
  publishShare,
16
17
  pullShare,
@@ -60,8 +61,10 @@ function fakeClient(session: ReviewSession): SessionClient {
60
61
  ),
61
62
  sessionMergeShared: mock(async (_id: string, incoming: { annotations: Annotation[] }) => {
62
63
  const known = new Set(session.annotations.map((existing) => existing.id));
64
+
63
65
  for (const note of incoming.annotations)
64
66
  if (!known.has(note.id)) session.annotations.push(note);
67
+
65
68
  return session;
66
69
  }),
67
70
  close: () => {},
@@ -80,8 +83,10 @@ async function connectedController(
80
83
  openClient: async () => client,
81
84
  clock,
82
85
  });
86
+
83
87
  controller.connect();
84
88
  await tick();
89
+
85
90
  return { controller, client };
86
91
  }
87
92
 
@@ -199,6 +204,7 @@ describe("mirror on annotate", () => {
199
204
  const { controller, client } = await connectedController(
200
205
  sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
201
206
  );
207
+
202
208
  (client.sessionAnnotate as ReturnType<typeof mock>).mockImplementationOnce(async () => {
203
209
  throw new Error("session is resolved");
204
210
  });
@@ -225,6 +231,7 @@ describe("startSharePoll", () => {
225
231
 
226
232
  // Act - immediate pull
227
233
  const stop = controller.startSharePoll();
234
+
228
235
  await tick();
229
236
 
230
237
  // Assert
@@ -251,6 +258,7 @@ describe("startSharePoll", () => {
251
258
  pullShare.mockClear();
252
259
  remote = sessionFixture({ annotations: [] });
253
260
  let release: () => void = () => {};
261
+
254
262
  pullShare.mockImplementationOnce(
255
263
  () => new Promise<ReviewSession>((resolve) => (release = () => resolve(remote))),
256
264
  );
@@ -262,6 +270,7 @@ describe("startSharePoll", () => {
262
270
 
263
271
  // Act - start (pull is in flight), leave, then let the pull settle
264
272
  const stop = controller.startSharePoll();
273
+
265
274
  await tick();
266
275
  expect(pullShare).toHaveBeenCalledTimes(1);
267
276
  stop();
@@ -279,6 +288,7 @@ describe("startSharePoll", () => {
279
288
  pullShare.mockClear();
280
289
  remote = sessionFixture({ annotations: [] });
281
290
  let release: () => void = () => {};
291
+
282
292
  pullShare.mockImplementationOnce(
283
293
  () => new Promise<ReviewSession>((resolve) => (release = () => resolve(remote))),
284
294
  );
@@ -293,6 +303,7 @@ describe("startSharePoll", () => {
293
303
  await tick();
294
304
  expect(pullShare).toHaveBeenCalledTimes(1);
295
305
  const stopB = controller.startSharePoll();
306
+
296
307
  await tick();
297
308
  expect(pullShare).toHaveBeenCalledTimes(2);
298
309