@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.
- package/package.json +5 -5
- package/src/App.appearance.test.tsx +2 -2
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +2 -2
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +2 -2
- package/src/App.prototype-markdown.test.tsx +2 -2
- package/src/App.prototype.test.tsx +2 -2
- package/src/App.readonly.test.tsx +2 -2
- package/src/App.rendering.test.tsx +2 -2
- package/src/App.submit-confirm.test.tsx +2 -2
- package/src/App.test.tsx +2 -2
- package/src/App.toast.test.tsx +3 -3
- package/src/App.tsx +240 -228
- package/src/App.walk.test.tsx +2 -2
- package/src/App.workbench.test.tsx +2 -2
- package/src/app-key-state.ts +3 -3
- package/src/app-screens.tsx +4 -4
- package/src/app-view-model.ts +6 -6
- package/src/components/AnnotatableFileView.stories.tsx +2 -2
- package/src/components/AnnotatableFileView.test.tsx +2 -2
- package/src/components/AnnotatableFileView.tsx +2 -2
- package/src/components/BareWorkbenchFileView.tsx +2 -7
- package/src/components/ChangesColumn.tsx +2 -2
- package/src/components/DiffContentView.tsx +2 -2
- package/src/components/ThreadView.selection.test.tsx +5 -5
- package/src/components/ThreadView.slash.test.tsx +49 -4
- package/src/components/ThreadView.tsx +2 -2
- package/src/components/WelcomePlayground.tsx +2 -2
- package/src/components/__snapshots__/stories.test.tsx.snap +6 -6
- package/src/components/agent-launcher.tsx +2 -2
- package/src/components/quick-actions-editor.test.tsx +49 -0
- package/src/components/quick-actions-editor.tsx +32 -20
- package/src/components/session-tree.test.ts +2 -2
- package/src/components/session-tree.ts +10 -10
- package/src/components/story-fixtures.ts +3 -3
- package/src/config.test.ts +30 -1
- package/src/config.ts +19 -0
- package/src/discussions.ts +2 -2
- package/src/intent-dispatch.test.ts +3 -6
- package/src/intent-dispatch.ts +6 -6
- package/src/serve.test.ts +2 -2
- package/src/share.ts +9 -14
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +68 -0
- package/src/slash-palette.ts +10 -3
- package/src/{session-controller.curate.test.ts → thread-controller.curate.test.ts} +8 -7
- package/src/{session-controller.live-diff.test.ts → thread-controller.live-diff.test.ts} +7 -6
- package/src/{session-controller.share.test.ts → thread-controller.share.test.ts} +14 -13
- package/src/{session-controller.tree.test.ts → thread-controller.tree.test.ts} +8 -7
- package/src/{session-controller.ts → thread-controller.ts} +18 -22
- package/src/use-annotation-surface.tsx +14 -10
- package/src/view-diff.ts +2 -2
package/src/app-view-model.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Thread, VerdictKind } from "@cueloop/schema";
|
|
3
3
|
import type { Mode } from "./intent-dispatch";
|
|
4
4
|
import type { Intent, KeyState } from "./keymap";
|
|
5
|
-
import type { Completion } from "./
|
|
5
|
+
import type { Completion } from "./thread-controller";
|
|
6
6
|
import type { WalkFile } from "./walk";
|
|
7
7
|
import { viewedCount } from "./walk";
|
|
8
8
|
import type { ConfirmCardProps } from "./components/ConfirmCard";
|
|
@@ -17,7 +17,7 @@ export function computeRoleCapabilities(
|
|
|
17
17
|
return { observer, isOwner: !observer && role === "owner" };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function deriveReviewFlags(session:
|
|
20
|
+
export function deriveReviewFlags(session: Thread | null) {
|
|
21
21
|
return {
|
|
22
22
|
isDiff: session?.artifact.type === "diff",
|
|
23
23
|
isPrototype: session?.artifact.type === "prototype",
|
|
@@ -72,7 +72,7 @@ export function isCompletionOverlayPhase(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
export function buildHeaderItems(params: {
|
|
75
|
-
session:
|
|
75
|
+
session: Thread;
|
|
76
76
|
resolved: boolean;
|
|
77
77
|
observer: boolean;
|
|
78
78
|
role: "owner" | "observer" | "collaborator";
|
|
@@ -102,7 +102,7 @@ export function buildHeaderItems(params: {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
export function buildRenderFlags(params: {
|
|
105
|
-
session:
|
|
105
|
+
session: Thread;
|
|
106
106
|
isOwner: boolean;
|
|
107
107
|
isDiff: boolean;
|
|
108
108
|
isPixelPrototype: boolean;
|
|
@@ -132,7 +132,7 @@ export function buildSubmitConfirmState(
|
|
|
132
132
|
deps: DraftHandlerDeps & {
|
|
133
133
|
mode: Mode;
|
|
134
134
|
isDiff: boolean;
|
|
135
|
-
session:
|
|
135
|
+
session: Thread;
|
|
136
136
|
walkFileList: WalkFile[];
|
|
137
137
|
viewedPaths: Set<string>;
|
|
138
138
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { SCHEMA_VERSION, type
|
|
2
|
+
import { SCHEMA_VERSION, type Thread } from "@cueloop/schema";
|
|
3
3
|
import { DARK } from "../theme";
|
|
4
4
|
import type { Story, StoryMeta } from "./story";
|
|
5
5
|
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
@@ -8,7 +8,7 @@ export const meta: StoryMeta = { title: "Surfaces/AnnotatableFileView" };
|
|
|
8
8
|
|
|
9
9
|
const SAMPLE = `export function add(a: number, b: number): number {\n return a + b;\n}\n`;
|
|
10
10
|
|
|
11
|
-
const SESSION:
|
|
11
|
+
const SESSION: Thread = {
|
|
12
12
|
schemaVersion: SCHEMA_VERSION,
|
|
13
13
|
id: "ses_story",
|
|
14
14
|
workspace: { repoRoot: "/repo", branch: "main" },
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
import { describe, expect, mock, test } from "bun:test";
|
|
7
7
|
import { testRender } from "@opentui/react/test-utils";
|
|
8
8
|
import React from "react";
|
|
9
|
-
import { SCHEMA_VERSION, type Anchor, type
|
|
9
|
+
import { SCHEMA_VERSION, type Anchor, type Thread } from "@cueloop/schema";
|
|
10
10
|
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
11
11
|
import { DARK } from "../theme";
|
|
12
12
|
import { dragText, typeText, pressKey, waitForText } from "../test-support";
|
|
13
13
|
|
|
14
14
|
const SAMPLE = "export function add(a: number, b: number) {\n return a + b;\n}\n";
|
|
15
15
|
|
|
16
|
-
function planSession():
|
|
16
|
+
function planSession(): Thread {
|
|
17
17
|
return {
|
|
18
18
|
schemaVersion: SCHEMA_VERSION,
|
|
19
19
|
id: "ses_plan",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
|
|
5
5
|
|
|
6
6
|
import React, { useEffect, useRef, useState } from "react";
|
|
7
|
-
import { makeAnchor, annotationTarget, type Anchor, type
|
|
7
|
+
import { makeAnchor, annotationTarget, type Anchor, type Thread } from "@cueloop/schema";
|
|
8
8
|
import type { Theme } from "../theme";
|
|
9
9
|
import type { QuickAction } from "../config";
|
|
10
10
|
import { useComponentTheme } from "./theme-context";
|
|
@@ -14,7 +14,7 @@ import { diffRowBlocks, fileContentsRows, marksByRows } from "../view-diff";
|
|
|
14
14
|
export interface AnnotatableFileViewProps {
|
|
15
15
|
path: string;
|
|
16
16
|
loadContents: (path: string) => Promise<string | null>;
|
|
17
|
-
session:
|
|
17
|
+
session: Thread;
|
|
18
18
|
quickActions: QuickAction[];
|
|
19
19
|
observer: boolean;
|
|
20
20
|
resolved?: boolean;
|
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
// opens it - so this ephemeral view lives only until that first note promotes it to a real thread.
|
|
4
4
|
|
|
5
5
|
import React from "react";
|
|
6
|
-
import {
|
|
7
|
-
SCHEMA_VERSION,
|
|
8
|
-
type Anchor,
|
|
9
|
-
type AnnotationTarget,
|
|
10
|
-
type ReviewSession,
|
|
11
|
-
} from "@cueloop/schema";
|
|
6
|
+
import { SCHEMA_VERSION, type Anchor, type AnnotationTarget, type Thread } from "@cueloop/schema";
|
|
12
7
|
import type { Theme } from "../theme";
|
|
13
8
|
import type { QuickAction } from "../config";
|
|
14
9
|
import { AnnotatableFileView } from "./AnnotatableFileView";
|
|
@@ -20,7 +15,7 @@ export interface WorkbenchCommenter {
|
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
/** An empty, unsaved thread: it holds no notes, just enough shape for the annotation surface. */
|
|
23
|
-
function draftThread():
|
|
18
|
+
function draftThread(): Thread {
|
|
24
19
|
return {
|
|
25
20
|
schemaVersion: SCHEMA_VERSION,
|
|
26
21
|
id: "workbench-draft",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ScrollArea } from "./ScrollArea";
|
|
7
7
|
import React, { useMemo, useState } from "react";
|
|
8
|
-
import type { DiffFileContents,
|
|
8
|
+
import type { DiffFileContents, Thread } from "@cueloop/schema";
|
|
9
9
|
import type { Theme } from "../theme";
|
|
10
10
|
import type { DiffRow } from "../view-diff";
|
|
11
11
|
import { Tree } from "./primitives/Tree";
|
|
@@ -28,7 +28,7 @@ export interface DiffColumnsState {
|
|
|
28
28
|
* Resets on session change via the guarded render-time pattern, no effect.
|
|
29
29
|
*/
|
|
30
30
|
export function useDiffColumns(params: {
|
|
31
|
-
session:
|
|
31
|
+
session: Thread | null;
|
|
32
32
|
rows: DiffRow[];
|
|
33
33
|
cursor: number;
|
|
34
34
|
setCursor: (index: number) => void;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { createTextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
13
|
-
import type {
|
|
13
|
+
import type { Thread } from "@cueloop/schema";
|
|
14
14
|
import { diffRowText, fileChangeCounts, type DiffRow, type Mark } from "../view-diff";
|
|
15
15
|
import type { TextSpan } from "../thread-selection";
|
|
16
16
|
import type { QuickAction } from "../config";
|
|
@@ -71,7 +71,7 @@ export interface DiffFoldControls {
|
|
|
71
71
|
|
|
72
72
|
export interface DiffContentViewProps {
|
|
73
73
|
rows: DiffRow[];
|
|
74
|
-
session:
|
|
74
|
+
session: Thread;
|
|
75
75
|
/** Annotations resolved onto rows (marksByRows), with char ranges and spans. */
|
|
76
76
|
marks: Map<number, Mark[]>;
|
|
77
77
|
quickActions: QuickAction[];
|
|
@@ -29,7 +29,7 @@ const STANZA_ROWS = [
|
|
|
29
29
|
"so a crash can never leave a torn file on disk.",
|
|
30
30
|
];
|
|
31
31
|
const STANZA = STANZA_ROWS.join("\n");
|
|
32
|
-
const BULLETS = ["store.ts - the
|
|
32
|
+
const BULLETS = ["store.ts - the ThreadStore class", "schema.ts - the on-disk record shape"];
|
|
33
33
|
const PLAN = `# Plan\n\n${PARAGRAPH}\n\n${STANZA}\n\n- ${BULLETS[0]}\n- ${BULLETS[1]}\n`;
|
|
34
34
|
/** Display block indices: title, paragraph, stanza, two bullets. */
|
|
35
35
|
const STANZA_BLOCK = 2;
|
|
@@ -479,8 +479,8 @@ describe("marking across blocks", () => {
|
|
|
479
479
|
await setup.mockMouse.pressDown(torn.column, torn.row);
|
|
480
480
|
await settle(setup);
|
|
481
481
|
|
|
482
|
-
// Act: onto "
|
|
483
|
-
await setup.mockMouse.moveTo(bullet.column + BULLETS[0]!.indexOf("
|
|
482
|
+
// Act: onto "Thread" in the first bullet
|
|
483
|
+
await setup.mockMouse.moveTo(bullet.column + BULLETS[0]!.indexOf("Thread") + 6, bullet.row);
|
|
484
484
|
await settle(setup);
|
|
485
485
|
|
|
486
486
|
// Assert: two rows, the blank row between them untouched
|
|
@@ -488,7 +488,7 @@ describe("marking across blocks", () => {
|
|
|
488
488
|
|
|
489
489
|
expect([...rows.keys()]).toEqual([torn.row, bullet.row]);
|
|
490
490
|
expect(rows.get(torn.row)).toBe("torn file on disk.");
|
|
491
|
-
expect(rows.get(bullet.row)).toBe("store.ts - the
|
|
491
|
+
expect(rows.get(bullet.row)).toBe("store.ts - the Thread");
|
|
492
492
|
});
|
|
493
493
|
|
|
494
494
|
test("a drag across two bullets marks both", async () => {
|
|
@@ -506,7 +506,7 @@ describe("marking across blocks", () => {
|
|
|
506
506
|
// Assert
|
|
507
507
|
const rows = highlightedByRow();
|
|
508
508
|
|
|
509
|
-
expect(rows.get(first.row)).toBe("the
|
|
509
|
+
expect(rows.get(first.row)).toBe("the ThreadStore class");
|
|
510
510
|
expect(rows.get(second.row)).toBe("schema.ts");
|
|
511
511
|
expect(rows.size).toBe(2);
|
|
512
512
|
});
|
|
@@ -8,6 +8,8 @@ import { settle, typeText } from "../test-support";
|
|
|
8
8
|
import { buildDisplay, marksByDisplay } from "../view-plan";
|
|
9
9
|
import { fixturePlanSession } from "./story-fixtures";
|
|
10
10
|
import { inlineSlashToken, resolveInlineSuggestion, ThreadView } from "./ThreadView";
|
|
11
|
+
import { slashItemsFrom, type SlashItem } from "../slash-palette";
|
|
12
|
+
import { SlashSkillsContext } from "../skills";
|
|
11
13
|
|
|
12
14
|
const SKILLS: QuickAction[] = [
|
|
13
15
|
{ prompt: "typescript magician" },
|
|
@@ -66,24 +68,67 @@ describe("inlineSlashToken", () => {
|
|
|
66
68
|
|
|
67
69
|
describe("resolveInlineSuggestion", () => {
|
|
68
70
|
test("offers the fuzzy closest skill for the trailing token", () => {
|
|
69
|
-
const inline = resolveInlineSuggestion(false, "run /type", SKILLS);
|
|
71
|
+
const inline = resolveInlineSuggestion(false, "run /type", slashItemsFrom(SKILLS));
|
|
70
72
|
|
|
71
73
|
expect(inline?.token).toBe("/type");
|
|
72
74
|
expect(inline?.suggestion.name).toBe("typescript-magician");
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
test("prefers a prefix match over a subsequence match", () => {
|
|
76
|
-
const inline = resolveInlineSuggestion(false, "run /impl", SKILLS);
|
|
78
|
+
const inline = resolveInlineSuggestion(false, "run /impl", slashItemsFrom(SKILLS));
|
|
77
79
|
|
|
78
80
|
expect(inline?.suggestion.name).toBe("implement");
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
test("stays silent while the palette owns a leading slash", () => {
|
|
82
|
-
expect(resolveInlineSuggestion(true, "/type", SKILLS)).toBeNull();
|
|
84
|
+
expect(resolveInlineSuggestion(true, "/type", slashItemsFrom(SKILLS))).toBeNull();
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
test("returns null when nothing matches the token", () => {
|
|
86
|
-
expect(resolveInlineSuggestion(false, "run /zzzz", SKILLS)).toBeNull();
|
|
88
|
+
expect(resolveInlineSuggestion(false, "run /zzzz", slashItemsFrom(SKILLS))).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
async function mountComposerWithSkills(skills: SlashItem[]) {
|
|
93
|
+
const display = buildDisplay(PLAN, undefined);
|
|
94
|
+
const setup = await testRender(
|
|
95
|
+
<SlashSkillsContext.Provider value={skills}>
|
|
96
|
+
<ThreadView
|
|
97
|
+
session={fixturePlanSession({
|
|
98
|
+
artifact: { type: "plan", content: PLAN, meta: { title: "Plan" } },
|
|
99
|
+
})}
|
|
100
|
+
display={display}
|
|
101
|
+
marks={marksByDisplay([], display)}
|
|
102
|
+
quickActions={SKILLS}
|
|
103
|
+
observer={false}
|
|
104
|
+
onAnnotate={() => {}}
|
|
105
|
+
onReply={() => {}}
|
|
106
|
+
onUpdateAnnotation={() => {}}
|
|
107
|
+
onExit={() => {}}
|
|
108
|
+
/>
|
|
109
|
+
</SlashSkillsContext.Provider>,
|
|
110
|
+
{ width: 72, height: 20 },
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await settle(setup);
|
|
114
|
+
await settle(setup);
|
|
115
|
+
|
|
116
|
+
return setup;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
describe("skills in the palette", () => {
|
|
120
|
+
test("a skill from context lists in the / palette and a pick inserts its /name", async () => {
|
|
121
|
+
const setup = await mountComposerWithSkills([
|
|
122
|
+
{ name: "vitest-patterns", description: "patterns for vitest", body: "" },
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
await typeText(setup, "/vitest");
|
|
126
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
127
|
+
|
|
128
|
+
setup.mockInput.pressKey("RETURN");
|
|
129
|
+
await settle(setup);
|
|
130
|
+
// the reference lands in the draft, exactly like a quick action
|
|
131
|
+
expect(setup.captureCharFrame()).toContain("/vitest-patterns");
|
|
87
132
|
});
|
|
88
133
|
});
|
|
89
134
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import React, { useEffect, useRef } from "react";
|
|
16
16
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
17
|
-
import type {
|
|
17
|
+
import type { Thread } from "@cueloop/schema";
|
|
18
18
|
import { displayText, type DisplayBlock, type Mark } from "../view-plan";
|
|
19
19
|
import type { TextSpan } from "../thread-selection";
|
|
20
20
|
import type { QuickAction } from "../config";
|
|
@@ -104,7 +104,7 @@ export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
|
104
104
|
];
|
|
105
105
|
|
|
106
106
|
export interface ThreadViewProps {
|
|
107
|
-
session:
|
|
107
|
+
session: Thread;
|
|
108
108
|
display: DisplayBlock[];
|
|
109
109
|
marks: Map<number, Mark[]>;
|
|
110
110
|
quickActions: QuickAction[];
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
newAnnotationId,
|
|
11
11
|
SCHEMA_VERSION,
|
|
12
12
|
type Annotation,
|
|
13
|
-
type
|
|
13
|
+
type Thread,
|
|
14
14
|
} from "@cueloop/schema";
|
|
15
15
|
import type { Theme } from "../theme";
|
|
16
16
|
import type { QuickAction } from "../config";
|
|
@@ -55,7 +55,7 @@ function welcomeCopy(version: string): string {
|
|
|
55
55
|
|
|
56
56
|
/** An ephemeral thread that holds nothing but the playground's notes, so the surface groups
|
|
57
57
|
* discussions exactly as a real review does without any of it reaching the daemon. */
|
|
58
|
-
function playgroundThread(annotations: Annotation[]):
|
|
58
|
+
function playgroundThread(annotations: Annotation[]): Thread {
|
|
59
59
|
return {
|
|
60
60
|
schemaVersion: SCHEMA_VERSION,
|
|
61
61
|
id: "welcome",
|
|
@@ -32,7 +32,7 @@ exports[`stories catalog Chrome/AnnotationCards/SlashPaletteOpen renders, snapsh
|
|
|
32
32
|
/out-of-scope This is out of scope for the task, so capture it as a fo
|
|
33
33
|
/let-s-chat-about-this Do not implement yet, and surface the open questions and
|
|
34
34
|
/prototype-this Build a throwaway prototype to answer the question, skip
|
|
35
|
-
(2/
|
|
35
|
+
(2/8)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
"
|
|
@@ -1726,11 +1726,11 @@ exports[`stories catalog Chrome/QuickActionsEditor/Collapsed renders, snapshots,
|
|
|
1726
1726
|
Characterize the current behavior with test…
|
|
1727
1727
|
▸ Consider existing repo patterns
|
|
1728
1728
|
Follow the nearest existing pattern in this…
|
|
1729
|
+
▸ LGTM
|
|
1730
|
+
This looks good to me.
|
|
1729
1731
|
+ Add action
|
|
1730
1732
|
|
|
1731
1733
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
1734
|
"
|
|
1735
1735
|
`;
|
|
1736
1736
|
|
|
@@ -1741,7 +1741,7 @@ exports[`stories catalog Chrome/QuickActionsEditor/RowExpanded renders, snapshot
|
|
|
1741
1741
|
▸ Zoom out, research in depth
|
|
1742
1742
|
Research the broader problem, prior art, an…
|
|
1743
1743
|
▾ Restate simplified
|
|
1744
|
-
> Restate this as the simplest thing that works
|
|
1744
|
+
> Restate this as the simplest thing that works
|
|
1745
1745
|
▸ Out of scope
|
|
1746
1746
|
This is out of scope for the task, so captu…
|
|
1747
1747
|
▸ Let's chat about this
|
|
@@ -1752,11 +1752,11 @@ exports[`stories catalog Chrome/QuickActionsEditor/RowExpanded renders, snapshot
|
|
|
1752
1752
|
Characterize the current behavior with test…
|
|
1753
1753
|
▸ Consider existing repo patterns
|
|
1754
1754
|
Follow the nearest existing pattern in this…
|
|
1755
|
+
▸ LGTM
|
|
1756
|
+
This looks good to me.
|
|
1755
1757
|
+ Add action
|
|
1756
1758
|
|
|
1757
1759
|
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
1760
|
"
|
|
1761
1761
|
`;
|
|
1762
1762
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React, { useEffect, useRef, useState } from "react";
|
|
9
|
-
import type {
|
|
9
|
+
import type { Thread } from "@cueloop/schema";
|
|
10
10
|
import type { Theme } from "../theme";
|
|
11
11
|
import { useComponentTheme } from "./theme-context";
|
|
12
12
|
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
@@ -51,7 +51,7 @@ export function planHandoffBriefing(sessionId: string): string {
|
|
|
51
51
|
|
|
52
52
|
/** Props for the Agent tab body: the session under review plus the launch callbacks. */
|
|
53
53
|
export interface AgentLauncherProps {
|
|
54
|
-
session:
|
|
54
|
+
session: Thread;
|
|
55
55
|
/**
|
|
56
56
|
* Fallback launch when no embedded terminal ships for this platform: run the
|
|
57
57
|
* harness in a herdr split. seedText is the plan-context briefing.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The Actions editor scrolls to keep the selected row in view when the list outgrows the pane. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import type { QuickAction } from "../config";
|
|
7
|
+
import { settle } from "../test-support";
|
|
8
|
+
import { QuickActionsEditor } from "./quick-actions-editor";
|
|
9
|
+
|
|
10
|
+
const MANY: QuickAction[] = Array.from({ length: 14 }, (_, index) => ({
|
|
11
|
+
prompt: `Action number ${index}`,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
async function mount(selectedIndex: number) {
|
|
15
|
+
const setup = await testRender(
|
|
16
|
+
<QuickActionsEditor
|
|
17
|
+
actions={MANY}
|
|
18
|
+
selectedIndex={selectedIndex}
|
|
19
|
+
expandedIndex={null}
|
|
20
|
+
onToggleExpand={() => {}}
|
|
21
|
+
onEditMetadata={() => {}}
|
|
22
|
+
onReset={() => {}}
|
|
23
|
+
onAdd={() => {}}
|
|
24
|
+
/>,
|
|
25
|
+
{ width: 60, height: 10 },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
await settle(setup);
|
|
29
|
+
await settle(setup);
|
|
30
|
+
await settle(setup);
|
|
31
|
+
await settle(setup);
|
|
32
|
+
|
|
33
|
+
return setup;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("QuickActionsEditor scrolling", () => {
|
|
37
|
+
test("a list too long for the pane clips into the scroll area instead of overlapping rows", async () => {
|
|
38
|
+
const setup = await mount(0);
|
|
39
|
+
const lines = setup.captureCharFrame().split("\n");
|
|
40
|
+
|
|
41
|
+
// the top rows read cleanly on their own lines - the overflow no longer paints rows on top of each other
|
|
42
|
+
expect(lines.some((line) => line.includes("Action number 0"))).toBe(true);
|
|
43
|
+
expect(lines.some((line) => line.includes("Action number 1"))).toBe(true);
|
|
44
|
+
// a bottom row is scrolled out of the clipped pane, not overlaid onto a visible one
|
|
45
|
+
expect(setup.captureCharFrame()).not.toContain("Action number 13");
|
|
46
|
+
|
|
47
|
+
setup.renderer.destroy();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* the app owns the actions, the selection, and which row is expanded.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useRef } from "react";
|
|
10
|
-
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
9
|
+
import React, { useEffect, useRef } from "react";
|
|
10
|
+
import type { KeyBinding, ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
|
|
11
11
|
import type { QuickAction } from "../config";
|
|
12
12
|
import type { Theme } from "../theme";
|
|
13
|
+
import { ScrollArea } from "./ScrollArea";
|
|
13
14
|
import { useComponentTheme } from "./theme-context";
|
|
14
15
|
|
|
15
16
|
// ⏎ never inserts a newline: a system prompt is one line, esc closes the editor.
|
|
@@ -40,6 +41,11 @@ export function QuickActionsEditor({
|
|
|
40
41
|
theme,
|
|
41
42
|
}: QuickActionsEditorProps): React.ReactNode {
|
|
42
43
|
const tokens = useComponentTheme(theme);
|
|
44
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
scrollRef.current?.scrollChildIntoView(`action-row-${selectedIndex}`);
|
|
48
|
+
}, [selectedIndex]);
|
|
43
49
|
|
|
44
50
|
return (
|
|
45
51
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
@@ -52,28 +58,33 @@ export function QuickActionsEditor({
|
|
|
52
58
|
</box>
|
|
53
59
|
</box>
|
|
54
60
|
<box style={{ height: 1 }} />
|
|
55
|
-
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
<ScrollArea scrollRef={scrollRef}>
|
|
62
|
+
{actions.map((action, index) => (
|
|
63
|
+
<ActionRow
|
|
64
|
+
key={index}
|
|
65
|
+
rowId={`action-row-${index}`}
|
|
66
|
+
action={action}
|
|
67
|
+
isSelected={index === selectedIndex}
|
|
68
|
+
isExpanded={index === expandedIndex}
|
|
69
|
+
onToggleExpand={() => onToggleExpand(index)}
|
|
70
|
+
onEditMetadata={(metadata) => onEditMetadata(index, metadata)}
|
|
71
|
+
theme={theme}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
<box
|
|
75
|
+
id={`action-row-${actions.length}`}
|
|
76
|
+
style={{ backgroundColor: selectedIndex === actions.length ? tokens.border : undefined }}
|
|
77
|
+
onMouseUp={onAdd}
|
|
78
|
+
>
|
|
79
|
+
<text fg={tokens.textMuted}>+ Add action</text>
|
|
80
|
+
</box>
|
|
81
|
+
</ScrollArea>
|
|
72
82
|
</box>
|
|
73
83
|
);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
function ActionRow({
|
|
87
|
+
rowId,
|
|
77
88
|
action,
|
|
78
89
|
isSelected,
|
|
79
90
|
isExpanded,
|
|
@@ -81,6 +92,7 @@ function ActionRow({
|
|
|
81
92
|
onEditMetadata,
|
|
82
93
|
theme,
|
|
83
94
|
}: {
|
|
95
|
+
rowId: string;
|
|
84
96
|
action: QuickAction;
|
|
85
97
|
isSelected: boolean;
|
|
86
98
|
isExpanded: boolean;
|
|
@@ -92,7 +104,7 @@ function ActionRow({
|
|
|
92
104
|
const inputRef = useRef<TextareaRenderable | null>(null);
|
|
93
105
|
|
|
94
106
|
return (
|
|
95
|
-
<box style={{ flexDirection: "column" }}>
|
|
107
|
+
<box id={rowId} style={{ flexDirection: "column" }}>
|
|
96
108
|
<box
|
|
97
109
|
style={{ backgroundColor: isSelected ? tokens.border : undefined }}
|
|
98
110
|
onMouseUp={onToggleExpand}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Thread, WorkspaceKey } from "@cueloop/schema";
|
|
3
3
|
import { groupInbox, projectName } from "./session-tree";
|
|
4
4
|
|
|
5
|
-
function session(id: string, title: string, rootCommit?: string, remote?: string):
|
|
5
|
+
function session(id: string, title: string, rootCommit?: string, remote?: string): Thread {
|
|
6
6
|
const workspace: WorkspaceKey = { repoRoot: `/home/dev/${id}-checkout`, branch: "main" };
|
|
7
7
|
if (rootCommit !== undefined) workspace.rootCommit = rootCommit;
|
|
8
8
|
if (remote !== undefined) workspace.remote = remote;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* walks, so the keyboard grammar keeps a single index across the two groups.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { Thread, WorkspaceKey } from "@cueloop/schema";
|
|
10
10
|
|
|
11
11
|
/** A repo's display name: the remote basename when present, else the working-tree folder name. */
|
|
12
12
|
export function projectName(workspace: WorkspaceKey): string {
|
|
@@ -33,31 +33,31 @@ export function projectName(workspace: WorkspaceKey): string {
|
|
|
33
33
|
export type InboxRow =
|
|
34
34
|
| { kind: "section"; id: string; label: string }
|
|
35
35
|
| { kind: "project"; id: string; label: string }
|
|
36
|
-
| { kind: "thread"; id: string; session:
|
|
36
|
+
| { kind: "thread"; id: string; session: Thread; selectionIndex: number };
|
|
37
37
|
|
|
38
38
|
export interface GroupedInbox {
|
|
39
39
|
rows: InboxRow[];
|
|
40
40
|
/** Threads in display order; the inbox cursor indexes this. */
|
|
41
|
-
ordered:
|
|
41
|
+
ordered: Thread[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function threadTitle(session:
|
|
44
|
+
function threadTitle(session: Thread): string {
|
|
45
45
|
return session.artifact.meta.title ?? session.id;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
interface ProjectGroup {
|
|
49
49
|
name: string;
|
|
50
|
-
sessions:
|
|
50
|
+
sessions: Thread[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/** Group pending sessions into Pinned, then Projects (by root commit), then standalone Threads. */
|
|
54
54
|
export function groupInbox(
|
|
55
|
-
sessions: readonly
|
|
55
|
+
sessions: readonly Thread[],
|
|
56
56
|
pinnedIds?: ReadonlySet<string>,
|
|
57
57
|
): GroupedInbox {
|
|
58
|
-
const pinned:
|
|
58
|
+
const pinned: Thread[] = [];
|
|
59
59
|
const projects = new Map<string, ProjectGroup>();
|
|
60
|
-
const standalone:
|
|
60
|
+
const standalone: Thread[] = [];
|
|
61
61
|
|
|
62
62
|
for (const session of sessions) {
|
|
63
63
|
if (pinnedIds?.has(session.id) === true) {
|
|
@@ -77,8 +77,8 @@ export function groupInbox(
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const rows: InboxRow[] = [];
|
|
80
|
-
const ordered:
|
|
81
|
-
const pushThread = (session:
|
|
80
|
+
const ordered: Thread[] = [];
|
|
81
|
+
const pushThread = (session: Thread): void => {
|
|
82
82
|
rows.push({ kind: "thread", id: session.id, session, selectionIndex: ordered.length });
|
|
83
83
|
ordered.push(session);
|
|
84
84
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Shared fixture data for the stories catalog: one plan session, one diff. */
|
|
2
2
|
|
|
3
|
-
import type { Annotation,
|
|
3
|
+
import type { Annotation, Thread } from "@cueloop/schema";
|
|
4
4
|
import { buildDisplay, marksByDisplay } from "../view-plan";
|
|
5
5
|
import { diffRows } from "../view-diff";
|
|
6
6
|
|
|
@@ -51,7 +51,7 @@ export const FIXTURE_ANNOTATIONS: Annotation[] = [
|
|
|
51
51
|
},
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
-
export function fixturePlanSession(overrides: Partial<
|
|
54
|
+
export function fixturePlanSession(overrides: Partial<Thread> = {}): Thread {
|
|
55
55
|
return {
|
|
56
56
|
schemaVersion: "1",
|
|
57
57
|
id: "s_story_plan",
|
|
@@ -70,7 +70,7 @@ export function fixturePlanSession(overrides: Partial<ReviewSession> = {}): Revi
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function fixtureDiffSession(overrides: Partial<
|
|
73
|
+
export function fixtureDiffSession(overrides: Partial<Thread> = {}): Thread {
|
|
74
74
|
return {
|
|
75
75
|
schemaVersion: "1",
|
|
76
76
|
id: "s_story_diff",
|