@cueloop/client 0.1.0-alpha.82 → 0.1.0-alpha.84

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.
@@ -363,13 +363,13 @@ exports[`stories catalog Overlays/CompletionOverlay/FeedbackCounting renders, sn
363
363
  `;
364
364
 
365
365
  exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, and carries its colors: Cards/ConfirmCard/ApproveDefault 1`] = `
366
- "┌────────────────────────────────┐
367
- │ [Approve] Changes │
368
- │ │
369
- │ summary for the agent ( │
370
- │ │
371
- │ send cancel │
372
- └────────────────────────────────┘
366
+ "┌────────────────────────────────────┐
367
+ │ [comment] [approve] [changes] │
368
+ │ │
369
+ │ summary for the agent (optional) │
370
+ │ │
371
+ │ send cancel │
372
+ └────────────────────────────────────┘
373
373
 
374
374
 
375
375
 
@@ -381,13 +381,13 @@ exports[`stories catalog Cards/ConfirmCard/ApproveDefault renders, snapshots, an
381
381
  `;
382
382
 
383
383
  exports[`stories catalog Cards/ConfirmCard/RequestChanges renders, snapshots, and carries its colors: Cards/ConfirmCard/RequestChanges 1`] = `
384
- "┌────────────────────────────────┐
385
- │ Approve [Changes] │
386
- │ │
387
- │ Tighten the steps. │
388
- │ │
389
- │ send cancel │
390
- └────────────────────────────────┘
384
+ "┌────────────────────────────────────┐
385
+ │ [comment] [approve] [changes] │
386
+ │ │
387
+ │ Tighten the steps. │
388
+ │ │
389
+ │ send cancel │
390
+ └────────────────────────────────────┘
391
391
 
392
392
 
393
393
 
@@ -399,15 +399,15 @@ exports[`stories catalog Cards/ConfirmCard/RequestChanges renders, snapshots, an
399
399
  `;
400
400
 
401
401
  exports[`stories catalog Cards/ConfirmCard/WithViewedSummary renders, snapshots, and carries its colors: Cards/ConfirmCard/WithViewedSummary 1`] = `
402
- "┌────────────────────────────────┐
403
- │ 2/3 files viewed │
404
- │ │
405
- │ [Approve] Changes │
406
- │ │
407
- │ summary for the agent ( │
408
- │ │
409
- │ send cancel │
410
- └────────────────────────────────┘
402
+ "┌────────────────────────────────────┐
403
+ │ 2/3 files viewed │
404
+ │ │
405
+ │ [comment] [approve] [changes] │
406
+ │ │
407
+ │ summary for the agent (optional) │
408
+ │ │
409
+ │ send cancel │
410
+ └────────────────────────────────────┘
411
411
 
412
412
 
413
413
 
package/src/config.ts CHANGED
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { OBSIDIAN_DEFAULTS, type ObsidianConfig } from "@cueloop/integration-obsidian";
12
- import type { MessageOutcome } from "@cueloop/schema";
12
+ import { MESSAGE_OUTCOMES, type MessageOutcome } from "@cueloop/schema";
13
13
  import { writeFileSync, mkdirSync } from "node:fs";
14
14
  import { dirname } from "node:path";
15
15
  import * as v from "valibot";
@@ -213,7 +213,7 @@ const UiSchema = v.object({
213
213
  editor: v.fallback(v.optional(v.string()), undefined),
214
214
  theme: v.fallback(v.optional(v.string()), undefined),
215
215
  diff_view: v.fallback(v.optional(v.picklist(["split", "stacked", "unified"])), undefined),
216
- default_message: v.fallback(v.optional(v.picklist(["approved", "changes_requested"])), undefined),
216
+ default_message: v.fallback(v.optional(v.picklist(MESSAGE_OUTCOMES)), undefined),
217
217
  pins: v.fallback(v.optional(v.array(v.string())), undefined),
218
218
  layout: v.fallback(
219
219
  v.optional(
@@ -276,7 +276,7 @@ describe("cycleMessage", () => {
276
276
  describe("marker-actions popover", () => {
277
277
  const span = { displayIndex: 3, wordIndex: 0, wordEnd: 0, start: 2, end: 9 };
278
278
 
279
- test("spanCut cuts the span's block and closes the popover", () => {
279
+ test("spanCut cuts only the marked character range and closes the popover", () => {
280
280
  // Arrange
281
281
  const deps = makeDeps({ mode: { type: "span", span } });
282
282
  const dispatch = createIntentDispatch(deps);
@@ -285,7 +285,7 @@ describe("marker-actions popover", () => {
285
285
  dispatch({ type: "spanCut" });
286
286
 
287
287
  // Assert
288
- expect(deps.controller.cut).toHaveBeenCalledWith(3);
288
+ expect(deps.controller.cut).toHaveBeenCalledWith(3, 2, 9);
289
289
  expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
290
290
  });
291
291
 
@@ -8,14 +8,13 @@
8
8
  */
9
9
 
10
10
  import type { Dispatch, MutableRefObject, SetStateAction } from "react";
11
- import { isAddressed, type Thread, type MessageOutcome } from "@cueloop/schema";
11
+ import { isAddressed, MESSAGE_OUTCOMES, type Thread, type MessageOutcome } from "@cueloop/schema";
12
12
  import { displayText, spanKey, startSpan, type DisplayBlock, type SpanState } from "./view-plan";
13
13
  import type { DiffRow } from "./view-diff";
14
14
  import type { ReviewController } from "./thread-controller";
15
15
  import type { Intent } from "./keymap";
16
16
  import type { TreeRow } from "./tree-view";
17
17
  import { quickActionBody, type QuickAction } from "./config";
18
- import { MESSAGE_OUTCOMES } from "./components/ConfirmCard";
19
18
 
20
19
  /** Which pane of the session tree / review the keyboard grammar is aimed at. */
21
20
  export type RailTab = "review" | "tree";
@@ -256,11 +255,10 @@ function handleSpanKey(intent: IntentOfType<"spanKey">, deps: IntentDispatchDeps
256
255
  }
257
256
 
258
257
  function handleSpanCut(_intent: IntentOfType<"spanCut">, deps: IntentDispatchDeps): void {
259
- // the block the span sits in, cut whole (partial-span cut is not modeled)
260
258
  const { mode } = deps;
261
259
 
262
260
  if (mode.type === "span") {
263
- deps.controller.cut(mode.span.displayIndex);
261
+ deps.controller.cut(mode.span.displayIndex, mode.span.start, mode.span.end);
264
262
  deps.setMode({ type: "normal" });
265
263
  }
266
264
  }
package/src/keymap.ts CHANGED
@@ -315,8 +315,6 @@ function spanGrammar(state: KeyState, name: string): Intent[] {
315
315
  if (state.resolved) return status("review submitted - read-only");
316
316
  if (name === "c") return [{ type: "openCompose", kind: "comment", from: "span" }];
317
317
  if (name === "a") return [{ type: "openSpanActions" }];
318
- // partial-span cut is not in the working-copy model, so cut removes the
319
- // whole block the span sits in; owner-only, like plan cut
320
318
  if (state.canEditPlan === false) return [];
321
319
 
322
320
  return [{ type: "spanCut" }];
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, mock, test } from "bun:test";
2
2
  import {
3
+ applyTextCuts,
3
4
  cutBlock,
4
5
  parseBlocks,
5
6
  restoreBlock,
@@ -7,6 +8,7 @@ import {
7
8
  type DiffFileContents,
8
9
  type HunkRejection,
9
10
  type Thread,
11
+ type TextCut,
10
12
  } from "@cueloop/schema";
11
13
  import { curateDiff } from "@cueloop/daemon/curate";
12
14
  import type { ThreadClient } from "@cueloop/daemon/client";
@@ -48,6 +50,7 @@ function diffSession(files?: DiffFileContents[]): Thread {
48
50
 
49
51
  interface WorkingCopySink {
50
52
  workingCopy?: string;
53
+ textCuts?: TextCut[];
51
54
  }
52
55
 
53
56
  const unimplemented = (member: string) => () =>
@@ -65,11 +68,15 @@ function fakeClient(initial: Thread, sink: WorkingCopySink): ThreadClient {
65
68
  sessionComment: unimplemented("sessionComment"),
66
69
  sessionAnnotate: unimplemented("sessionAnnotate"),
67
70
  sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
68
- sessionSetWorkingCopy: mock(async (_id: string, content: string | undefined) => {
69
- sink.workingCopy = content;
70
-
71
- return { ...session, workingCopy: content };
72
- }),
71
+ sessionSetWorkingCopy: mock(
72
+ async (_id: string, content: string | undefined, textCuts?: TextCut[]) => {
73
+ sink.workingCopy = content;
74
+ sink.textCuts = textCuts;
75
+ session = { ...session, workingCopy: content, textCuts };
76
+
77
+ return session;
78
+ },
79
+ ),
73
80
  // the daemon's block primitives, stood in for with the same pure helpers it uses
74
81
  sessionCutBlock: mock(async (_id: string, blockIndex: number) => {
75
82
  const working = session.workingCopy ?? session.artifact.content;
@@ -154,7 +161,7 @@ describe("diff hunk curation", () => {
154
161
  await tick();
155
162
 
156
163
  // Assert
157
- expect(controller.getSnapshot().status).toContain("change rejected");
164
+ expect(controller.getSnapshot().toast).toBeNull();
158
165
  expect(sink.workingCopy).toBe("");
159
166
  });
160
167
 
@@ -181,7 +188,7 @@ describe("diff hunk curation", () => {
181
188
  await tick();
182
189
 
183
190
  // Assert - no decisions left, so the working copy reverts to undefined
184
- expect(controller.getSnapshot().status).toContain("change restored");
191
+ expect(controller.getSnapshot().toast).toBeNull();
185
192
  expect(sink.workingCopy).toBeUndefined();
186
193
  expect(controller.rejectedRows().size).toBe(0);
187
194
  });
@@ -195,7 +202,7 @@ describe("diff hunk curation", () => {
195
202
  await tick();
196
203
 
197
204
  // Assert - the single hunk is the whole diff, so nothing remains
198
- expect(controller.getSnapshot().status).toContain("hunk rejected");
205
+ expect(controller.getSnapshot().toast).toBeNull();
199
206
  expect(sink.workingCopy).toBe("");
200
207
  });
201
208
 
@@ -249,7 +256,7 @@ describe("diff hunk curation", () => {
249
256
  await tick();
250
257
 
251
258
  // Assert - the list empties and the working copy reverts to the full diff
252
- expect(controller.getSnapshot().status).toContain("removal restored");
259
+ expect(controller.getSnapshot().toast).toBeNull();
253
260
  expect(controller.curationItems().length).toBe(0);
254
261
  expect(sink.workingCopy).toBeUndefined();
255
262
  expect(controller.rejectedRows().size).toBe(0);
@@ -355,7 +362,7 @@ describe("plan cut removals", () => {
355
362
  await tick();
356
363
 
357
364
  // Assert - restoring the only cut round-trips to the submitted revision
358
- expect(controller.getSnapshot().status).toContain("removal restored");
365
+ expect(controller.getSnapshot().toast).toBeNull();
359
366
  expect(sink.workingCopy).toBeUndefined();
360
367
  });
361
368
 
@@ -373,6 +380,25 @@ describe("plan cut removals", () => {
373
380
  expect(client.sessionCutBlock).toHaveBeenNthCalledWith(2, "ses_plan", 1);
374
381
  expect(sink.workingCopy).toBe("# Title\n");
375
382
  });
383
+
384
+ test("a whole-block Cut composes with an earlier exact Cut", async () => {
385
+ const start = PLAN_CONTENT.indexOf("paragraph.");
386
+ const end = PLAN_CONTENT.indexOf("Second") + "Sec".length;
387
+ const textCuts = [{ start, end, quote: PLAN_CONTENT.slice(start, end) }];
388
+ const session = {
389
+ ...planSession(applyTextCuts(PLAN_CONTENT, textCuts)),
390
+ textCuts,
391
+ };
392
+ const { controller, client, sink } = await connected(session);
393
+
394
+ controller.cut(1);
395
+ await tick();
396
+
397
+ expect(client.sessionSetWorkingCopy).toHaveBeenCalledTimes(1);
398
+ expect(client.sessionCutBlock).not.toHaveBeenCalled();
399
+ expect(sink.textCuts).toHaveLength(1);
400
+ expect(sink.workingCopy).toBe("# Title\nond paragraph.\n");
401
+ });
376
402
  });
377
403
 
378
404
  // a file whose hunk touches line 2 only, so lines 1/3/4 are unchanged tail/context
@@ -141,7 +141,7 @@ describe("switching threads keeps the cached projection curatable", () => {
141
141
  controller.toggleRejectChange(4);
142
142
  await tick();
143
143
 
144
- expect(controller.getSnapshot().status).toContain("change rejected");
144
+ expect(controller.getSnapshot().toast).toBeNull();
145
145
  expect(controller.getSnapshot().session?.workingCopy).toBe("");
146
146
  });
147
147