@cueloop/client 0.1.0-alpha.72 → 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.
Files changed (40) hide show
  1. package/package.json +5 -5
  2. package/src/App.tsx +35 -170
  3. package/src/App.workbench.test.tsx +103 -2
  4. package/src/app-screens.tsx +84 -10
  5. package/src/components/AppShell.brand.test.tsx +58 -0
  6. package/src/components/AppShell.tsx +32 -6
  7. package/src/components/BareWorkbenchFileView.tsx +1 -1
  8. package/src/components/ConfirmCard.tsx +0 -1
  9. package/src/components/GridTabContent.stories.tsx +59 -0
  10. package/src/components/GridTabContent.tsx +165 -0
  11. package/src/components/SettingsDialog.tsx +4 -13
  12. package/src/components/ThreadTree.tsx +6 -18
  13. package/src/components/__snapshots__/stories.test.tsx.snap +112 -68
  14. package/src/components/primitives/Card.tsx +1 -1
  15. package/src/components/primitives/frame.test.ts +5 -5
  16. package/src/components/primitives/frame.ts +5 -7
  17. package/src/components/quick-actions-editor.stories.tsx +2 -0
  18. package/src/components/quick-actions-editor.test.tsx +36 -2
  19. package/src/components/quick-actions-editor.tsx +104 -29
  20. package/src/components/truncate-title.test.ts +31 -0
  21. package/src/components/truncate-title.ts +15 -0
  22. package/src/config.test.ts +36 -0
  23. package/src/config.ts +38 -8
  24. package/src/index.ts +2 -0
  25. package/src/intent-dispatch.test.ts +1 -0
  26. package/src/launch-layout.test.ts +52 -0
  27. package/src/launch-layout.ts +42 -0
  28. package/src/run.ts +10 -0
  29. package/src/serve.ts +31 -0
  30. package/src/thread-controller.live-diff.test.ts +119 -0
  31. package/src/thread-controller.ts +88 -27
  32. package/src/use-changes-workbench.ts +41 -4
  33. package/src/use-remember-layout.ts +31 -0
  34. package/src/use-settings-dialog.tsx +11 -2
  35. package/src/view-diff.test.ts +73 -2
  36. package/src/view-diff.ts +38 -0
  37. package/src/workbench-snapshot.test.ts +81 -0
  38. package/src/workbench-snapshot.ts +29 -0
  39. package/src/components/fade-title.test.ts +0 -49
  40. package/src/components/fade-title.ts +0 -61
@@ -0,0 +1,29 @@
1
+ import { recaptureMainHead, type DiffFileContents, type Thread } from "@cueloop/schema";
2
+
3
+ /** Reads the live working-tree diff for a repo root (the daemon's `repo.diff` RPC). */
4
+ export type RepoDiff = (repoRoot: string) => Promise<{
5
+ patch: string;
6
+ files: DiffFileContents[];
7
+ }>;
8
+
9
+ /**
10
+ * Freeze a workbench thread's live working-tree diff into its artifact for a remote reviewer, who cannot
11
+ * see the owner's tree. The `snapshot` marker pins the render to this captured diff while keeping the
12
+ * workbench marker, so the reviewer still sees the file-targeted feedback already on the thread. Any other
13
+ * thread is already stable and passes through; the owner's own session keeps the original live thread.
14
+ */
15
+ export async function snapshotWorkbench(session: Thread, repoDiff: RepoDiff): Promise<Thread> {
16
+ if (session.artifact.type !== "diff" || session.artifact.meta.workbench !== true) return session;
17
+
18
+ const { patch, files } = await repoDiff(session.workspace.repoRoot);
19
+ const artifact = {
20
+ ...session.artifact,
21
+ content: patch,
22
+ files,
23
+ meta: { ...session.artifact.meta, snapshot: true },
24
+ };
25
+ // the share path rebuilds content from history; recapture the branch head so it matches the fresh patch
26
+ const history = session.history ? recaptureMainHead(session.history, patch) : session.history;
27
+
28
+ return { ...session, artifact, history };
29
+ }
@@ -1,49 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { fadeTitle, mixHex } from "./fade-title";
3
-
4
- describe("mixHex", () => {
5
- test("returns the endpoints at t=0 and t=1", () => {
6
- expect(mixHex("#000000", "#ffffff", 0)).toBe("#000000");
7
- expect(mixHex("#000000", "#ffffff", 1)).toBe("#ffffff");
8
- });
9
-
10
- test("blends halfway", () => {
11
- expect(mixHex("#000000", "#ffffff", 0.5)).toBe("#808080");
12
- });
13
-
14
- test("clamps out-of-range t", () => {
15
- expect(mixHex("#101010", "#202020", 2)).toBe("#202020");
16
- expect(mixHex("#101010", "#202020", -1)).toBe("#101010");
17
- });
18
- });
19
-
20
- describe("fadeTitle", () => {
21
- test("a title that fits is one plain segment", () => {
22
- const segments = fadeTitle("short", 20, "#aabbcc", "#000000");
23
-
24
- expect(segments).toEqual([{ text: "short", fg: "#aabbcc" }]);
25
- });
26
-
27
- test("a long title is clipped to the width and its tail fades toward the background", () => {
28
- const segments = fadeTitle("Review the accent change in the theme", 20, "#ffffff", "#000000");
29
-
30
- expect(segments.map((segment) => segment.text).join("")).toHaveLength(20);
31
- // the head keeps the base color, the last characters step toward the fade color
32
- expect(segments[0]!.fg).toBe("#ffffff");
33
- expect(segments.at(-1)!.fg).not.toBe("#ffffff");
34
- // the very last character is the most faded (closest to the background)
35
- const lastTwo = segments.slice(-2);
36
-
37
- expect(mixHex("#ffffff", "#000000", 0)).not.toBe(lastTwo[1]!.fg);
38
- });
39
-
40
- test("newlines collapse to spaces so the row never wraps", () => {
41
- const segments = fadeTitle("line one\nline two", 40, "#ffffff", "#000000");
42
-
43
- expect(segments[0]!.text).toBe("line one line two");
44
- });
45
-
46
- test("a zero width yields nothing", () => {
47
- expect(fadeTitle("anything", 0, "#ffffff", "#000000")).toEqual([]);
48
- });
49
- });
@@ -1,61 +0,0 @@
1
- /**
2
- * Fits a thread title onto one line, fading the last few characters toward the
3
- * background instead of cutting hard or wrapping - a gradient mask on the right.
4
- * Returns colored segments the caller renders as spans.
5
- */
6
-
7
- export interface TitleSegment {
8
- text: string;
9
- fg: string;
10
- }
11
-
12
- function channel(hex: string, start: number): number {
13
- return Number.parseInt(hex.slice(start, start + 2), 16);
14
- }
15
-
16
- /** Blend two #rrggbb colors; t=0 is `from`, t=1 is `to`. */
17
- export function mixHex(from: string, to: string, t: number): string {
18
- const clamp = Math.max(0, Math.min(1, t));
19
- const parts = [1, 3, 5].map((start) => {
20
- const value = Math.round(
21
- channel(from, start) + (channel(to, start) - channel(from, start)) * clamp,
22
- );
23
-
24
- return value.toString(16).padStart(2, "0");
25
- });
26
-
27
- return `#${parts.join("")}`;
28
- }
29
-
30
- /** The tail length that fades out; kept short so most of the title stays legible. */
31
- const FADE_WIDTH = 5;
32
-
33
- export function fadeTitle(
34
- title: string,
35
- maxWidth: number,
36
- baseColor: string,
37
- fadeColor: string,
38
- ): TitleSegment[] {
39
- const oneLine = title.replace(/\n/g, " ");
40
-
41
- if (maxWidth <= 0) return [];
42
- if (oneLine.length <= maxWidth) return [{ text: oneLine, fg: baseColor }];
43
-
44
- const clipped = oneLine.slice(0, maxWidth);
45
- const fadeLength = Math.min(FADE_WIDTH, maxWidth);
46
- const head = clipped.slice(0, maxWidth - fadeLength);
47
- const segments: TitleSegment[] = head.length > 0 ? [{ text: head, fg: baseColor }] : [];
48
-
49
- for (let index = 0; index < fadeLength; index++) {
50
- // the last character reaches the background fully, so the tail masks out
51
- // rather than leaving a legible word fragment
52
- const progress = (index + 1) / fadeLength;
53
-
54
- segments.push({
55
- text: clipped[maxWidth - fadeLength + index]!,
56
- fg: mixHex(baseColor, fadeColor, progress),
57
- });
58
- }
59
-
60
- return segments;
61
- }