@cueloop/client 0.1.0-alpha.69 → 0.1.0-alpha.70

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 (53) hide show
  1. package/package.json +5 -5
  2. package/src/App.appearance.test.tsx +2 -2
  3. package/src/App.autoclose.test.tsx +2 -2
  4. package/src/App.diff.test.tsx +2 -2
  5. package/src/App.export.test.tsx +2 -2
  6. package/src/App.plan-review.test.tsx +2 -2
  7. package/src/App.prototype-markdown.test.tsx +2 -2
  8. package/src/App.prototype.test.tsx +2 -2
  9. package/src/App.readonly.test.tsx +2 -2
  10. package/src/App.rendering.test.tsx +2 -2
  11. package/src/App.submit-confirm.test.tsx +2 -2
  12. package/src/App.test.tsx +2 -2
  13. package/src/App.toast.test.tsx +3 -3
  14. package/src/App.tsx +240 -228
  15. package/src/App.walk.test.tsx +2 -2
  16. package/src/App.workbench.test.tsx +2 -2
  17. package/src/app-key-state.ts +3 -3
  18. package/src/app-screens.tsx +4 -4
  19. package/src/app-view-model.ts +6 -6
  20. package/src/components/AnnotatableFileView.stories.tsx +2 -2
  21. package/src/components/AnnotatableFileView.test.tsx +2 -2
  22. package/src/components/AnnotatableFileView.tsx +2 -2
  23. package/src/components/BareWorkbenchFileView.tsx +2 -7
  24. package/src/components/ChangesColumn.tsx +2 -2
  25. package/src/components/DiffContentView.tsx +2 -2
  26. package/src/components/ThreadView.selection.test.tsx +5 -5
  27. package/src/components/ThreadView.slash.test.tsx +49 -4
  28. package/src/components/ThreadView.tsx +2 -2
  29. package/src/components/WelcomePlayground.tsx +2 -2
  30. package/src/components/__snapshots__/stories.test.tsx.snap +6 -6
  31. package/src/components/agent-launcher.tsx +2 -2
  32. package/src/components/quick-actions-editor.test.tsx +49 -0
  33. package/src/components/quick-actions-editor.tsx +32 -20
  34. package/src/components/session-tree.test.ts +2 -2
  35. package/src/components/session-tree.ts +10 -10
  36. package/src/components/story-fixtures.ts +3 -3
  37. package/src/config.test.ts +30 -1
  38. package/src/config.ts +19 -0
  39. package/src/discussions.ts +2 -2
  40. package/src/intent-dispatch.test.ts +3 -6
  41. package/src/intent-dispatch.ts +6 -6
  42. package/src/serve.test.ts +2 -2
  43. package/src/share.ts +9 -14
  44. package/src/skills.test.ts +74 -0
  45. package/src/skills.ts +68 -0
  46. package/src/slash-palette.ts +10 -3
  47. package/src/{session-controller.curate.test.ts → thread-controller.curate.test.ts} +8 -7
  48. package/src/{session-controller.live-diff.test.ts → thread-controller.live-diff.test.ts} +7 -6
  49. package/src/{session-controller.share.test.ts → thread-controller.share.test.ts} +14 -13
  50. package/src/{session-controller.tree.test.ts → thread-controller.tree.test.ts} +8 -7
  51. package/src/{session-controller.ts → thread-controller.ts} +18 -22
  52. package/src/use-annotation-surface.tsx +14 -10
  53. package/src/view-diff.ts +2 -2
@@ -1,17 +1,17 @@
1
1
  import { beforeEach, describe, expect, mock, test, type Mock } from "bun:test";
2
2
  import { ManualClock } from "@opentui/core/testing";
3
- import { SCHEMA_VERSION, type Annotation, type ReviewSession } from "@cueloop/schema";
3
+ import { SCHEMA_VERSION, type Annotation, type Thread } from "@cueloop/schema";
4
4
  import type { SessionClient } from "@cueloop/daemon/client";
5
5
  import {
6
6
  createReviewController,
7
7
  SHARE_RECONNECT_MAX_MS,
8
8
  SHARE_RECONNECT_MIN_MS,
9
9
  type ShareTransport,
10
- } from "./session-controller";
10
+ } from "./thread-controller";
11
11
  import { mergeFromShare, type ShareWatchHandlers } from "./share";
12
12
 
13
13
  const publishShare = mock(async () => ({ line: "ssh p_abc123xy@cueloop.dev", copied: true }));
14
- let remote: ReviewSession;
14
+ let remote: Thread;
15
15
  const pullShare = mock(async () => remote);
16
16
  const pushShare = mock(
17
17
  async (_shareId: string, _annotations: Array<Omit<Annotation, "createdAt">>) => {},
@@ -27,7 +27,7 @@ const shareTransport: ShareTransport = {
27
27
  mergeFromShare,
28
28
  };
29
29
 
30
- function sessionFixture(overrides: Partial<ReviewSession> = {}): ReviewSession {
30
+ function sessionFixture(overrides: Partial<Thread> = {}): Thread {
31
31
  return {
32
32
  schemaVersion: SCHEMA_VERSION,
33
33
  id: "ses_1",
@@ -54,19 +54,20 @@ function annotation(id: string, author: string): Annotation {
54
54
  }
55
55
 
56
56
  interface FakeSessionClient extends SessionClient {
57
- sessionAnnotate: Mock<SessionClient["sessionAnnotate"]>;
57
+ sessionComment: Mock<SessionClient["sessionComment"]>;
58
58
  }
59
59
 
60
60
  const unimplemented = (member: string) => () =>
61
61
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
62
62
 
63
- function fakeClient(session: ReviewSession): FakeSessionClient {
63
+ function fakeClient(session: Thread): FakeSessionClient {
64
64
  return {
65
65
  onEvent: () => () => {},
66
66
  subscribe: async () => {},
67
67
  sessionGet: async () => session,
68
68
  sessionList: async () => [session],
69
- sessionAnnotate: mock<SessionClient["sessionAnnotate"]>(async () => session),
69
+ sessionComment: mock<SessionClient["sessionComment"]>(async () => session),
70
+ sessionAnnotate: unimplemented("sessionAnnotate"),
70
71
  sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
71
72
  sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
72
73
  sessionCutBlock: unimplemented("sessionCutBlock"),
@@ -100,7 +101,7 @@ function fakeClient(session: ReviewSession): FakeSessionClient {
100
101
  const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
101
102
 
102
103
  async function connectedController(
103
- session: ReviewSession,
104
+ session: Thread,
104
105
  clock?: ManualClock,
105
106
  transport: ShareTransport = shareTransport,
106
107
  ): Promise<{ controller: ReturnType<typeof createReviewController>; client: FakeSessionClient }> {
@@ -233,7 +234,7 @@ describe("mirror on annotate", () => {
233
234
  sessionFixture({ shareId: "p_abc123xy", annotations: [annotation("a1", "SHA256:me")] }),
234
235
  );
235
236
 
236
- client.sessionAnnotate.mockImplementationOnce(async () => {
237
+ client.sessionComment.mockImplementationOnce(async () => {
237
238
  throw new Error("session is resolved");
238
239
  });
239
240
 
@@ -259,7 +260,7 @@ describe("reply", () => {
259
260
  await tick();
260
261
 
261
262
  // Assert
262
- const wire = client.sessionAnnotate.mock.calls.at(-1)![1];
263
+ const wire = client.sessionComment.mock.calls.at(-1)![1];
263
264
 
264
265
  expect(wire).toEqual({
265
266
  id: id!,
@@ -287,7 +288,7 @@ describe("reply", () => {
287
288
  await tick();
288
289
 
289
290
  // Assert
290
- expect(client.sessionAnnotate.mock.calls.at(-1)?.[1]).toMatchObject({
291
+ expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({
291
292
  id: "a2",
292
293
  body: "revised reply",
293
294
  replyTo: "a1",
@@ -310,7 +311,7 @@ describe("reply", () => {
310
311
  await tick();
311
312
 
312
313
  // Assert
313
- expect(client.sessionAnnotate.mock.calls.at(-1)?.[1]).toMatchObject({ replyTo: "a1" });
314
+ expect(client.sessionComment.mock.calls.at(-1)?.[1]).toMatchObject({ replyTo: "a1" });
314
315
  });
315
316
 
316
317
  test("replying to an unknown annotation does nothing", async () => {
@@ -319,7 +320,7 @@ describe("reply", () => {
319
320
 
320
321
  // Act + Assert
321
322
  expect(controller.reply("missing", "x")).toBeUndefined();
322
- expect(client.sessionAnnotate).not.toHaveBeenCalled();
323
+ expect(client.sessionComment).not.toHaveBeenCalled();
323
324
  });
324
325
  });
325
326
 
@@ -12,10 +12,10 @@ import {
12
12
  SCHEMA_VERSION,
13
13
  switchBranch,
14
14
  type Annotation,
15
- type ReviewSession,
15
+ type Thread,
16
16
  } from "@cueloop/schema";
17
17
  import type { SessionClient } from "@cueloop/daemon/client";
18
- import { createReviewController, type ShareTransport } from "./session-controller";
18
+ import { createReviewController, type ShareTransport } from "./thread-controller";
19
19
  import { mergeFromShare } from "./share";
20
20
 
21
21
  const AT = "2026-01-01T00:00:00.000Z";
@@ -31,8 +31,8 @@ function annotation(id: string, body: string): Annotation {
31
31
  }
32
32
 
33
33
  /** A plan with one comment on main and its history. */
34
- function sessionFixture(): ReviewSession {
35
- const session: ReviewSession = {
34
+ function sessionFixture(): Thread {
35
+ const session: Thread = {
36
36
  schemaVersion: SCHEMA_VERSION,
37
37
  id: "ses_1",
38
38
  workspace: { repoRoot: "/repo", branch: "main" },
@@ -49,7 +49,7 @@ function sessionFixture(): ReviewSession {
49
49
  return session;
50
50
  }
51
51
 
52
- const publish = mock(async (session: ReviewSession) => ({
52
+ const publish = mock(async (session: Thread) => ({
53
53
  line: `ssh p_${session.id}@cueloop.dev`,
54
54
  copied: true,
55
55
  }));
@@ -67,14 +67,15 @@ const unimplemented = (member: string) => () =>
67
67
  Promise.reject(new Error(`fakeClient does not implement ${member}`));
68
68
 
69
69
  /** Tree requests never resolve here: what the controller shows is its own guess. */
70
- const pending = () => new Promise<ReviewSession>(() => {});
70
+ const pending = () => new Promise<Thread>(() => {});
71
71
 
72
- function fakeClient(session: ReviewSession) {
72
+ function fakeClient(session: Thread) {
73
73
  return {
74
74
  onEvent: () => () => {},
75
75
  subscribe: async () => {},
76
76
  sessionGet: async () => session,
77
77
  sessionList: async () => [session],
78
+ sessionComment: unimplemented("sessionComment"),
78
79
  sessionAnnotate: unimplemented("sessionAnnotate"),
79
80
  sessionRemoveAnnotation: unimplemented("sessionRemoveAnnotation"),
80
81
  sessionSetWorkingCopy: unimplemented("sessionSetWorkingCopy"),
@@ -30,7 +30,7 @@ import {
30
30
  type Annotation,
31
31
  type AnnotationTarget,
32
32
  type DiffFileContents,
33
- type ReviewSession,
33
+ type Thread,
34
34
  type SessionHistory,
35
35
  type VerdictKind,
36
36
  } from "@cueloop/schema";
@@ -124,8 +124,8 @@ function planCutId(base: { lineStart: number; lineEnd: number }): string {
124
124
  }
125
125
 
126
126
  export interface ControllerSnapshot {
127
- session: ReviewSession | null;
128
- inbox: ReviewSession[] | null;
127
+ session: Thread | null;
128
+ inbox: Thread[] | null;
129
129
  status: string;
130
130
  toast: ToastState | null;
131
131
  error: string | null;
@@ -360,7 +360,7 @@ class Controller implements ReviewController {
360
360
  private shareStop: (() => void) | null = null;
361
361
  private shareRun: object | null = null;
362
362
  /** Projections keyed by session identity (and the live diff for non-diff threads) so renders reuse one computation. */
363
- private derivedFor: ReviewSession | null = null;
363
+ private derivedFor: Thread | null = null;
364
364
  /** The launch/workspace repo's live working-tree diff, feeding the Changes view for non-diff threads. */
365
365
  private liveDiff: LiveWorkingDiff | null = null;
366
366
  private derivedForLiveDiff: LiveWorkingDiff | null = null;
@@ -661,7 +661,7 @@ class Controller implements ReviewController {
661
661
  }
662
662
 
663
663
  /** Optimistic apply: the daemon response is the next session snapshot. */
664
- private apply(mutation: Promise<ReviewSession>): void {
664
+ private apply(mutation: Promise<Thread>): void {
665
665
  mutation
666
666
  .then((session) => this.update({ session }))
667
667
  .catch((cause: unknown) =>
@@ -674,7 +674,7 @@ class Controller implements ReviewController {
674
674
  * builds on the first instead of on the stale snapshot. Answers arrive in
675
675
  * request order over the socket and replace the guess with the daemon's copy.
676
676
  */
677
- private applyOptimistic(expected: ReviewSession, mutation: Promise<ReviewSession>): void {
677
+ private applyOptimistic(expected: Thread, mutation: Promise<Thread>): void {
678
678
  this.update({ session: expected });
679
679
  mutation
680
680
  .then((session) => this.update({ session }))
@@ -762,7 +762,7 @@ class Controller implements ReviewController {
762
762
  );
763
763
 
764
764
  if (baseIndex === -1) return;
765
- const expected: ReviewSession = {
765
+ const expected: Thread = {
766
766
  ...session,
767
767
  workingCopy: restoreBlock(base, working, block.base, line),
768
768
  };
@@ -847,7 +847,7 @@ class Controller implements ReviewController {
847
847
  const session = this.snapshot.session;
848
848
 
849
849
  if (!session) return;
850
- const expected: ReviewSession = { ...session, curation: rejections };
850
+ const expected: Thread = { ...session, curation: rejections };
851
851
 
852
852
  if (!rejections.length) delete expected.curation;
853
853
  this.applyOptimistic(expected, this.client!.sessionCurate(session.id, rejections));
@@ -980,7 +980,7 @@ class Controller implements ReviewController {
980
980
  * resolving stay stored (the feedback serializer flags orphaned anchors);
981
981
  * the count feeds the one-line banner above the sheet.
982
982
  */
983
- private reconcileAnnotations(session: ReviewSession, editedContent: string): void {
983
+ private reconcileAnnotations(session: Thread, editedContent: string): void {
984
984
  const editedBlocks = parseBlocks(editedContent);
985
985
  const orphanCount = session.annotations.filter(
986
986
  (annotation) => resolveAnchor(annotation.anchor, editedBlocks) === null,
@@ -1049,7 +1049,7 @@ class Controller implements ReviewController {
1049
1049
  resolvedTarget.kind === "artifact"
1050
1050
  ? { id: newAnnotationId(), kind, anchor, body }
1051
1051
  : { id: newAnnotationId(), kind, anchor, body, target: resolvedTarget };
1052
- const persisted = this.client!.sessionAnnotate(session.id, wire);
1052
+ const persisted = this.client!.sessionComment(session.id, wire);
1053
1053
 
1054
1054
  this.apply(persisted);
1055
1055
  this.mirrorAnnotation(persisted, wire);
@@ -1065,7 +1065,7 @@ class Controller implements ReviewController {
1065
1065
  target.kind === "artifact"
1066
1066
  ? { id: newAnnotationId(), kind: "comment", anchor, body }
1067
1067
  : { id: newAnnotationId(), kind: "comment", anchor, body, target };
1068
- const persisted = this.client!.sessionAnnotate(session.id, wire);
1068
+ const persisted = this.client!.sessionComment(session.id, wire);
1069
1069
 
1070
1070
  this.apply(persisted);
1071
1071
  this.mirrorAnnotation(persisted, wire);
@@ -1108,7 +1108,7 @@ class Controller implements ReviewController {
1108
1108
  replyTo: root.replyTo ?? root.id,
1109
1109
  };
1110
1110
  const wire = root.target ? { ...base, target: root.target } : base;
1111
- const persisted = this.client!.sessionAnnotate(session.id, wire);
1111
+ const persisted = this.client!.sessionComment(session.id, wire);
1112
1112
 
1113
1113
  this.apply(persisted);
1114
1114
  this.mirrorAnnotation(persisted, wire);
@@ -1122,7 +1122,7 @@ class Controller implements ReviewController {
1122
1122
  if (!session) return undefined;
1123
1123
  const anchor = { quote, prefix: "", suffix: "", selector };
1124
1124
  const wire = { id: newAnnotationId(), kind: "comment", anchor, body };
1125
- const persisted = this.client!.sessionAnnotate(session.id, wire);
1125
+ const persisted = this.client!.sessionComment(session.id, wire);
1126
1126
 
1127
1127
  this.apply(persisted);
1128
1128
  this.mirrorAnnotation(persisted, wire);
@@ -1147,7 +1147,7 @@ class Controller implements ReviewController {
1147
1147
  };
1148
1148
 
1149
1149
  if (existing.replyTo !== undefined) wire.replyTo = existing.replyTo;
1150
- const persisted = this.client!.sessionAnnotate(session.id, wire);
1150
+ const persisted = this.client!.sessionComment(session.id, wire);
1151
1151
 
1152
1152
  this.apply(persisted);
1153
1153
  this.mirrorAnnotation(persisted, wire);
@@ -1156,7 +1156,7 @@ class Controller implements ReviewController {
1156
1156
 
1157
1157
  // push only after the local write lands, so a rejected write never leaks to the share
1158
1158
  private mirrorAnnotation(
1159
- persisted: Promise<ReviewSession>,
1159
+ persisted: Promise<Thread>,
1160
1160
  annotation: Omit<Annotation, "createdAt">,
1161
1161
  ): void {
1162
1162
  const session = this.snapshot.session;
@@ -1391,12 +1391,8 @@ class Controller implements ReviewController {
1391
1391
  * Show a moved tree at once - the record re-derived from its new path - and
1392
1392
  * let the daemon's answer replace it. A refused move surfaces as status.
1393
1393
  */
1394
- private moveTree(
1395
- session: ReviewSession,
1396
- history: SessionHistory,
1397
- request: () => Promise<ReviewSession>,
1398
- ): void {
1399
- const expected: ReviewSession = { ...session, history };
1394
+ private moveTree(session: Thread, history: SessionHistory, request: () => Promise<Thread>): void {
1395
+ const expected: Thread = { ...session, history };
1400
1396
 
1401
1397
  applyPathView(
1402
1398
  expected,
@@ -1421,7 +1417,7 @@ class Controller implements ReviewController {
1421
1417
  .catch(() => {});
1422
1418
  }
1423
1419
 
1424
- private mergeShared(remote: ReviewSession): Promise<void> {
1420
+ private mergeShared(remote: Thread): Promise<void> {
1425
1421
  const session = this.snapshot.session;
1426
1422
 
1427
1423
  if (!session || !this.client) return Promise.resolve();
@@ -12,10 +12,10 @@
12
12
  * cards this hook builds under the visual line a span ends on.
13
13
  */
14
14
 
15
- import React, { useEffect, useRef, useState } from "react";
15
+ import React, { useContext, useEffect, useRef, useState } from "react";
16
16
  import type { KeyEvent, MouseEvent as TerminalMouseEvent, TextRenderable } from "@opentui/core";
17
17
  import { useKeyboard } from "@opentui/react";
18
- import type { ReviewSession } from "@cueloop/schema";
18
+ import type { Thread } from "@cueloop/schema";
19
19
  import type { Mark } from "./view-plan";
20
20
  import type { QuickAction } from "./config";
21
21
  import type { Theme } from "./theme";
@@ -37,7 +37,13 @@ import {
37
37
  } from "./thread-selection";
38
38
  import { annotationPaletteFor, type AnnotationPalette } from "./annotation-palette";
39
39
  import { printableSequence, type MarkRange, type VisualLine } from "./mark-runs";
40
- import { resolveInlineSuggestion, slashFilter, slashItemsFrom } from "./slash-palette";
40
+ import {
41
+ mergeSlashItems,
42
+ resolveInlineSuggestion,
43
+ slashFilter,
44
+ slashItemsFrom,
45
+ } from "./slash-palette";
46
+ import { SlashSkillsContext } from "./skills";
41
47
  import { discussionsFrom, type Discussion } from "./discussions";
42
48
  import {
43
49
  CommentRow,
@@ -68,7 +74,7 @@ export interface ComposeState {
68
74
 
69
75
  export interface AnnotationSurfaceOptions {
70
76
  source: LineSource;
71
- session: ReviewSession;
77
+ session: Thread;
72
78
  marks: Map<number, Mark[]>;
73
79
  quickActions: QuickAction[];
74
80
  tokens: Theme;
@@ -305,14 +311,12 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
305
311
  setCompose(null);
306
312
  setComposeText("");
307
313
  };
314
+ const skills = useContext(SlashSkillsContext);
315
+ const paletteItems = mergeSlashItems(slashItemsFrom(quickActions), skills);
308
316
  // open only while typing the token: a leading "/" with no space yet
309
317
  const slashActive = compose !== null && /^\/\S*$/.test(composeText);
310
- const slashItems = slashActive
311
- ? slashFilter(slashItemsFrom(quickActions), composeText.slice(1).trim())
312
- : [];
313
- // inline skill completion: the trailing "/word" token and its closest match,
314
- // offered as a hint below the composer that tab completes to the full name
315
- const inlineSlash = resolveInlineSuggestion(slashActive, composeText, quickActions);
318
+ const slashItems = slashActive ? slashFilter(paletteItems, composeText.slice(1).trim()) : [];
319
+ const inlineSlash = resolveInlineSuggestion(slashActive, composeText, paletteItems);
316
320
 
317
321
  const saveComment = (body: string): void => {
318
322
  // the body saves verbatim - typed newlines are the author's choice;
package/src/view-diff.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  resolveAnchor,
13
13
  type Annotation,
14
14
  type Block,
15
- type ReviewSession,
15
+ type Thread,
16
16
  } from "@cueloop/schema";
17
17
  import { spanRangeInBlock, type TextSpan } from "./thread-selection";
18
18
  import type { Mark } from "./view-plan";
@@ -253,7 +253,7 @@ export function fileTargetMarks(
253
253
  * replies) toward the file its span ends in. Feeds the changed-files tree and tab badges, so a
254
254
  * file whose tab is closed still shows it carries feedback.
255
255
  */
256
- export function commentCountsByFile(session: ReviewSession, rows: DiffRow[]): Map<string, number> {
256
+ export function commentCountsByFile(session: Thread, rows: DiffRow[]): Map<string, number> {
257
257
  const discussions = discussionsFrom(session, marksByRows(session.annotations, rows));
258
258
  const counts = new Map<string, number>();
259
259