@cueloop/client 0.1.0-alpha.97 → 0.1.0-alpha.99
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 +11 -11
- package/src/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +31 -8
- package/src/annotations/use-annotation-surface.tsx +257 -31
- package/src/app/App.deleted-scroll.test.tsx +1 -0
- package/src/app/App.diff.test.tsx +1 -0
- package/src/app/App.identity.test.tsx +1 -0
- package/src/app/App.regression.test.tsx +5 -0
- package/src/app/App.share-dialog.test.tsx +5 -0
- package/src/app/App.submit-confirm.test.tsx +3 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.tsx +223 -97
- package/src/app/App.visual.test.tsx +2 -0
- package/src/app/App.workbench.test.tsx +53 -0
- package/src/app/app-screens.tsx +3 -0
- package/src/app/components/AppShell.tsx +1 -0
- package/src/app/components/AppShell.zoom.test.tsx +4 -3
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/run.ts +1 -0
- package/src/diff/components/AnnotatableFileView.test.tsx +2 -0
- package/src/diff/components/AnnotatableFileView.tsx +8 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +10 -0
- package/src/diff/diff-intraline.test.ts +1 -0
- package/src/diff/diff-intraline.ts +2 -0
- package/src/diff/split-diff.ts +2 -0
- package/src/integrations/components/ShareDialog.tsx +1 -0
- package/src/markdown/components/ThreadView.tsx +62 -16
- package/src/markdown/mark-runs.ts +1 -0
- package/src/markdown/markdown-highlight.ts +2 -0
- package/src/performance/otlp-trace.test.ts +3 -0
- package/src/performance/otlp-trace.ts +4 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/settings/components/quick-actions-editor.test.tsx +3 -0
- package/src/settings/config.test.ts +15 -0
- package/src/settings/config.ts +14 -4
- package/src/stories/__snapshots__/stories.test.tsx.snap +74 -0
- package/src/stories/stories-app.tsx +3 -0
- package/src/stories/story.ts +2 -0
- package/src/terminal/pty.ts +1 -0
- package/src/thread/agent-continuation.test.ts +32 -0
- package/src/thread/agent-continuation.ts +13 -0
- package/src/thread/agent-transcript.test.ts +161 -0
- package/src/thread/agent-transcript.ts +336 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +112 -0
- package/src/thread/components/AgentConfigControls.tsx +114 -0
- package/src/thread/components/AgentThreadPane.stories.tsx +105 -0
- package/src/thread/components/AgentThreadPane.test.tsx +829 -0
- package/src/thread/components/AgentThreadPane.tsx +367 -0
- package/src/thread/components/ThreadFooter.test.tsx +2 -0
- package/src/thread/components/ThreadFooter.tsx +3 -0
- package/src/thread/components/ThreadTree.test.tsx +41 -0
- package/src/thread/components/ThreadTree.tsx +25 -5
- package/src/thread/components/thread-tree.test.ts +3 -0
- package/src/thread/components/thread-tree.ts +4 -0
- package/src/thread/thread-controller.curate.test.ts +1 -0
- package/src/thread/thread-controller.live-diff.test.ts +7 -0
- package/src/thread/thread-controller.share.test.ts +1 -0
- package/src/thread/thread-controller.ts +42 -0
- package/src/thread/use-thread-agent.test.tsx +208 -0
- package/src/thread/use-thread-agent.ts +111 -0
- package/src/ui/components/PromptDialog.test.tsx +4 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/primitives/Dialog.test.tsx +1 -0
- package/src/workbench/components/ChangesColumn.tsx +1 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +2 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +7 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +1 -0
- package/src/workbench/components/editor-grid.test.ts +11 -0
- package/src/workbench/components/editor-grid.ts +10 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
|
@@ -193,6 +193,7 @@ describe("diff review", () => {
|
|
|
193
193
|
test("pasting an image into a diff comment drops in an [Image #n] placeholder", async () => {
|
|
194
194
|
// Arrange - mark the added line and open the inline comment composer
|
|
195
195
|
const setup = await renderApp();
|
|
196
|
+
|
|
196
197
|
await dragText(setup, "new Map()", "new Map()", "new Map()".length);
|
|
197
198
|
await typeText(setup, "see ");
|
|
198
199
|
|
|
@@ -48,6 +48,7 @@ function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string
|
|
|
48
48
|
for (const line of setup.captureSpans().lines) {
|
|
49
49
|
for (const span of line.spans) {
|
|
50
50
|
const rgb = span.fg?.toInts();
|
|
51
|
+
|
|
51
52
|
if (
|
|
52
53
|
rgb &&
|
|
53
54
|
rgb[0] === ACCENT_DARK[0] &&
|
|
@@ -55,6 +56,7 @@ function accentTitle(setup: Awaited<ReturnType<typeof renderReadyApp>>): string
|
|
|
55
56
|
rgb[2] === ACCENT_DARK[2]
|
|
56
57
|
) {
|
|
57
58
|
const text = span.text.trim();
|
|
59
|
+
|
|
58
60
|
if (text.includes("review")) return text;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -69,6 +71,7 @@ test("ctrl+q quits even when a menu holds the keyboard", async () => {
|
|
|
69
71
|
width: 120,
|
|
70
72
|
height: 32,
|
|
71
73
|
});
|
|
74
|
+
|
|
72
75
|
await waitForText(setup, "review");
|
|
73
76
|
|
|
74
77
|
// open the settings dialog from the top-left gear - it takes the keyboard
|
|
@@ -83,10 +86,12 @@ test("ctrl+q quits even when a menu holds the keyboard", async () => {
|
|
|
83
86
|
|
|
84
87
|
test("clicking a sidebar thread gives it the selected accent at once", async () => {
|
|
85
88
|
const setup = await renderReadyApp(<App home={home} />, { width: 120, height: 32 });
|
|
89
|
+
|
|
86
90
|
await waitForText(setup, "Second review");
|
|
87
91
|
|
|
88
92
|
// click whichever thread is not already the selected (accent) one
|
|
89
93
|
const target = accentTitle(setup)?.includes("Second") ? "First review" : "Second review";
|
|
94
|
+
|
|
90
95
|
await clickText(setup, target);
|
|
91
96
|
|
|
92
97
|
expect(accentTitle(setup)).toContain(target.split(" ")[0]!);
|
|
@@ -67,6 +67,7 @@ describe("share dialog", () => {
|
|
|
67
67
|
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
68
68
|
{ width: 120, height: 32 },
|
|
69
69
|
);
|
|
70
|
+
|
|
70
71
|
await waitForText(setup, "cueloop");
|
|
71
72
|
|
|
72
73
|
// Act
|
|
@@ -84,6 +85,7 @@ describe("share dialog", () => {
|
|
|
84
85
|
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
85
86
|
{ width: 120, height: 32 },
|
|
86
87
|
);
|
|
88
|
+
|
|
87
89
|
await waitForText(setup, "cueloop");
|
|
88
90
|
|
|
89
91
|
// Act: open the dialog, step into the body, activate "+ new link"
|
|
@@ -116,6 +118,7 @@ describe("share dialog", () => {
|
|
|
116
118
|
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
117
119
|
{ width: 120, height: 32 },
|
|
118
120
|
);
|
|
121
|
+
|
|
119
122
|
await waitForText(setup, "cueloop");
|
|
120
123
|
|
|
121
124
|
// Act: open the wizard, turn auth on but add no handles, then try to create
|
|
@@ -140,6 +143,7 @@ describe("share dialog", () => {
|
|
|
140
143
|
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
141
144
|
{ width: 120, height: 32 },
|
|
142
145
|
);
|
|
146
|
+
|
|
143
147
|
await waitForText(setup, "cueloop");
|
|
144
148
|
|
|
145
149
|
// Act + Assert: right arrow crosses into the body (its hint appears)
|
|
@@ -159,6 +163,7 @@ describe("share dialog", () => {
|
|
|
159
163
|
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
160
164
|
{ width: 120, height: 32 },
|
|
161
165
|
);
|
|
166
|
+
|
|
162
167
|
await waitForText(setup, "cueloop");
|
|
163
168
|
|
|
164
169
|
// Act
|
|
@@ -111,6 +111,7 @@ describe("send message confirm", () => {
|
|
|
111
111
|
.captureCharFrame()
|
|
112
112
|
.split("\n")
|
|
113
113
|
.find((row) => row.includes("[comment] [approve] [changes]"));
|
|
114
|
+
|
|
114
115
|
await press(setup, "right");
|
|
115
116
|
const commentSelector = setup
|
|
116
117
|
.captureCharFrame()
|
|
@@ -220,6 +221,7 @@ describe("send message confirm", () => {
|
|
|
220
221
|
// Arrange
|
|
221
222
|
seedAnnotations(1);
|
|
222
223
|
const setup = await renderApp();
|
|
224
|
+
|
|
223
225
|
await pressKey(setup, "RETURN", { meta: true });
|
|
224
226
|
await waitForText(setup, "[approve]");
|
|
225
227
|
|
|
@@ -233,6 +235,7 @@ describe("send message confirm", () => {
|
|
|
233
235
|
test("pasting an image into the summary drops in an [Image #n] placeholder", async () => {
|
|
234
236
|
// Arrange
|
|
235
237
|
const setup = await renderApp();
|
|
238
|
+
|
|
236
239
|
await pressKey(setup, "RETURN", { meta: true });
|
|
237
240
|
await waitForText(setup, "[approve]");
|
|
238
241
|
|
package/src/app/App.test.tsx
CHANGED
|
@@ -378,6 +378,7 @@ describe("no-thread shell", () => {
|
|
|
378
378
|
// unique "rename" row - the star action sits directly above it
|
|
379
379
|
await waitForText(setup, "rename");
|
|
380
380
|
const rename = locateText(setup, "rename");
|
|
381
|
+
|
|
381
382
|
await setup.mockMouse.click(rename.column, rename.row - 1);
|
|
382
383
|
|
|
383
384
|
// Assert - a Starred section now holds the thread
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { canRunThreadAgent } from "./App";
|
|
3
|
+
|
|
4
|
+
test("experimental agent UI requires a local owner and an enabled flag", () => {
|
|
5
|
+
const local = { enabled: true, owner: true, editing: false, shared: false };
|
|
6
|
+
|
|
7
|
+
expect(canRunThreadAgent(local)).toBe(true);
|
|
8
|
+
expect(canRunThreadAgent({ ...local, enabled: false })).toBe(false);
|
|
9
|
+
expect(canRunThreadAgent({ ...local, owner: false })).toBe(false);
|
|
10
|
+
expect(canRunThreadAgent({ ...local, editing: true })).toBe(false);
|
|
11
|
+
expect(canRunThreadAgent({ ...local, shared: true })).toBe(false);
|
|
12
|
+
});
|
package/src/app/App.tsx
CHANGED
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
type ThemeName,
|
|
36
36
|
} from "../appearance/theme-presets";
|
|
37
37
|
import type { Theme } from "../appearance/theme";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
createReviewController,
|
|
40
|
+
type ShareTransport,
|
|
41
|
+
type ReviewController,
|
|
42
|
+
} from "../thread/thread-controller";
|
|
39
43
|
import type { ThreadClient } from "@cueloop/daemon/client";
|
|
40
44
|
import { createIntentDispatch, type Mode, type RailTab } from "./intent-dispatch";
|
|
41
45
|
import { reduceKey, type KeyState } from "../keyboard/keymap";
|
|
@@ -62,6 +66,7 @@ import { useThreadBodyEditing } from "../markdown/use-thread-body-editing";
|
|
|
62
66
|
import { useRememberLayout } from "../workbench/use-remember-layout";
|
|
63
67
|
import type { LaunchLayout } from "./launch-layout";
|
|
64
68
|
import { ThreadFooter, THREAD_FOOTER_HEIGHT } from "../thread/components/ThreadFooter";
|
|
69
|
+
import { AgentThreadPrototype, agentOwnsKeyboard } from "../thread/components/AgentThreadPane";
|
|
65
70
|
import { ConfirmCard } from "../ui/components/ConfirmCard";
|
|
66
71
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "../markdown/components/ThreadView";
|
|
67
72
|
import {
|
|
@@ -371,6 +376,7 @@ function ProjectPanelBody(props: {
|
|
|
371
376
|
/>
|
|
372
377
|
);
|
|
373
378
|
}
|
|
379
|
+
|
|
374
380
|
return (
|
|
375
381
|
<ProjectTreeView
|
|
376
382
|
key={props.diffSourceKey}
|
|
@@ -612,6 +618,7 @@ export function App({
|
|
|
612
618
|
// A shared plan you own polls for collaborator notes while it is open; the
|
|
613
619
|
// merge refreshes through the normal event path. Stops on leave.
|
|
614
620
|
const threadShared = isSharedThread(session);
|
|
621
|
+
|
|
615
622
|
useEffect(() => {
|
|
616
623
|
if (!isOwner || !threadShared) return;
|
|
617
624
|
|
|
@@ -650,6 +657,7 @@ export function App({
|
|
|
650
657
|
const [focusedPane, setFocusedPane] = useState<FocusPane>(() =>
|
|
651
658
|
initialFocusedPane(sessionId, workbench.zoomed, layout),
|
|
652
659
|
);
|
|
660
|
+
|
|
653
661
|
// only the session view persists from here; the bare shell owns its own (NoThreadShell), so this
|
|
654
662
|
// workbench stays inactive with no session and never clobbers what the shell saved
|
|
655
663
|
useRememberLayout(
|
|
@@ -697,11 +705,13 @@ export function App({
|
|
|
697
705
|
);
|
|
698
706
|
// opt-in: render a prototype as a kitty pixel mockup instead of the markdown doc
|
|
699
707
|
const [prototypePixels, setPrototypePixels] = useState(false);
|
|
708
|
+
const [threadAgentEnabled, setThreadAgentEnabled] = useState(false);
|
|
700
709
|
|
|
701
710
|
useEffect(() => {
|
|
702
711
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
703
712
|
|
|
704
713
|
setPrototypePixels(config.experimental.prototypePixels);
|
|
714
|
+
setThreadAgentEnabled(config.experimental.threadAgent);
|
|
705
715
|
keysRef.current = config.keys;
|
|
706
716
|
keyBindings.setKeys(config.keys);
|
|
707
717
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
@@ -758,6 +768,7 @@ export function App({
|
|
|
758
768
|
};
|
|
759
769
|
const syncGithubIdentity = (): void => {
|
|
760
770
|
const generation = (identityGenerationRef.current += 1);
|
|
771
|
+
|
|
761
772
|
void resolveGithubIdentity().then((github) => {
|
|
762
773
|
if (identityGenerationRef.current !== generation) return;
|
|
763
774
|
if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
|
|
@@ -843,12 +854,20 @@ export function App({
|
|
|
843
854
|
() => (session && isDiff ? commentCountsByFile(session, rows) : undefined),
|
|
844
855
|
[session, isDiff, rows],
|
|
845
856
|
);
|
|
857
|
+
|
|
846
858
|
// entering a diff opens the right region in changed-files mode; a plan or reply opens it closed
|
|
847
859
|
workbench.syncSession(session?.id, isDiff);
|
|
848
860
|
// plans, replies, and the default (markdown) prototype open in the thread view, diffs in the
|
|
849
861
|
// diff sheet: both drive the shared annotation surface; only the pixel prototype keeps the keymap
|
|
850
862
|
const threadViewActive = session !== null && !isPixelPrototype;
|
|
851
863
|
const [threadComposing, setThreadComposing] = useState(false);
|
|
864
|
+
const [agentActive, setAgentActive] = useState(false);
|
|
865
|
+
const [agentControls, setAgentControls] = useState<React.ReactNode>(null);
|
|
866
|
+
const agentActions = useRef<(id: string) => { label: string; run: () => void } | undefined>(
|
|
867
|
+
() => undefined,
|
|
868
|
+
);
|
|
869
|
+
const agentInvoke = useRef<(() => void) | undefined>(undefined);
|
|
870
|
+
const [agentState, setAgentState] = useState<import("@cueloop/schema").ThreadAgentState>();
|
|
852
871
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
853
872
|
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
854
873
|
// inline body edit: the markdown editor owns the thread pane and all keys while open
|
|
@@ -975,6 +994,7 @@ export function App({
|
|
|
975
994
|
workbench.changesOpen,
|
|
976
995
|
workbench.projectOpen,
|
|
977
996
|
);
|
|
997
|
+
|
|
978
998
|
useEffect(() => {
|
|
979
999
|
// the bare shell drives its panes from its own workbench, so only reconcile against this one
|
|
980
1000
|
if (!session) return;
|
|
@@ -1020,7 +1040,28 @@ export function App({
|
|
|
1020
1040
|
controller.open(id);
|
|
1021
1041
|
};
|
|
1022
1042
|
|
|
1043
|
+
const createEmptyThread = enabledThreadAction(
|
|
1044
|
+
canRunThreadAgent({
|
|
1045
|
+
enabled: threadAgentEnabled,
|
|
1046
|
+
owner: isOwner,
|
|
1047
|
+
editing: false,
|
|
1048
|
+
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1049
|
+
}),
|
|
1050
|
+
() => {
|
|
1051
|
+
void controller.createEmptyThread();
|
|
1052
|
+
setFocusedPane("thread");
|
|
1053
|
+
},
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1023
1056
|
const threadSurfaceHandledKey = (key: KeyEvent): boolean => {
|
|
1057
|
+
if (
|
|
1058
|
+
agentOwnsKeyboard(agentActive, focusedPane) &&
|
|
1059
|
+
(key.name === "return" || key.name === "enter") &&
|
|
1060
|
+
key.ctrl &&
|
|
1061
|
+
!key.meta &&
|
|
1062
|
+
!key.super
|
|
1063
|
+
)
|
|
1064
|
+
return true;
|
|
1024
1065
|
if (!threadViewActive || threadViewSuspended) return false;
|
|
1025
1066
|
if (!threadComposing) {
|
|
1026
1067
|
const chord = resolveSessionChord(key, { isOwner, resolved });
|
|
@@ -1130,6 +1171,7 @@ export function App({
|
|
|
1130
1171
|
rows={grouped.rows}
|
|
1131
1172
|
inboxCursor={inboxCursor}
|
|
1132
1173
|
onOpenThread={openThread}
|
|
1174
|
+
onCreateThread={createEmptyThread}
|
|
1133
1175
|
mode={mode}
|
|
1134
1176
|
theme={theme}
|
|
1135
1177
|
controller={controller}
|
|
@@ -1225,6 +1267,7 @@ export function App({
|
|
|
1225
1267
|
|
|
1226
1268
|
const threadFooter = (
|
|
1227
1269
|
<ThreadFooter
|
|
1270
|
+
controls={threadAgentControls(agentActive, agentControls)}
|
|
1228
1271
|
repo={projectName(activeSession.workspace)}
|
|
1229
1272
|
branch={activeSession.workspace.branch}
|
|
1230
1273
|
onSubmit={onSubmitRequest}
|
|
@@ -1265,6 +1308,7 @@ export function App({
|
|
|
1265
1308
|
pinnedIds={pinnedIds}
|
|
1266
1309
|
width={30}
|
|
1267
1310
|
onSelect={openThread}
|
|
1311
|
+
onCreateThread={createEmptyThread}
|
|
1268
1312
|
onPin={togglePin}
|
|
1269
1313
|
onRename={(id, title) =>
|
|
1270
1314
|
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
@@ -1278,106 +1322,138 @@ export function App({
|
|
|
1278
1322
|
}
|
|
1279
1323
|
threadTitle={reviewThreadTitle(activeSession)}
|
|
1280
1324
|
threadActions={
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1325
|
+
<>
|
|
1326
|
+
{showOwnerActions
|
|
1327
|
+
? ownerThreadActions({
|
|
1328
|
+
editing: bodyEditing.editing,
|
|
1329
|
+
onEdit: onEditRequest,
|
|
1330
|
+
onExitEdit: bodyEditing.requestExit,
|
|
1331
|
+
onShare: () => dispatch({ type: "share" }),
|
|
1332
|
+
theme,
|
|
1333
|
+
})
|
|
1334
|
+
: showDiffRefresh
|
|
1335
|
+
? refreshDiffAction(() => void controller.refreshDiff(), theme)
|
|
1336
|
+
: undefined}
|
|
1337
|
+
</>
|
|
1292
1338
|
}
|
|
1293
1339
|
threadPanel={
|
|
1294
1340
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1295
1341
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
)
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1342
|
+
<AgentThreadPrototype
|
|
1343
|
+
key={activeSession.id}
|
|
1344
|
+
enabled={canRunThreadAgent({
|
|
1345
|
+
enabled: threadAgentEnabled,
|
|
1346
|
+
owner: isOwner,
|
|
1347
|
+
editing: bodyEditing.editing,
|
|
1348
|
+
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1349
|
+
})}
|
|
1350
|
+
observer={observer}
|
|
1351
|
+
pixelPrototype={isPixelPrototype}
|
|
1352
|
+
thread={activeSession}
|
|
1353
|
+
home={home}
|
|
1354
|
+
theme={theme}
|
|
1355
|
+
focused={focusedPane === "thread"}
|
|
1356
|
+
suspended={threadViewSuspended}
|
|
1357
|
+
onActiveChange={setAgentActive}
|
|
1358
|
+
onControlsChange={setAgentControls}
|
|
1359
|
+
onStateChange={setAgentState}
|
|
1360
|
+
onActionsChange={(actions) => {
|
|
1361
|
+
agentActions.current = actions;
|
|
1362
|
+
}}
|
|
1363
|
+
onInvokeChange={(invoke) => {
|
|
1364
|
+
agentInvoke.current = invoke;
|
|
1365
|
+
}}
|
|
1366
|
+
onRevealReply={() => setFocusedPane("thread")}
|
|
1367
|
+
flushMutations={() => flushReviewMutations(controller)}
|
|
1368
|
+
onOpenFile={(path) => workbench.openFile(path, "contents", true)}
|
|
1369
|
+
onNextPane={cyclePanes}
|
|
1370
|
+
>
|
|
1371
|
+
{chooseThreadBody({
|
|
1372
|
+
isPixelPrototype,
|
|
1373
|
+
isDiff,
|
|
1374
|
+
editingBody: bodyEditing.editing,
|
|
1375
|
+
prototype: (
|
|
1376
|
+
<PrototypePixels
|
|
1377
|
+
prototypePath={prototypePath}
|
|
1378
|
+
canComment={prototypeCanComment}
|
|
1379
|
+
onCommentElement={onCommentPrototype}
|
|
1380
|
+
onComposingChange={setPrototypeComposing}
|
|
1381
|
+
hidden={chromeHidden}
|
|
1382
|
+
/>
|
|
1383
|
+
),
|
|
1384
|
+
diffPlaceholder: activeSession.artifact.meta.prBrief ? (
|
|
1385
|
+
<ThreadView
|
|
1386
|
+
session={pullRequestBriefThread(activeSession)}
|
|
1387
|
+
display={buildDisplay(activeSession.artifact.meta.prBrief)}
|
|
1388
|
+
marks={new Map()}
|
|
1389
|
+
quickActions={quickActions}
|
|
1390
|
+
suspended={threadViewSuspended}
|
|
1391
|
+
resolved
|
|
1392
|
+
observer
|
|
1393
|
+
onComposingChange={() => {}}
|
|
1394
|
+
onObserverBlocked={() => {}}
|
|
1395
|
+
onCursorChange={() => {}}
|
|
1396
|
+
onAnnotate={() => {}}
|
|
1397
|
+
onReply={() => {}}
|
|
1398
|
+
onUpdateAnnotation={() => {}}
|
|
1399
|
+
resolveAuthorLabel={() => undefined}
|
|
1400
|
+
onExit={() => onExit?.(0)}
|
|
1401
|
+
/>
|
|
1402
|
+
) : (
|
|
1403
|
+
<box
|
|
1404
|
+
style={{
|
|
1405
|
+
flexGrow: 1,
|
|
1406
|
+
flexDirection: "column",
|
|
1407
|
+
alignItems: "center",
|
|
1408
|
+
justifyContent: "center",
|
|
1409
|
+
}}
|
|
1410
|
+
>
|
|
1411
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1412
|
+
</box>
|
|
1413
|
+
),
|
|
1414
|
+
editor: bodyEditing.renderEditor(theme),
|
|
1415
|
+
threadView: (
|
|
1416
|
+
<ThreadView
|
|
1417
|
+
session={activeSession}
|
|
1418
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1419
|
+
editOrphanCount={editOrphanCount}
|
|
1420
|
+
onComposingChange={setThreadComposing}
|
|
1421
|
+
onNavCommand={runNavCommand}
|
|
1422
|
+
resolved={resolved}
|
|
1423
|
+
onObserverBlocked={(reason) =>
|
|
1424
|
+
controller.setStatus(
|
|
1425
|
+
reason === "observer"
|
|
1426
|
+
? "observer - read-only"
|
|
1427
|
+
: "review submitted - read-only",
|
|
1428
|
+
)
|
|
1429
|
+
}
|
|
1430
|
+
onCursorChange={setCursor}
|
|
1431
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1432
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1433
|
+
display={display}
|
|
1434
|
+
marks={marks}
|
|
1435
|
+
quickActions={quickActions}
|
|
1436
|
+
observer={observer}
|
|
1437
|
+
onAnnotate={(span, body) =>
|
|
1438
|
+
controller.annotate(
|
|
1439
|
+
"comment",
|
|
1440
|
+
span.start.blockIndex,
|
|
1441
|
+
span.start.char,
|
|
1442
|
+
span.end.char,
|
|
1443
|
+
body,
|
|
1444
|
+
span.end.blockIndex,
|
|
1445
|
+
)
|
|
1446
|
+
}
|
|
1447
|
+
onReply={(rootAnnotationId, body) =>
|
|
1448
|
+
controller.reply(rootAnnotationId, body)
|
|
1449
|
+
}
|
|
1450
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1451
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1452
|
+
onExit={() => onExit?.(0)}
|
|
1453
|
+
/>
|
|
1454
|
+
),
|
|
1455
|
+
})}
|
|
1456
|
+
</AgentThreadPrototype>
|
|
1381
1457
|
</box>
|
|
1382
1458
|
{threadFooter}
|
|
1383
1459
|
</box>
|
|
@@ -1444,6 +1520,15 @@ export function App({
|
|
|
1444
1520
|
onReply: (rootAnnotationId, body) =>
|
|
1445
1521
|
void controller.reply(rootAnnotationId, body),
|
|
1446
1522
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1523
|
+
onInvoke: agentActive
|
|
1524
|
+
? () => {
|
|
1525
|
+
agentInvoke.current?.();
|
|
1526
|
+
setFocusedPane("thread");
|
|
1527
|
+
}
|
|
1528
|
+
: undefined,
|
|
1529
|
+
annotationAction: (id) => agentActions.current(id),
|
|
1530
|
+
isAnnotationReadOnly: (id) =>
|
|
1531
|
+
isSubmittedComment(agentState, activeSession.id, id),
|
|
1447
1532
|
resolveAuthorLabel,
|
|
1448
1533
|
onNavCommand: runNavCommand,
|
|
1449
1534
|
onExit: () => onExit?.(0),
|
|
@@ -1548,3 +1633,44 @@ export function App({
|
|
|
1548
1633
|
</SlashSkillsContext.Provider>
|
|
1549
1634
|
);
|
|
1550
1635
|
}
|
|
1636
|
+
|
|
1637
|
+
function enabledThreadAction(enabled: boolean, action: () => void): (() => void) | undefined {
|
|
1638
|
+
return enabled ? action : undefined;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/** Experimental agent UI is available only in a local owner's review. */
|
|
1642
|
+
export function canRunThreadAgent(options: {
|
|
1643
|
+
enabled: boolean;
|
|
1644
|
+
owner: boolean;
|
|
1645
|
+
editing: boolean;
|
|
1646
|
+
shared: boolean;
|
|
1647
|
+
}): boolean {
|
|
1648
|
+
return options.enabled && options.owner && !options.editing && !options.shared;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function isSubmittedComment(
|
|
1652
|
+
state: import("@cueloop/schema").ThreadAgentState | undefined,
|
|
1653
|
+
threadId: string,
|
|
1654
|
+
commentId: string,
|
|
1655
|
+
): boolean {
|
|
1656
|
+
return (
|
|
1657
|
+
state?.threadId === threadId &&
|
|
1658
|
+
Boolean(state.submissions?.some((entry) => entry.commentId === commentId))
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function flushReviewMutations(controller: ReviewController): Promise<void> {
|
|
1663
|
+
return controller.flushMutations?.() ?? Promise.resolve();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function threadAgentControls(active: boolean, controls: React.ReactNode): React.ReactNode {
|
|
1667
|
+
return active ? controls : undefined;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
/** Injected shared transports never own a local harness connection. */
|
|
1671
|
+
function isSharedThreadConnection(
|
|
1672
|
+
openClient: AppProps["openClient"],
|
|
1673
|
+
shareTransport: ShareTransport | undefined,
|
|
1674
|
+
): boolean {
|
|
1675
|
+
return Boolean(openClient || shareTransport);
|
|
1676
|
+
}
|
|
@@ -33,6 +33,7 @@ function hasSpanColor(
|
|
|
33
33
|
for (const span of line.spans) {
|
|
34
34
|
if (!span.text.includes(needle) || !span.fg) continue;
|
|
35
35
|
const [red, green, blue] = span.fg.toInts();
|
|
36
|
+
|
|
36
37
|
if (red === color[0] && green === color[1] && blue === color[2]) return true;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -47,6 +48,7 @@ for (const mode of ["dark", "light"] as const) {
|
|
|
47
48
|
width: 120,
|
|
48
49
|
height: 32,
|
|
49
50
|
});
|
|
51
|
+
|
|
50
52
|
await waitForText(setup, "Second review");
|
|
51
53
|
|
|
52
54
|
await clickText(setup, "Second review");
|