cueloop 0.1.0-alpha.73 → 0.1.0-alpha.74
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/main.ts +26 -35
- package/src/pr.ts +2 -2
- package/src/share-command.test.ts +37 -0
- package/src/share-command.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueloop",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.74",
|
|
4
4
|
"description": "Terminal review surface for coding agents: annotate plans and diffs, return a verdict, send structured feedback back",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"build:binary": "bun build --compile --external cpu-features ./src/main.ts --outfile dist/cueloop"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@cueloop/adapters": "0.1.0-alpha.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
33
|
+
"@cueloop/adapters": "0.1.0-alpha.74",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.74",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.74",
|
|
36
|
+
"@cueloop/schema": "0.1.0-alpha.74",
|
|
37
37
|
"valibot": "^1.4.2"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
package/src/main.ts
CHANGED
|
@@ -23,9 +23,8 @@ import {
|
|
|
23
23
|
import { sessionCommand } from "./session-commands";
|
|
24
24
|
import { CLI_VERSION } from "./version";
|
|
25
25
|
import { DaemonClient } from "@cueloop/daemon/client";
|
|
26
|
-
import { workingTreeDiff } from "@cueloop/daemon/working-tree";
|
|
27
26
|
import type { Thread } from "@cueloop/schema";
|
|
28
|
-
import { openReview
|
|
27
|
+
import { openReview } from "@cueloop/daemon/review";
|
|
29
28
|
|
|
30
29
|
const argv = process.argv.slice(2);
|
|
31
30
|
const cmd = argv[0];
|
|
@@ -167,6 +166,7 @@ async function openReviewOfKind(
|
|
|
167
166
|
match: (session: Thread) => boolean,
|
|
168
167
|
label: string,
|
|
169
168
|
selector: string | undefined,
|
|
169
|
+
layout: "review" | "plan" | undefined,
|
|
170
170
|
emptyMessage?: string,
|
|
171
171
|
): Promise<number> {
|
|
172
172
|
const client = await DaemonClient.connect({ autostart: true });
|
|
@@ -179,7 +179,7 @@ async function openReviewOfKind(
|
|
|
179
179
|
}
|
|
180
180
|
const target = resolveOpenTarget(sessions, { match, selector });
|
|
181
181
|
|
|
182
|
-
if (target.kind === "session") return runTui(target.sessionId);
|
|
182
|
+
if (target.kind === "session") return runTui(target.sessionId, layout);
|
|
183
183
|
if (target.kind === "no-pending" && emptyMessage !== undefined) {
|
|
184
184
|
console.error(emptyMessage);
|
|
185
185
|
|
|
@@ -192,7 +192,7 @@ async function openReviewOfKind(
|
|
|
192
192
|
|
|
193
193
|
/** `cueloop plan [id|title]` - open the latest pending plan, or address one. */
|
|
194
194
|
async function planCommand(argv: string[]): Promise<number> {
|
|
195
|
-
return openReviewOfKind(isPlanReview, "plan", openSelector(parseArgs(argv)));
|
|
195
|
+
return openReviewOfKind(isPlanReview, "plan", openSelector(parseArgs(argv)), "plan");
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/**
|
|
@@ -201,7 +201,7 @@ async function planCommand(argv: string[]): Promise<number> {
|
|
|
201
201
|
* by the /cueloop:reply skill; the opener is scope-only, like plan.
|
|
202
202
|
*/
|
|
203
203
|
async function replyCommand(argv: string[]): Promise<number> {
|
|
204
|
-
return openReviewOfKind(isReplyReview, "reply", openSelector(parseArgs(argv)));
|
|
204
|
+
return openReviewOfKind(isReplyReview, "reply", openSelector(parseArgs(argv)), "plan");
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -221,7 +221,7 @@ async function prototypeCommand(argv: string[]): Promise<number> {
|
|
|
221
221
|
selector !== undefined && !isSessionId(selector) && (isHtmlPrototype || isMarkdownPrototype);
|
|
222
222
|
|
|
223
223
|
if (wantsOpen || !looksLikeFile)
|
|
224
|
-
return openReviewOfKind(isPrototypeReview, "prototype", selector);
|
|
224
|
+
return openReviewOfKind(isPrototypeReview, "prototype", selector, "plan");
|
|
225
225
|
|
|
226
226
|
const path = resolve(selector);
|
|
227
227
|
const content = await Bun.file(path)
|
|
@@ -244,47 +244,30 @@ async function prototypeCommand(argv: string[]): Promise<number> {
|
|
|
244
244
|
|
|
245
245
|
client.close();
|
|
246
246
|
|
|
247
|
-
return runTui(review.id);
|
|
247
|
+
return runTui(review.id, "plan");
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* `cueloop diff`
|
|
251
|
+
* `cueloop diff` opens the repo's workbench in the review layout:
|
|
252
252
|
* - a selector (`cueloop diff <id|title>`) or an explicit `--open`/`--latest`
|
|
253
|
-
* opens a pending diff
|
|
254
|
-
* -
|
|
255
|
-
*
|
|
256
|
-
*
|
|
253
|
+
* opens a specific pending diff thread;
|
|
254
|
+
* - a bare `cueloop diff` find-or-creates the per-repo workbench thread - a live-diff
|
|
255
|
+
* annotation container - so it is immediately annotatable and always shows the
|
|
256
|
+
* current working tree, reusing the same thread across re-runs.
|
|
257
257
|
*/
|
|
258
258
|
async function diffCommand(argv: string[]): Promise<number> {
|
|
259
259
|
const parsed = parseArgs(argv);
|
|
260
260
|
const selector = openSelector(parsed);
|
|
261
261
|
const wantsOpen = selector !== undefined || "open" in parsed.flags || "latest" in parsed.flags;
|
|
262
262
|
|
|
263
|
-
if (wantsOpen) return openReviewOfKind(isDiffReview, "diff", selector);
|
|
263
|
+
if (wantsOpen) return openReviewOfKind(isDiffReview, "diff", selector, "review");
|
|
264
264
|
|
|
265
|
-
const workspace = await resolveWorkspace();
|
|
266
|
-
const diff = await workingTreeDiff();
|
|
267
|
-
|
|
268
|
-
if (!diff.patch.trim()) {
|
|
269
|
-
return openReviewOfKind(
|
|
270
|
-
isDiffReview,
|
|
271
|
-
"diff",
|
|
272
|
-
undefined,
|
|
273
|
-
"working tree is clean and no pending diff review - nothing to open",
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
265
|
const client = await DaemonClient.connect({ autostart: true });
|
|
277
|
-
const
|
|
278
|
-
type: "diff",
|
|
279
|
-
content: diff.patch,
|
|
280
|
-
files: diff.files,
|
|
281
|
-
workspace,
|
|
282
|
-
title: `working tree @ ${workspace.branch}`,
|
|
283
|
-
});
|
|
266
|
+
const workbench = await client.sessionWorkbench(process.cwd());
|
|
284
267
|
|
|
285
268
|
client.close();
|
|
286
269
|
|
|
287
|
-
return runTui(
|
|
270
|
+
return runTui(workbench.id, "review");
|
|
288
271
|
}
|
|
289
272
|
|
|
290
273
|
/**
|
|
@@ -310,13 +293,21 @@ async function reviewEntry(argv: string[]): Promise<number> {
|
|
|
310
293
|
isPrReview,
|
|
311
294
|
"PR",
|
|
312
295
|
explicitOpen || looksLikeSessionId ? selector : undefined,
|
|
296
|
+
"review",
|
|
313
297
|
);
|
|
314
298
|
}
|
|
315
299
|
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
/**
|
|
301
|
+
* A create-command dictates the pane composition it opens in; a bare launch (`layout`
|
|
302
|
+
* omitted) restores the remembered one. Resolve the factory here so the heavy client
|
|
303
|
+
* module stays lazily imported for non-TUI commands.
|
|
304
|
+
*/
|
|
305
|
+
async function runTui(sessionId?: string, layout?: "review" | "plan"): Promise<number> {
|
|
306
|
+
const { runClient, reviewLayout, planLayout } = await import("@cueloop/client");
|
|
307
|
+
const resolved =
|
|
308
|
+
layout === "review" ? reviewLayout() : layout === "plan" ? planLayout() : undefined;
|
|
318
309
|
|
|
319
|
-
return runClient({ sessionId });
|
|
310
|
+
return runClient({ sessionId, layout: resolved });
|
|
320
311
|
}
|
|
321
312
|
|
|
322
313
|
function printHelp(): void {
|
package/src/pr.ts
CHANGED
|
@@ -80,9 +80,9 @@ export async function reviewCommand(argv: string[]): Promise<number> {
|
|
|
80
80
|
return 0;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const { runClient } = await import("@cueloop/client");
|
|
83
|
+
const { runClient, reviewLayout } = await import("@cueloop/client");
|
|
84
84
|
|
|
85
|
-
await runClient({ sessionId: session.id });
|
|
85
|
+
await runClient({ sessionId: session.id, layout: reviewLayout() });
|
|
86
86
|
|
|
87
87
|
const after = await getSession(session.id);
|
|
88
88
|
|
|
@@ -133,6 +133,43 @@ describe(shareSession, () => {
|
|
|
133
133
|
expect(deps.lines).toEqual(["share link copied - ssh p_abc123xy@cueloop.dev"]);
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
test("freezes a workbench thread's live diff into the shared artifact", async () => {
|
|
137
|
+
// Arrange - a workbench thread (renders live locally) plus a daemon that reports a fresh diff
|
|
138
|
+
const workbench = sessionFixture("ses_wb", {
|
|
139
|
+
artifact: {
|
|
140
|
+
type: "diff",
|
|
141
|
+
content: "STALE",
|
|
142
|
+
files: [],
|
|
143
|
+
meta: { workbench: true, title: "Workbench" },
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const client = {
|
|
147
|
+
...fakeClient([workbench]),
|
|
148
|
+
repoDiff: async () => ({
|
|
149
|
+
patch: "FRESH",
|
|
150
|
+
files: [{ path: "x.ts", oldContents: "", newContents: "y\n", status: "added" as const }],
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
let published: Thread | undefined;
|
|
154
|
+
const deps = depsSpy({
|
|
155
|
+
publish: mock(async (session: Thread) => {
|
|
156
|
+
published = session;
|
|
157
|
+
|
|
158
|
+
return { line: "ssh p_abc123xy@cueloop.dev", copied: true };
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Act
|
|
163
|
+
const code = await shareSession(client, { sessionId: "ses_wb" }, deps);
|
|
164
|
+
|
|
165
|
+
// Assert - the remote gets the fresh snapshot, marked so it renders frozen while keeping its notes
|
|
166
|
+
expect(code).toBe(0);
|
|
167
|
+
expect(published?.artifact.content).toBe("FRESH");
|
|
168
|
+
expect(published?.artifact.files).toHaveLength(1);
|
|
169
|
+
expect(published?.artifact.meta.snapshot).toBe(true);
|
|
170
|
+
expect(published?.artifact.meta.workbench).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
136
173
|
test("without an id, shares the most recent session", async () => {
|
|
137
174
|
// Arrange
|
|
138
175
|
const publish = mock(
|
package/src/share-command.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
publishShare,
|
|
13
13
|
pullShare,
|
|
14
14
|
shareIdFromLine,
|
|
15
|
+
snapshotWorkbench,
|
|
15
16
|
type ShareResult,
|
|
16
17
|
type ShareTarget,
|
|
17
18
|
} from "@cueloop/client";
|
|
@@ -81,7 +82,11 @@ export async function shareSession(
|
|
|
81
82
|
return 1;
|
|
82
83
|
}
|
|
83
84
|
const session = params.fork ? await client.sessionFork(picked.id) : picked;
|
|
84
|
-
|
|
85
|
+
// a workbench thread renders live locally; freeze its diff so the remote reviewer gets a stable snapshot
|
|
86
|
+
const shared = client.repoDiff
|
|
87
|
+
? await snapshotWorkbench(session, client.repoDiff.bind(client))
|
|
88
|
+
: session;
|
|
89
|
+
const { line, copied } = await deps.publish(shared, { host: params.host, port: params.port });
|
|
85
90
|
const shareId = shareIdFromLine(line);
|
|
86
91
|
|
|
87
92
|
if (shareId) await client.sessionSetShareId(session.id, shareId);
|