cueloop 0.1.0-alpha.73 → 0.1.0-alpha.75

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cueloop",
3
- "version": "0.1.0-alpha.73",
3
+ "version": "0.1.0-alpha.75",
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.73",
34
- "@cueloop/client": "0.1.0-alpha.73",
35
- "@cueloop/daemon": "0.1.0-alpha.73",
36
- "@cueloop/schema": "0.1.0-alpha.73",
33
+ "@cueloop/adapters": "0.1.0-alpha.75",
34
+ "@cueloop/client": "0.1.0-alpha.75",
35
+ "@cueloop/daemon": "0.1.0-alpha.75",
36
+ "@cueloop/schema": "0.1.0-alpha.75",
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, resolveWorkspace } from "@cueloop/daemon/review";
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` disambiguates create from open by intent:
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 review;
254
- * - otherwise a dirty working tree still creates a review as before;
255
- * - a clean working tree carries no create input, so it opens the latest
256
- * pending diff review instead of erroring.
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 review = await openReview(client, {
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(review.id);
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
- async function runTui(sessionId?: string): Promise<number> {
317
- const { runClient } = await import("@cueloop/client");
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(
@@ -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
- const { line, copied } = await deps.publish(session, { host: params.host, port: params.port });
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);