@cueloop/client 0.1.0-alpha.96 → 0.1.0-alpha.98
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/annotations/components/AnnotationCards.test.tsx +42 -0
- package/src/annotations/components/AnnotationCards.tsx +29 -6
- package/src/annotations/use-annotation-surface.tsx +245 -30
- package/src/app/App.thread-agent.test.ts +12 -0
- package/src/app/App.tsx +217 -97
- package/src/app/app-screens.tsx +2 -0
- package/src/app/components/AppShell.zoom.test.tsx +4 -3
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/diff/components/AnnotatableFileView.tsx +6 -0
- package/src/diff/components/DiffContentView.tsx +9 -0
- package/src/markdown/components/ThreadView.tsx +60 -16
- 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/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 +332 -0
- package/src/thread/components/AgentConfigControls.stories.tsx +39 -0
- package/src/thread/components/AgentConfigControls.test.tsx +110 -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 +780 -0
- package/src/thread/components/AgentThreadPane.tsx +356 -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/thread-controller.ts +34 -0
- package/src/thread/use-thread-agent.test.tsx +65 -0
- package/src/thread/use-thread-agent.ts +101 -0
- package/src/workbench/components/GridTabContent.tsx +6 -0
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 {
|
|
@@ -697,11 +702,13 @@ export function App({
|
|
|
697
702
|
);
|
|
698
703
|
// opt-in: render a prototype as a kitty pixel mockup instead of the markdown doc
|
|
699
704
|
const [prototypePixels, setPrototypePixels] = useState(false);
|
|
705
|
+
const [threadAgentEnabled, setThreadAgentEnabled] = useState(false);
|
|
700
706
|
|
|
701
707
|
useEffect(() => {
|
|
702
708
|
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
703
709
|
|
|
704
710
|
setPrototypePixels(config.experimental.prototypePixels);
|
|
711
|
+
setThreadAgentEnabled(config.experimental.threadAgent);
|
|
705
712
|
keysRef.current = config.keys;
|
|
706
713
|
keyBindings.setKeys(config.keys);
|
|
707
714
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
@@ -849,6 +856,13 @@ export function App({
|
|
|
849
856
|
// diff sheet: both drive the shared annotation surface; only the pixel prototype keeps the keymap
|
|
850
857
|
const threadViewActive = session !== null && !isPixelPrototype;
|
|
851
858
|
const [threadComposing, setThreadComposing] = useState(false);
|
|
859
|
+
const [agentActive, setAgentActive] = useState(false);
|
|
860
|
+
const [agentControls, setAgentControls] = useState<React.ReactNode>(null);
|
|
861
|
+
const agentActions = useRef<(id: string) => { label: string; run: () => void } | undefined>(
|
|
862
|
+
() => undefined,
|
|
863
|
+
);
|
|
864
|
+
const agentInvoke = useRef<(() => void) | undefined>(undefined);
|
|
865
|
+
const [agentState, setAgentState] = useState<import("@cueloop/schema").ThreadAgentState>();
|
|
852
866
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
853
867
|
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
854
868
|
// inline body edit: the markdown editor owns the thread pane and all keys while open
|
|
@@ -1020,7 +1034,28 @@ export function App({
|
|
|
1020
1034
|
controller.open(id);
|
|
1021
1035
|
};
|
|
1022
1036
|
|
|
1037
|
+
const createEmptyThread = enabledThreadAction(
|
|
1038
|
+
canRunThreadAgent({
|
|
1039
|
+
enabled: threadAgentEnabled,
|
|
1040
|
+
owner: isOwner,
|
|
1041
|
+
editing: false,
|
|
1042
|
+
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1043
|
+
}),
|
|
1044
|
+
() => {
|
|
1045
|
+
void controller.createEmptyThread();
|
|
1046
|
+
setFocusedPane("thread");
|
|
1047
|
+
},
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1023
1050
|
const threadSurfaceHandledKey = (key: KeyEvent): boolean => {
|
|
1051
|
+
if (
|
|
1052
|
+
agentOwnsKeyboard(agentActive, focusedPane) &&
|
|
1053
|
+
(key.name === "return" || key.name === "enter") &&
|
|
1054
|
+
key.ctrl &&
|
|
1055
|
+
!key.meta &&
|
|
1056
|
+
!key.super
|
|
1057
|
+
)
|
|
1058
|
+
return true;
|
|
1024
1059
|
if (!threadViewActive || threadViewSuspended) return false;
|
|
1025
1060
|
if (!threadComposing) {
|
|
1026
1061
|
const chord = resolveSessionChord(key, { isOwner, resolved });
|
|
@@ -1130,6 +1165,7 @@ export function App({
|
|
|
1130
1165
|
rows={grouped.rows}
|
|
1131
1166
|
inboxCursor={inboxCursor}
|
|
1132
1167
|
onOpenThread={openThread}
|
|
1168
|
+
onCreateThread={createEmptyThread}
|
|
1133
1169
|
mode={mode}
|
|
1134
1170
|
theme={theme}
|
|
1135
1171
|
controller={controller}
|
|
@@ -1225,6 +1261,7 @@ export function App({
|
|
|
1225
1261
|
|
|
1226
1262
|
const threadFooter = (
|
|
1227
1263
|
<ThreadFooter
|
|
1264
|
+
controls={threadAgentControls(agentActive, agentControls)}
|
|
1228
1265
|
repo={projectName(activeSession.workspace)}
|
|
1229
1266
|
branch={activeSession.workspace.branch}
|
|
1230
1267
|
onSubmit={onSubmitRequest}
|
|
@@ -1265,6 +1302,7 @@ export function App({
|
|
|
1265
1302
|
pinnedIds={pinnedIds}
|
|
1266
1303
|
width={30}
|
|
1267
1304
|
onSelect={openThread}
|
|
1305
|
+
onCreateThread={createEmptyThread}
|
|
1268
1306
|
onPin={togglePin}
|
|
1269
1307
|
onRename={(id, title) =>
|
|
1270
1308
|
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
@@ -1278,106 +1316,138 @@ export function App({
|
|
|
1278
1316
|
}
|
|
1279
1317
|
threadTitle={reviewThreadTitle(activeSession)}
|
|
1280
1318
|
threadActions={
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1319
|
+
<>
|
|
1320
|
+
{showOwnerActions
|
|
1321
|
+
? ownerThreadActions({
|
|
1322
|
+
editing: bodyEditing.editing,
|
|
1323
|
+
onEdit: onEditRequest,
|
|
1324
|
+
onExitEdit: bodyEditing.requestExit,
|
|
1325
|
+
onShare: () => dispatch({ type: "share" }),
|
|
1326
|
+
theme,
|
|
1327
|
+
})
|
|
1328
|
+
: showDiffRefresh
|
|
1329
|
+
? refreshDiffAction(() => void controller.refreshDiff(), theme)
|
|
1330
|
+
: undefined}
|
|
1331
|
+
</>
|
|
1292
1332
|
}
|
|
1293
1333
|
threadPanel={
|
|
1294
1334
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1295
1335
|
<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
|
-
|
|
1336
|
+
<AgentThreadPrototype
|
|
1337
|
+
key={activeSession.id}
|
|
1338
|
+
enabled={canRunThreadAgent({
|
|
1339
|
+
enabled: threadAgentEnabled,
|
|
1340
|
+
owner: isOwner,
|
|
1341
|
+
editing: bodyEditing.editing,
|
|
1342
|
+
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1343
|
+
})}
|
|
1344
|
+
observer={observer}
|
|
1345
|
+
pixelPrototype={isPixelPrototype}
|
|
1346
|
+
thread={activeSession}
|
|
1347
|
+
home={home}
|
|
1348
|
+
theme={theme}
|
|
1349
|
+
focused={focusedPane === "thread"}
|
|
1350
|
+
suspended={threadViewSuspended}
|
|
1351
|
+
onActiveChange={setAgentActive}
|
|
1352
|
+
onControlsChange={setAgentControls}
|
|
1353
|
+
onStateChange={setAgentState}
|
|
1354
|
+
onActionsChange={(actions) => {
|
|
1355
|
+
agentActions.current = actions;
|
|
1356
|
+
}}
|
|
1357
|
+
onInvokeChange={(invoke) => {
|
|
1358
|
+
agentInvoke.current = invoke;
|
|
1359
|
+
}}
|
|
1360
|
+
onRevealReply={() => setFocusedPane("thread")}
|
|
1361
|
+
flushMutations={() => flushReviewMutations(controller)}
|
|
1362
|
+
onOpenFile={(path) => workbench.openFile(path, "contents", true)}
|
|
1363
|
+
onNextPane={cyclePanes}
|
|
1364
|
+
>
|
|
1365
|
+
{chooseThreadBody({
|
|
1366
|
+
isPixelPrototype,
|
|
1367
|
+
isDiff,
|
|
1368
|
+
editingBody: bodyEditing.editing,
|
|
1369
|
+
prototype: (
|
|
1370
|
+
<PrototypePixels
|
|
1371
|
+
prototypePath={prototypePath}
|
|
1372
|
+
canComment={prototypeCanComment}
|
|
1373
|
+
onCommentElement={onCommentPrototype}
|
|
1374
|
+
onComposingChange={setPrototypeComposing}
|
|
1375
|
+
hidden={chromeHidden}
|
|
1376
|
+
/>
|
|
1377
|
+
),
|
|
1378
|
+
diffPlaceholder: activeSession.artifact.meta.prBrief ? (
|
|
1379
|
+
<ThreadView
|
|
1380
|
+
session={pullRequestBriefThread(activeSession)}
|
|
1381
|
+
display={buildDisplay(activeSession.artifact.meta.prBrief)}
|
|
1382
|
+
marks={new Map()}
|
|
1383
|
+
quickActions={quickActions}
|
|
1384
|
+
suspended={threadViewSuspended}
|
|
1385
|
+
resolved
|
|
1386
|
+
observer
|
|
1387
|
+
onComposingChange={() => {}}
|
|
1388
|
+
onObserverBlocked={() => {}}
|
|
1389
|
+
onCursorChange={() => {}}
|
|
1390
|
+
onAnnotate={() => {}}
|
|
1391
|
+
onReply={() => {}}
|
|
1392
|
+
onUpdateAnnotation={() => {}}
|
|
1393
|
+
resolveAuthorLabel={() => undefined}
|
|
1394
|
+
onExit={() => onExit?.(0)}
|
|
1395
|
+
/>
|
|
1396
|
+
) : (
|
|
1397
|
+
<box
|
|
1398
|
+
style={{
|
|
1399
|
+
flexGrow: 1,
|
|
1400
|
+
flexDirection: "column",
|
|
1401
|
+
alignItems: "center",
|
|
1402
|
+
justifyContent: "center",
|
|
1403
|
+
}}
|
|
1404
|
+
>
|
|
1405
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1406
|
+
</box>
|
|
1407
|
+
),
|
|
1408
|
+
editor: bodyEditing.renderEditor(theme),
|
|
1409
|
+
threadView: (
|
|
1410
|
+
<ThreadView
|
|
1411
|
+
session={activeSession}
|
|
1412
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1413
|
+
editOrphanCount={editOrphanCount}
|
|
1414
|
+
onComposingChange={setThreadComposing}
|
|
1415
|
+
onNavCommand={runNavCommand}
|
|
1416
|
+
resolved={resolved}
|
|
1417
|
+
onObserverBlocked={(reason) =>
|
|
1418
|
+
controller.setStatus(
|
|
1419
|
+
reason === "observer"
|
|
1420
|
+
? "observer - read-only"
|
|
1421
|
+
: "review submitted - read-only",
|
|
1422
|
+
)
|
|
1423
|
+
}
|
|
1424
|
+
onCursorChange={setCursor}
|
|
1425
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1426
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1427
|
+
display={display}
|
|
1428
|
+
marks={marks}
|
|
1429
|
+
quickActions={quickActions}
|
|
1430
|
+
observer={observer}
|
|
1431
|
+
onAnnotate={(span, body) =>
|
|
1432
|
+
controller.annotate(
|
|
1433
|
+
"comment",
|
|
1434
|
+
span.start.blockIndex,
|
|
1435
|
+
span.start.char,
|
|
1436
|
+
span.end.char,
|
|
1437
|
+
body,
|
|
1438
|
+
span.end.blockIndex,
|
|
1439
|
+
)
|
|
1440
|
+
}
|
|
1441
|
+
onReply={(rootAnnotationId, body) =>
|
|
1442
|
+
controller.reply(rootAnnotationId, body)
|
|
1443
|
+
}
|
|
1444
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1445
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1446
|
+
onExit={() => onExit?.(0)}
|
|
1447
|
+
/>
|
|
1448
|
+
),
|
|
1449
|
+
})}
|
|
1450
|
+
</AgentThreadPrototype>
|
|
1381
1451
|
</box>
|
|
1382
1452
|
{threadFooter}
|
|
1383
1453
|
</box>
|
|
@@ -1444,6 +1514,15 @@ export function App({
|
|
|
1444
1514
|
onReply: (rootAnnotationId, body) =>
|
|
1445
1515
|
void controller.reply(rootAnnotationId, body),
|
|
1446
1516
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1517
|
+
onInvoke: agentActive
|
|
1518
|
+
? () => {
|
|
1519
|
+
agentInvoke.current?.();
|
|
1520
|
+
setFocusedPane("thread");
|
|
1521
|
+
}
|
|
1522
|
+
: undefined,
|
|
1523
|
+
annotationAction: (id) => agentActions.current(id),
|
|
1524
|
+
isAnnotationReadOnly: (id) =>
|
|
1525
|
+
isSubmittedComment(agentState, activeSession.id, id),
|
|
1447
1526
|
resolveAuthorLabel,
|
|
1448
1527
|
onNavCommand: runNavCommand,
|
|
1449
1528
|
onExit: () => onExit?.(0),
|
|
@@ -1548,3 +1627,44 @@ export function App({
|
|
|
1548
1627
|
</SlashSkillsContext.Provider>
|
|
1549
1628
|
);
|
|
1550
1629
|
}
|
|
1630
|
+
|
|
1631
|
+
function enabledThreadAction(enabled: boolean, action: () => void): (() => void) | undefined {
|
|
1632
|
+
return enabled ? action : undefined;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/** Experimental agent UI is available only in a local owner's review. */
|
|
1636
|
+
export function canRunThreadAgent(options: {
|
|
1637
|
+
enabled: boolean;
|
|
1638
|
+
owner: boolean;
|
|
1639
|
+
editing: boolean;
|
|
1640
|
+
shared: boolean;
|
|
1641
|
+
}): boolean {
|
|
1642
|
+
return options.enabled && options.owner && !options.editing && !options.shared;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function isSubmittedComment(
|
|
1646
|
+
state: import("@cueloop/schema").ThreadAgentState | undefined,
|
|
1647
|
+
threadId: string,
|
|
1648
|
+
commentId: string,
|
|
1649
|
+
): boolean {
|
|
1650
|
+
return (
|
|
1651
|
+
state?.threadId === threadId &&
|
|
1652
|
+
Boolean(state.submissions?.some((entry) => entry.commentId === commentId))
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function flushReviewMutations(controller: ReviewController): Promise<void> {
|
|
1657
|
+
return controller.flushMutations?.() ?? Promise.resolve();
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function threadAgentControls(active: boolean, controls: React.ReactNode): React.ReactNode {
|
|
1661
|
+
return active ? controls : undefined;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/** Injected shared transports never own a local harness connection. */
|
|
1665
|
+
function isSharedThreadConnection(
|
|
1666
|
+
openClient: AppProps["openClient"],
|
|
1667
|
+
shareTransport: ShareTransport | undefined,
|
|
1668
|
+
): boolean {
|
|
1669
|
+
return Boolean(openClient || shareTransport);
|
|
1670
|
+
}
|
package/src/app/app-screens.tsx
CHANGED
|
@@ -160,6 +160,7 @@ export function NoThreadShell(props: {
|
|
|
160
160
|
inboxCursor: number;
|
|
161
161
|
/** Open a sidebar thread and move the cursor onto it, so the row shows its selected backdrop at once. */
|
|
162
162
|
onOpenThread: (id: string) => void;
|
|
163
|
+
onCreateThread?: () => void;
|
|
163
164
|
mode: Mode;
|
|
164
165
|
theme: Theme;
|
|
165
166
|
controller: ReviewController;
|
|
@@ -291,6 +292,7 @@ export function NoThreadShell(props: {
|
|
|
291
292
|
pinnedIds={pinnedIds}
|
|
292
293
|
width={30}
|
|
293
294
|
onSelect={onOpenThread}
|
|
295
|
+
onCreateThread={props.onCreateThread}
|
|
294
296
|
onRequestDelete={(id, title) =>
|
|
295
297
|
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
296
298
|
}
|
|
@@ -13,7 +13,7 @@ import { AppShell } from "./AppShell";
|
|
|
13
13
|
import { EditorGrid } from "../../workbench/components/EditorGrid";
|
|
14
14
|
import { changesTab, makeGroup, splitGroup } from "../../workbench/components/editor-grid";
|
|
15
15
|
import { NERD } from "../../ui/components/primitives/icons";
|
|
16
|
-
import { allowEventLoopUpdates } from "../../testing/test-support";
|
|
16
|
+
import { allowEventLoopUpdates, settle } from "../../testing/test-support";
|
|
17
17
|
|
|
18
18
|
const grid = makeGroup([changesTab()]);
|
|
19
19
|
|
|
@@ -59,8 +59,9 @@ async function zoomIconColumn(
|
|
|
59
59
|
{ width, height: 8 },
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Split widths commit through React after the frame measurement. Renderer idle alone
|
|
63
|
+
// can still expose the initial fractional split, before its whole-cell widths land.
|
|
64
|
+
await settle(setup);
|
|
64
65
|
const column = setup.captureCharFrame().split("\n")[0]!.indexOf(NERD.zoom);
|
|
65
66
|
|
|
66
67
|
setup.renderer.destroy();
|
|
@@ -62,6 +62,7 @@ const EMPTY_SNAPSHOT: ControllerSnapshot = {
|
|
|
62
62
|
function baseController(): ReviewController {
|
|
63
63
|
return {
|
|
64
64
|
readOnly: false,
|
|
65
|
+
createEmptyThread: mock(async () => {}),
|
|
65
66
|
subscribe: mock(() => () => {}),
|
|
66
67
|
getSnapshot: mock(() => EMPTY_SNAPSHOT),
|
|
67
68
|
connect: mock(),
|
|
@@ -34,6 +34,9 @@ export interface AnnotatableFileViewProps {
|
|
|
34
34
|
onAddComment: (anchor: Anchor, body: string) => void;
|
|
35
35
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
36
36
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
37
|
+
onInvoke?: () => void;
|
|
38
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
39
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
37
40
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
38
41
|
onExit: () => void;
|
|
39
42
|
theme?: Theme;
|
|
@@ -126,6 +129,9 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
|
|
|
126
129
|
}
|
|
127
130
|
onReply={props.onReply}
|
|
128
131
|
onUpdateAnnotation={props.onUpdateAnnotation}
|
|
132
|
+
onInvoke={props.onInvoke}
|
|
133
|
+
isAnnotationReadOnly={props.isAnnotationReadOnly}
|
|
134
|
+
annotationAction={props.annotationAction}
|
|
129
135
|
resolveAuthorLabel={props.resolveAuthorLabel}
|
|
130
136
|
onExit={props.onExit}
|
|
131
137
|
theme={props.theme}
|
|
@@ -98,6 +98,9 @@ export interface DiffContentViewProps {
|
|
|
98
98
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
99
99
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
100
100
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
101
|
+
onInvoke?: () => void;
|
|
102
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
103
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
101
104
|
/** The author's display name for a comment's hover tooltip. */
|
|
102
105
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
103
106
|
onNavCommand?: (key: KeyEvent, selection: TextSpan | null) => boolean;
|
|
@@ -542,6 +545,9 @@ export function DiffContentView({
|
|
|
542
545
|
onAnnotate,
|
|
543
546
|
onReply,
|
|
544
547
|
onUpdateAnnotation,
|
|
548
|
+
onInvoke,
|
|
549
|
+
isAnnotationReadOnly,
|
|
550
|
+
annotationAction,
|
|
545
551
|
resolveAuthorLabel,
|
|
546
552
|
onNavCommand,
|
|
547
553
|
onExit,
|
|
@@ -610,6 +616,9 @@ export function DiffContentView({
|
|
|
610
616
|
onAnnotate,
|
|
611
617
|
onReply,
|
|
612
618
|
onUpdateAnnotation,
|
|
619
|
+
onInvoke,
|
|
620
|
+
isAnnotationReadOnly,
|
|
621
|
+
annotationAction,
|
|
613
622
|
dragViewport: () => scrollBoxDragViewport(scrollRef.current),
|
|
614
623
|
resolveAuthorLabel: (annotation) => {
|
|
615
624
|
const label = resolveAuthorLabel?.(annotation);
|
|
@@ -36,7 +36,12 @@ import { wrapLines, type MarkRange, type VisualLine } from "../mark-runs";
|
|
|
36
36
|
import { MarkdownGridBlock } from "./MarkdownGridBlock";
|
|
37
37
|
import { useFrameMeasure } from "../../ui/use-frame-measure";
|
|
38
38
|
import { scrollBoxDragViewport, useTerminalVirtualizer } from "../../ui/use-terminal-virtualizer";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
useAnnotationSurface,
|
|
41
|
+
type LineSource,
|
|
42
|
+
type PromptFocusRequest,
|
|
43
|
+
type PromptRestoreRequest,
|
|
44
|
+
} from "../../annotations/use-annotation-surface";
|
|
40
45
|
import { NavModeHint } from "../../keyboard/components/NavModeHint";
|
|
41
46
|
import { DiscussionMarkerRail } from "../../annotations/components/DiscussionMarkerRail";
|
|
42
47
|
import { useComponentTheme } from "../../appearance/components/theme-context";
|
|
@@ -150,6 +155,17 @@ export const THREAD_VIEW_CHEATSHEET: CheatsheetSection[] = [
|
|
|
150
155
|
];
|
|
151
156
|
|
|
152
157
|
export interface ThreadViewProps {
|
|
158
|
+
/** Agent transcripts permit feedback only on finalized answer blocks. */
|
|
159
|
+
canAnnotateBlock?: (blockIndex: number) => boolean;
|
|
160
|
+
isAnnotationReadOnly?: (id: string) => boolean;
|
|
161
|
+
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
162
|
+
requestedBlock?: { blockIndex: number };
|
|
163
|
+
onInvoke?: () => void;
|
|
164
|
+
/** The final prompt line uses a plain composer instead of a comment card. */
|
|
165
|
+
isPromptBlock?: (blockIndex: number) => boolean;
|
|
166
|
+
promptFocusRequest?: PromptFocusRequest;
|
|
167
|
+
promptRestoreRequest?: PromptRestoreRequest;
|
|
168
|
+
renderBlock?: (index: number) => React.ReactNode | undefined;
|
|
153
169
|
session: Thread;
|
|
154
170
|
display: DisplayBlock[];
|
|
155
171
|
marks: Map<number, Mark[]>;
|
|
@@ -202,13 +218,22 @@ export function ThreadView({
|
|
|
202
218
|
onNavCommand,
|
|
203
219
|
onExit,
|
|
204
220
|
theme,
|
|
221
|
+
canAnnotateBlock,
|
|
222
|
+
isAnnotationReadOnly,
|
|
223
|
+
annotationAction,
|
|
224
|
+
requestedBlock,
|
|
225
|
+
onInvoke,
|
|
226
|
+
isPromptBlock,
|
|
227
|
+
promptFocusRequest,
|
|
228
|
+
promptRestoreRequest,
|
|
229
|
+
renderBlock,
|
|
205
230
|
}: ThreadViewProps): React.ReactNode {
|
|
206
231
|
const tokens = useComponentTheme(theme);
|
|
207
232
|
const source: LineSource = {
|
|
208
233
|
count: display.length,
|
|
209
234
|
// the surface hit-tests and paints in rendered text (inline markers concealed); a grid anchors on its raw source
|
|
210
235
|
textAt: (blockIndex) => renderedText(display[blockIndex]!),
|
|
211
|
-
annotatable: () => true,
|
|
236
|
+
annotatable: canAnnotateBlock ?? (() => true),
|
|
212
237
|
};
|
|
213
238
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
214
239
|
const surface = useAnnotationSurface({
|
|
@@ -228,6 +253,13 @@ export function ThreadView({
|
|
|
228
253
|
onAnnotate,
|
|
229
254
|
onReply,
|
|
230
255
|
onUpdateAnnotation,
|
|
256
|
+
isAnnotationReadOnly,
|
|
257
|
+
annotationAction,
|
|
258
|
+
requestedBlock,
|
|
259
|
+
onInvoke,
|
|
260
|
+
isPromptBlock,
|
|
261
|
+
promptFocusRequest,
|
|
262
|
+
promptRestoreRequest,
|
|
231
263
|
dragViewport: () => scrollBoxDragViewport(scrollRef.current),
|
|
232
264
|
resolveAuthorLabel,
|
|
233
265
|
onNavCommand,
|
|
@@ -316,7 +348,7 @@ export function ThreadView({
|
|
|
316
348
|
</span>
|
|
317
349
|
);
|
|
318
350
|
|
|
319
|
-
const lineRowFor = (context: LineContext): React.ReactNode => {
|
|
351
|
+
const lineRowFor = (context: LineContext, hasComposer = false): React.ReactNode => {
|
|
320
352
|
const {
|
|
321
353
|
blockIndex,
|
|
322
354
|
roleRuns,
|
|
@@ -329,6 +361,9 @@ export function ThreadView({
|
|
|
329
361
|
syntaxSpans,
|
|
330
362
|
} = context;
|
|
331
363
|
|
|
364
|
+
// The prompt composer occupies its blank line instead of adding a row below it.
|
|
365
|
+
if (isPromptBlock?.(blockIndex) && hasComposer) return null;
|
|
366
|
+
|
|
332
367
|
return (
|
|
333
368
|
<box key={`line-${lineIndex}`} style={{ flexDirection: "row" }}>
|
|
334
369
|
<text selectable={false}>
|
|
@@ -366,6 +401,13 @@ export function ThreadView({
|
|
|
366
401
|
};
|
|
367
402
|
|
|
368
403
|
const blockNodeFor = (blockIndex: number): React.ReactNode => {
|
|
404
|
+
const custom = renderBlock?.(blockIndex);
|
|
405
|
+
if (custom !== undefined)
|
|
406
|
+
return (
|
|
407
|
+
<box key={`custom-${blockIndex}`} ref={virtual.measureRef(blockIndex)}>
|
|
408
|
+
{custom}
|
|
409
|
+
</box>
|
|
410
|
+
);
|
|
369
411
|
const block = display[blockIndex]!;
|
|
370
412
|
const { baseFg, baseAttributes, marker } = blockStyle(block, tokens);
|
|
371
413
|
// list items of one list stay tight; every other block sits a blank row below its neighbour
|
|
@@ -422,22 +464,24 @@ export function ThreadView({
|
|
|
422
464
|
const line = lines[lineIndex]!;
|
|
423
465
|
const isLastLine = lineIndex === lines.length - 1;
|
|
424
466
|
|
|
467
|
+
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
425
468
|
lineRows.push(
|
|
426
|
-
lineRowFor(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
469
|
+
lineRowFor(
|
|
470
|
+
{
|
|
471
|
+
blockIndex,
|
|
472
|
+
roleRuns,
|
|
473
|
+
line,
|
|
474
|
+
lineIndex,
|
|
475
|
+
ranges,
|
|
476
|
+
marker,
|
|
477
|
+
baseFg,
|
|
478
|
+
baseAttributes,
|
|
479
|
+
syntaxSpans,
|
|
480
|
+
},
|
|
481
|
+
cards.length > 0,
|
|
482
|
+
),
|
|
437
483
|
);
|
|
438
484
|
if (isLastLine) lineRows.push(headingRule(block));
|
|
439
|
-
const cards = surface.cardsAfterLine(blockIndex, line, isLastLine);
|
|
440
|
-
|
|
441
485
|
lineRows.push(...cards);
|
|
442
486
|
if (cards.length > 0 && !isLastLine) {
|
|
443
487
|
lineRows.push(<box key={`gap-${lineIndex}`} style={{ height: 1 }} />);
|
|
@@ -659,3 +659,18 @@ describe("the /lgtm default quick action", () => {
|
|
|
659
659
|
expect(quickActionBody(lgtm!)).toBe("LGTM\n\nThis looks good to me.");
|
|
660
660
|
});
|
|
661
661
|
});
|
|
662
|
+
|
|
663
|
+
test("Thread agents default off and require a boolean experimental TOML flag", () => {
|
|
664
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-agent-config-"));
|
|
665
|
+
const path = join(dir, "config.toml");
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
|
|
669
|
+
writeFileSync(path, "[experimental]\nthread_agent = true\n");
|
|
670
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(true);
|
|
671
|
+
writeFileSync(path, '[experimental]\nthread_agent = "true"\n');
|
|
672
|
+
expect(loadConfig({ userConfigPath: path }).experimental.threadAgent).toBe(false);
|
|
673
|
+
} finally {
|
|
674
|
+
rmSync(dir, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
});
|