@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.
- package/package.json +5 -5
- package/src/App.autoclose.test.tsx +7 -0
- package/src/App.diff.test.tsx +11 -0
- package/src/App.export.test.tsx +5 -0
- package/src/App.plan-review.test.tsx +42 -0
- package/src/App.prototype.test.tsx +11 -0
- package/src/App.readonly.test.tsx +10 -0
- package/src/App.rendering.test.tsx +4 -0
- package/src/App.review-panel.test.tsx +7 -0
- package/src/App.submit-confirm.test.tsx +11 -0
- package/src/App.test.tsx +13 -0
- package/src/App.toast.test.tsx +2 -0
- package/src/App.tsx +27 -0
- package/src/App.walk.test.tsx +10 -0
- package/src/app-key-state.ts +1 -0
- package/src/app-screens.tsx +4 -0
- package/src/app-view-model.ts +21 -0
- package/src/attribution.ts +4 -0
- package/src/clipboard.ts +3 -0
- package/src/components/AnnotationCard.tsx +9 -0
- package/src/components/Breadcrumb.tsx +1 -0
- package/src/components/CodeBlock.tsx +1 -0
- package/src/components/CompletionOverlay.tsx +1 -0
- package/src/components/ConfirmCard.tsx +2 -0
- package/src/components/ConfirmDialog.tsx +2 -0
- package/src/components/DiffSheet.scroll.test.tsx +12 -0
- package/src/components/DiffSheet.syntax.test.tsx +3 -0
- package/src/components/DiffSheet.tsx +19 -0
- package/src/components/InboxList.tsx +2 -0
- package/src/components/KeybindsDialog.tsx +1 -0
- package/src/components/MarkerPopover.tsx +1 -0
- package/src/components/MenuBar.tsx +1 -0
- package/src/components/PlanSheet.tsx +26 -0
- package/src/components/PromptDialog.tsx +2 -0
- package/src/components/PrototypeSheet.tsx +27 -0
- package/src/components/ReviewPanel.tsx +2 -0
- package/src/components/ReviewRail.tsx +1 -0
- package/src/components/SettingsDialog.tsx +6 -0
- package/src/components/SettingsRows.tsx +4 -0
- package/src/components/Toast.tsx +1 -0
- package/src/components/agent-launcher.test.tsx +1 -0
- package/src/components/agent-launcher.tsx +2 -0
- package/src/components/diff-sheet-layout.test.ts +1 -0
- package/src/components/diff-sheet-layout.ts +17 -0
- package/src/components/plan-sheet-run-style.ts +4 -0
- package/src/components/primitives/Button.tsx +1 -0
- package/src/components/primitives/Card.tsx +1 -0
- package/src/components/primitives/Dialog.tsx +2 -0
- package/src/components/primitives/StatusBar.tsx +1 -0
- package/src/components/primitives/Tabs.tsx +4 -0
- package/src/components/quick-actions-editor.tsx +2 -0
- package/src/components/stories-app.tsx +1 -0
- package/src/components/stories.test.tsx +4 -0
- package/src/components/story.ts +6 -0
- package/src/components/syntax-highlight.ts +10 -0
- package/src/components/terminal-pane.ts +12 -0
- package/src/components/theme-context.tsx +1 -0
- package/src/components/truncate-text.ts +1 -0
- package/src/config.test.ts +18 -0
- package/src/config.ts +27 -0
- package/src/diff-hunk-curate.ts +20 -0
- package/src/diff-intraline.test.ts +1 -0
- package/src/diff-intraline.ts +14 -0
- package/src/diff-syntax.ts +22 -0
- package/src/editor.test.ts +6 -0
- package/src/editor.ts +12 -0
- package/src/ghostty-terminal.test.ts +2 -0
- package/src/ghostty-terminal.ts +8 -0
- package/src/herdr.test.ts +2 -0
- package/src/herdr.ts +3 -0
- package/src/integrations.ts +2 -0
- package/src/intent-dispatch.test.ts +6 -0
- package/src/intent-dispatch.ts +37 -0
- package/src/key-bindings.test.ts +4 -0
- package/src/key-bindings.ts +22 -0
- package/src/keymap.test.ts +13 -0
- package/src/keymap.ts +43 -0
- package/src/kitty-image.ts +9 -0
- package/src/prototype-browser.test.ts +1 -0
- package/src/prototype-browser.ts +22 -0
- package/src/pty.test.ts +3 -0
- package/src/pty.ts +15 -0
- package/src/run.ts +1 -0
- package/src/serve.test.ts +1 -0
- package/src/serve.ts +3 -0
- package/src/session-controller.curate.test.ts +7 -0
- package/src/session-controller.share.test.ts +11 -0
- package/src/session-controller.ts +79 -0
- package/src/share.ts +7 -0
- package/src/test-support.test.ts +5 -0
- package/src/test-support.ts +10 -0
- package/src/theme-presets.ts +1 -0
- package/src/use-settings-dialog.tsx +12 -0
- package/src/view-diff.test.ts +3 -0
- package/src/view-diff.ts +5 -0
- package/src/view-plan.test.ts +7 -0
- package/src/view-plan.ts +59 -0
- 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
|
|