@cueloop/client 0.1.0-alpha.100 → 0.1.0-alpha.102
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.tsx +15 -1
- package/src/annotations/use-annotation-surface.tsx +64 -21
- package/src/app/App.thread-agent.test.ts +8 -0
- package/src/app/App.tsx +23 -6
- package/src/diff/components/AnnotatableFileView.tsx +2 -2
- package/src/diff/components/DiffContentView.tsx +3 -3
- package/src/integrations/components/ShareDialog.agent.test.tsx +50 -0
- package/src/integrations/components/ShareDialog.tsx +27 -3
- package/src/integrations/components/share-dialog-store.ts +4 -0
- package/src/markdown/components/ThreadView.tsx +4 -4
- package/src/thread/agent-transcript.ts +11 -10
- package/src/thread/components/AgentThreadPane.test.tsx +335 -4
- package/src/thread/components/AgentThreadPane.tsx +61 -31
- package/src/thread/thread-controller.ts +30 -22
- package/src/thread/use-thread-agent.ts +7 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.102",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
33
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
30
|
+
"@cueloop/daemon": "0.1.0-alpha.102",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.102",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.102",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.102",
|
|
34
34
|
"@opentui/core": "^0.5.14",
|
|
35
35
|
"@opentui/keymap": "0.5.14",
|
|
36
36
|
"@opentui/react": "^0.5.14",
|
|
@@ -57,6 +57,7 @@ export function Composer({
|
|
|
57
57
|
onInput,
|
|
58
58
|
placeholder,
|
|
59
59
|
agentEnabled = false,
|
|
60
|
+
focusRequest,
|
|
60
61
|
}: {
|
|
61
62
|
seed: string;
|
|
62
63
|
glyph: string | null;
|
|
@@ -66,6 +67,7 @@ export function Composer({
|
|
|
66
67
|
onInput: (text: string, caret: number) => void;
|
|
67
68
|
placeholder?: string;
|
|
68
69
|
agentEnabled?: boolean;
|
|
70
|
+
focusRequest?: number;
|
|
69
71
|
}): React.ReactNode {
|
|
70
72
|
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
71
73
|
const pastedImageCount = useRef(0);
|
|
@@ -105,6 +107,9 @@ export function Composer({
|
|
|
105
107
|
onReady(() => editorRef.current?.plainText ?? "");
|
|
106
108
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
109
|
}, []);
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
editorRef.current?.focus();
|
|
112
|
+
}, [focusRequest]);
|
|
108
113
|
|
|
109
114
|
return (
|
|
110
115
|
<box style={{ flexDirection: "row" }}>
|
|
@@ -312,12 +317,14 @@ export function CommentRow({
|
|
|
312
317
|
tokens,
|
|
313
318
|
authorLabel,
|
|
314
319
|
action,
|
|
320
|
+
onFocus,
|
|
315
321
|
}: {
|
|
316
322
|
annotation: Annotation;
|
|
317
323
|
tokens: Theme;
|
|
318
324
|
/** The author's resolved display name, shown as a tooltip when the dot is hovered. */
|
|
319
325
|
authorLabel?: string;
|
|
320
326
|
action?: { label: string; run: () => void };
|
|
327
|
+
onFocus?: () => void;
|
|
321
328
|
}): React.ReactNode {
|
|
322
329
|
// own comments (no author) wear the filled dot, collaborators the outline
|
|
323
330
|
const own = annotation.author === undefined;
|
|
@@ -332,7 +339,14 @@ export function CommentRow({
|
|
|
332
339
|
: {};
|
|
333
340
|
|
|
334
341
|
return (
|
|
335
|
-
<box
|
|
342
|
+
<box
|
|
343
|
+
style={{ flexDirection: "column" }}
|
|
344
|
+
onMouseDown={(event) => {
|
|
345
|
+
if (!onFocus) return;
|
|
346
|
+
event.stopPropagation();
|
|
347
|
+
onFocus();
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
336
350
|
{annotation.body.split("\n").map((line, lineIndex) => (
|
|
337
351
|
<box key={lineIndex} style={{ flexDirection: "row" }}>
|
|
338
352
|
<box style={{ flexShrink: 0 }} {...(lineIndex === 0 ? dotHover : {})}>
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
slashItemsFrom,
|
|
48
48
|
} from "../keyboard/slash-palette";
|
|
49
49
|
import { SlashSkillsContext } from "../keyboard/skills";
|
|
50
|
-
import { discussionsFrom, type Discussion } from "./discussions";
|
|
50
|
+
import { discussionsFrom, spanKey, type Discussion } from "./discussions";
|
|
51
51
|
import {
|
|
52
52
|
CommentRow,
|
|
53
53
|
Composer,
|
|
@@ -107,13 +107,13 @@ export interface AnnotationSurfaceOptions {
|
|
|
107
107
|
) => boolean;
|
|
108
108
|
focusedAnnotationId?: string;
|
|
109
109
|
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
110
|
-
onAnnotate: (span: TextSpan, body: string) => void;
|
|
111
|
-
onReply: (rootAnnotationId: string, body: string) => void;
|
|
110
|
+
onAnnotate: (span: TextSpan, body: string) => string | void;
|
|
111
|
+
onReply: (rootAnnotationId: string, body: string) => string | void;
|
|
112
112
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
113
113
|
isAnnotationReadOnly?: (id: string) => boolean;
|
|
114
114
|
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
115
115
|
requestedBlock?: { blockIndex: number };
|
|
116
|
-
onInvoke?: () => void;
|
|
116
|
+
onInvoke?: (commentId?: string) => void;
|
|
117
117
|
isPromptBlock?: (blockIndex: number) => boolean;
|
|
118
118
|
promptFocusRequest?: PromptFocusRequest;
|
|
119
119
|
promptRestoreRequest?: PromptRestoreRequest;
|
|
@@ -277,19 +277,28 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
277
277
|
// app owns it (onFocusAnnotation), the focused discussion is the one holding
|
|
278
278
|
// the focused card and focusing here names the root comment; standalone, it
|
|
279
279
|
// is local state
|
|
280
|
-
const [localFocus, setLocalFocus] = useState<string | null>(null);
|
|
281
|
-
const
|
|
282
|
-
onFocusAnnotation === undefined
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
280
|
+
const [localFocus, setLocalFocus] = useState<{ key: string; annotationId?: string } | null>(null);
|
|
281
|
+
const getFocusedDiscussion = (): string | null => {
|
|
282
|
+
if (onFocusAnnotation === undefined) return localFocus?.key ?? null;
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
discussions.find((discussion) =>
|
|
286
|
+
discussion.annotations.some((annotation) => annotation.id === focusedAnnotationId),
|
|
287
|
+
)?.key ?? null
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
const focusedDiscussion = getFocusedDiscussion();
|
|
287
291
|
const setFocusedDiscussion = (key: string | null): void => {
|
|
288
|
-
if (onFocusAnnotation === undefined) return setLocalFocus(key);
|
|
292
|
+
if (onFocusAnnotation === undefined) return setLocalFocus(key === null ? null : { key });
|
|
289
293
|
onFocusAnnotation(discussions.find((discussion) => discussion.key === key)?.rootId);
|
|
290
294
|
};
|
|
295
|
+
const focusSavedComment = (annotationId: string, key: string): void => {
|
|
296
|
+
if (onFocusAnnotation) onFocusAnnotation(annotationId);
|
|
297
|
+
else setLocalFocus({ key, annotationId });
|
|
298
|
+
};
|
|
291
299
|
const [folded, setFolded] = useState<Set<string>>(new Set());
|
|
292
300
|
const [composeText, setComposeText] = useState("");
|
|
301
|
+
const [composerFocusRequest, setComposerFocusRequest] = useState(0);
|
|
293
302
|
const composeTextRef = useRef("");
|
|
294
303
|
const setDraft = (text: string): void => {
|
|
295
304
|
composeTextRef.current = text;
|
|
@@ -436,6 +445,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
436
445
|
readComposerText.current = null;
|
|
437
446
|
composeRef.current = state;
|
|
438
447
|
setCompose(state);
|
|
448
|
+
setComposerFocusRequest((request) => request + 1);
|
|
439
449
|
setDraft(state.seed);
|
|
440
450
|
setCaretOffset(state.seed.length);
|
|
441
451
|
setSlashIndex(0);
|
|
@@ -471,7 +481,8 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
471
481
|
if (!target || body.trim().length === 0) return;
|
|
472
482
|
if (target.editAnnotationId !== null) {
|
|
473
483
|
onUpdateAnnotation(target.editAnnotationId, body);
|
|
474
|
-
if (
|
|
484
|
+
if (target.discussionKey) focusSavedComment(target.editAnnotationId, target.discussionKey);
|
|
485
|
+
if (invoke) onInvoke?.(target.editAnnotationId);
|
|
475
486
|
|
|
476
487
|
return;
|
|
477
488
|
}
|
|
@@ -479,8 +490,10 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
479
490
|
const discussion = discussions.find((candidate) => candidate.key === target.discussionKey);
|
|
480
491
|
|
|
481
492
|
if (discussion) {
|
|
482
|
-
onReply(discussion.rootId, body);
|
|
483
|
-
|
|
493
|
+
const commentId = onReply(discussion.rootId, body);
|
|
494
|
+
|
|
495
|
+
if (commentId) focusSavedComment(commentId, discussion.key);
|
|
496
|
+
if (invoke && commentId) onInvoke?.(commentId);
|
|
484
497
|
|
|
485
498
|
return;
|
|
486
499
|
}
|
|
@@ -488,10 +501,22 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
488
501
|
const span = target.span ?? caretSpan();
|
|
489
502
|
|
|
490
503
|
if (span) {
|
|
491
|
-
onAnnotate(span, body);
|
|
492
|
-
|
|
504
|
+
const commentId = onAnnotate(span, body);
|
|
505
|
+
|
|
506
|
+
completeSavedComment(span, commentId, prompt, invoke);
|
|
493
507
|
}
|
|
494
508
|
};
|
|
509
|
+
const completeSavedComment = (
|
|
510
|
+
span: TextSpan,
|
|
511
|
+
commentId: string | void,
|
|
512
|
+
prompt: boolean,
|
|
513
|
+
invoke: boolean,
|
|
514
|
+
): void => {
|
|
515
|
+
if (!prompt && commentId) {
|
|
516
|
+
focusSavedComment(commentId, spanKey(span));
|
|
517
|
+
}
|
|
518
|
+
if (invoke && (prompt || commentId)) onInvoke?.(commentId || undefined);
|
|
519
|
+
};
|
|
495
520
|
/** A new discussion on the typing anchor; the card renders under the span's last block. */
|
|
496
521
|
const openNewCompose = (seed: string): void => {
|
|
497
522
|
const span = caretSpan();
|
|
@@ -852,6 +877,16 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
852
877
|
openNewCompose(sequence);
|
|
853
878
|
};
|
|
854
879
|
|
|
880
|
+
const invokeFocusedComment = (): void => {
|
|
881
|
+
const discussion = discussions.find((candidate) => candidate.key === focusedDiscussion);
|
|
882
|
+
const focusedCommentId = focusedAnnotationId ?? localFocus?.annotationId;
|
|
883
|
+
const commentId =
|
|
884
|
+
discussion?.annotations.find((annotation) => annotation.id === focusedCommentId)?.id ??
|
|
885
|
+
discussion?.annotations.at(-1)?.id;
|
|
886
|
+
|
|
887
|
+
onInvoke?.(commentId);
|
|
888
|
+
};
|
|
889
|
+
|
|
855
890
|
useSharedKeyboard((key) => {
|
|
856
891
|
if (suspended) return;
|
|
857
892
|
if (key.ctrl && key.name === "q") return onExit();
|
|
@@ -861,7 +896,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
861
896
|
if (isAgentInvokeKey(key, onInvoke)) {
|
|
862
897
|
key.preventDefault();
|
|
863
898
|
|
|
864
|
-
return
|
|
899
|
+
return invokeFocusedComment();
|
|
865
900
|
}
|
|
866
901
|
if (key.name === "escape") {
|
|
867
902
|
// from type mode esc only enters nav, so a held mark survives for `c`
|
|
@@ -903,6 +938,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
903
938
|
tokens={tokens}
|
|
904
939
|
onSave={saveComment}
|
|
905
940
|
agentEnabled={Boolean(onInvoke)}
|
|
941
|
+
focusRequest={composerFocusRequest}
|
|
906
942
|
onReady={(readText) => {
|
|
907
943
|
readComposerText.current = readText;
|
|
908
944
|
composerReady.current = true;
|
|
@@ -948,6 +984,11 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
948
984
|
tokens={tokens}
|
|
949
985
|
authorLabel={resolveAuthorLabel?.(annotation)}
|
|
950
986
|
action={annotationAction?.(annotation.id)}
|
|
987
|
+
onFocus={() => {
|
|
988
|
+
blurSaveCompose();
|
|
989
|
+
focusSavedComment(annotation.id, discussion.key);
|
|
990
|
+
setCursor(discussion.blockIndex);
|
|
991
|
+
}}
|
|
951
992
|
/>
|
|
952
993
|
),
|
|
953
994
|
};
|
|
@@ -996,6 +1037,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
996
1037
|
return ranges;
|
|
997
1038
|
};
|
|
998
1039
|
|
|
1040
|
+
const composeBlockIndex = compose?.kind === "prompt" ? source.count - 1 : compose?.blockIndex;
|
|
999
1041
|
const cardsAfterLine = (
|
|
1000
1042
|
blockIndex: number,
|
|
1001
1043
|
line: VisualLine,
|
|
@@ -1004,7 +1046,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1004
1046
|
const endsInLine = (end: number): boolean =>
|
|
1005
1047
|
(end === 0 && line.start === 0) || (end - 1 >= line.start && end - 1 < line.end);
|
|
1006
1048
|
const nodes: React.ReactNode[] = [];
|
|
1007
|
-
const composeHere = compose &&
|
|
1049
|
+
const composeHere = compose && composeBlockIndex === blockIndex;
|
|
1008
1050
|
const newComposeHere = Boolean(composeHere && compose.discussionKey === null && composerNode);
|
|
1009
1051
|
const composeAnchoredHere =
|
|
1010
1052
|
newComposeHere && (compose!.span ? endsInLine(compose!.span.end.char) : isLastLine);
|
|
@@ -1151,7 +1193,8 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1151
1193
|
const geometry = allGeometry();
|
|
1152
1194
|
const promptLine = geometry.find((entry) => isPromptBlock?.(entry.blockIndex));
|
|
1153
1195
|
const lastReplyLine = geometry
|
|
1154
|
-
|
|
1196
|
+
// Activity blocks render outside the text geometry, so use the last measured line.
|
|
1197
|
+
.filter((entry) => entry.blockIndex < blockIndex)
|
|
1155
1198
|
.sort((left, right) => right.y - left.y)[0];
|
|
1156
1199
|
const promptStart = promptLine ? promptLine.y - 1 : lastReplyLine && lastReplyLine.y + 1;
|
|
1157
1200
|
const viewport = dragViewport?.();
|
|
@@ -1226,7 +1269,7 @@ export function useAnnotationSurface(options: AnnotationSurfaceOptions): Annotat
|
|
|
1226
1269
|
|
|
1227
1270
|
function isAgentInvokeKey(
|
|
1228
1271
|
key: { name: string; ctrl?: boolean; meta?: boolean; super?: boolean },
|
|
1229
|
-
onInvoke?: () => void,
|
|
1272
|
+
onInvoke?: (commentId?: string) => void,
|
|
1230
1273
|
): boolean {
|
|
1231
1274
|
return Boolean(
|
|
1232
1275
|
onInvoke &&
|
|
@@ -10,3 +10,11 @@ test("experimental agent UI requires a local owner and an enabled flag", () => {
|
|
|
10
10
|
expect(canRunThreadAgent({ ...local, editing: true })).toBe(false);
|
|
11
11
|
expect(canRunThreadAgent({ ...local, shared: true })).toBe(false);
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
test("a per-share agent capability enables collaborator chat without a gateway flag", () => {
|
|
15
|
+
const shared = { enabled: false, owner: false, editing: false, shared: true };
|
|
16
|
+
|
|
17
|
+
expect(canRunThreadAgent(shared)).toBe(false);
|
|
18
|
+
expect(canRunThreadAgent({ ...shared, sharedAgent: true })).toBe(true);
|
|
19
|
+
expect(canRunThreadAgent({ ...shared, sharedAgent: true, editing: true })).toBe(false);
|
|
20
|
+
});
|
package/src/app/App.tsx
CHANGED
|
@@ -119,6 +119,7 @@ export interface AppProps {
|
|
|
119
119
|
sessionId?: string;
|
|
120
120
|
initialReview?: { session: Thread; diff: { patch: string; files: DiffFileContents[] } };
|
|
121
121
|
initialClient?: ThreadClient;
|
|
122
|
+
agentClient?: import("../thread/use-thread-agent").ThreadAgentClient;
|
|
122
123
|
cwd?: string;
|
|
123
124
|
readOnly?: boolean;
|
|
124
125
|
onExit?: (code: number) => void;
|
|
@@ -531,6 +532,7 @@ export function App({
|
|
|
531
532
|
sessionId,
|
|
532
533
|
initialReview,
|
|
533
534
|
initialClient,
|
|
535
|
+
agentClient,
|
|
534
536
|
cwd,
|
|
535
537
|
readOnly = false,
|
|
536
538
|
onExit,
|
|
@@ -840,7 +842,7 @@ export function App({
|
|
|
840
842
|
const agentActions = useRef<(id: string) => { label: string; run: () => void } | undefined>(
|
|
841
843
|
() => undefined,
|
|
842
844
|
);
|
|
843
|
-
const agentInvoke = useRef<(() => void) | undefined>(undefined);
|
|
845
|
+
const agentInvoke = useRef<((commentId?: string) => void) | undefined>(undefined);
|
|
844
846
|
const [agentState, setAgentState] = useState<import("@cueloop/schema").ThreadAgentState>();
|
|
845
847
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
846
848
|
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
@@ -1315,11 +1317,13 @@ export function App({
|
|
|
1315
1317
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1316
1318
|
<AgentThreadPrototype
|
|
1317
1319
|
key={activeSession.id}
|
|
1320
|
+
client={agentClient}
|
|
1318
1321
|
enabled={canRunThreadAgent({
|
|
1319
1322
|
enabled: threadAgentEnabled,
|
|
1320
1323
|
owner: isOwner,
|
|
1321
1324
|
editing: bodyEditing.editing,
|
|
1322
1325
|
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1326
|
+
sharedAgent: sharedAgentAvailable(agentClient, activeSession),
|
|
1323
1327
|
})}
|
|
1324
1328
|
observer={observer}
|
|
1325
1329
|
pixelPrototype={isPixelPrototype}
|
|
@@ -1480,7 +1484,7 @@ export function App({
|
|
|
1480
1484
|
// a diff thread's Changes view is the artifact itself; any other thread's is the
|
|
1481
1485
|
// live working-tree diff, so its notes carry a file target and anchor there
|
|
1482
1486
|
onAnnotate: (span, body) =>
|
|
1483
|
-
|
|
1487
|
+
controller.annotate(
|
|
1484
1488
|
"comment",
|
|
1485
1489
|
span.start.blockIndex,
|
|
1486
1490
|
span.start.char,
|
|
@@ -1492,11 +1496,11 @@ export function App({
|
|
|
1492
1496
|
: { kind: "file", path: "", rev: "worktree" },
|
|
1493
1497
|
),
|
|
1494
1498
|
onReply: (rootAnnotationId, body) =>
|
|
1495
|
-
|
|
1499
|
+
controller.reply(rootAnnotationId, body),
|
|
1496
1500
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1497
1501
|
onInvoke: agentActive
|
|
1498
|
-
? () => {
|
|
1499
|
-
agentInvoke.current?.();
|
|
1502
|
+
? (commentId) => {
|
|
1503
|
+
agentInvoke.current?.(commentId);
|
|
1500
1504
|
setFocusedPane("thread");
|
|
1501
1505
|
}
|
|
1502
1506
|
: undefined,
|
|
@@ -1592,6 +1596,12 @@ export function App({
|
|
|
1592
1596
|
threadName={shareThreadName(session)}
|
|
1593
1597
|
links={shareLinksFor(session)}
|
|
1594
1598
|
isOwner={isOwner}
|
|
1599
|
+
agentAvailable={canRunThreadAgent({
|
|
1600
|
+
enabled: threadAgentEnabled,
|
|
1601
|
+
owner: isOwner,
|
|
1602
|
+
editing: false,
|
|
1603
|
+
shared: isSharedThreadConnection(openClient, shareTransport),
|
|
1604
|
+
})}
|
|
1595
1605
|
onCreateLink={(input) => controller.createShareLink(input)}
|
|
1596
1606
|
onUpdateLink={(id, input) => controller.updateShareLink(id, input)}
|
|
1597
1607
|
onDeleteLink={(id) => controller.deleteShareLink(id)}
|
|
@@ -1618,8 +1628,15 @@ export function canRunThreadAgent(options: {
|
|
|
1618
1628
|
owner: boolean;
|
|
1619
1629
|
editing: boolean;
|
|
1620
1630
|
shared: boolean;
|
|
1631
|
+
sharedAgent?: boolean;
|
|
1621
1632
|
}): boolean {
|
|
1622
|
-
|
|
1633
|
+
if (options.editing) return false;
|
|
1634
|
+
|
|
1635
|
+
return Boolean(options.sharedAgent || (options.enabled && options.owner && !options.shared));
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function sharedAgentAvailable(client: AppProps["agentClient"], thread: Thread): boolean {
|
|
1639
|
+
return Boolean(client && thread.shares?.[0]?.agentEnabled);
|
|
1623
1640
|
}
|
|
1624
1641
|
|
|
1625
1642
|
function isSubmittedComment(
|
|
@@ -31,9 +31,9 @@ export interface AnnotatableFileViewProps {
|
|
|
31
31
|
onCursorChange?: (rowIndex: number) => void;
|
|
32
32
|
onObserverBlocked?: (reason: "observer" | "resolved") => void;
|
|
33
33
|
onAddComment: (anchor: Anchor, body: string) => void;
|
|
34
|
-
onReply: (rootAnnotationId: string, body: string) => void;
|
|
34
|
+
onReply: (rootAnnotationId: string, body: string) => string | void;
|
|
35
35
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
36
|
-
onInvoke?: () => void;
|
|
36
|
+
onInvoke?: (commentId?: string) => void;
|
|
37
37
|
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
38
38
|
isAnnotationReadOnly?: (id: string) => boolean;
|
|
39
39
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
@@ -86,10 +86,10 @@ export interface DiffContentViewProps {
|
|
|
86
86
|
onCursorChange?: (rowIndex: number) => void;
|
|
87
87
|
focusedAnnotationId?: string;
|
|
88
88
|
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
89
|
-
onAnnotate: (span: TextSpan, body: string) => void;
|
|
90
|
-
onReply: (rootAnnotationId: string, body: string) => void;
|
|
89
|
+
onAnnotate: (span: TextSpan, body: string) => string | void;
|
|
90
|
+
onReply: (rootAnnotationId: string, body: string) => string | void;
|
|
91
91
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
92
|
-
onInvoke?: () => void;
|
|
92
|
+
onInvoke?: (commentId?: string) => void;
|
|
93
93
|
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
94
94
|
isAnnotationReadOnly?: (id: string) => boolean;
|
|
95
95
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DARK } from "../../appearance/theme";
|
|
5
|
+
import { press, waitForText } from "../../testing/test-support";
|
|
6
|
+
import { ShareDialog } from "./ShareDialog";
|
|
7
|
+
import { newWizardDraft, shareDialogStore } from "./share-dialog-store";
|
|
8
|
+
import type { NewShareLink } from "../../thread/thread-controller";
|
|
9
|
+
|
|
10
|
+
for (const enabled of [false, true])
|
|
11
|
+
test(`share agent toggle is ${enabled ? "opt-in" : "hidden"}`, async () => {
|
|
12
|
+
const created: NewShareLink[] = [];
|
|
13
|
+
|
|
14
|
+
shareDialogStore.getState().reset();
|
|
15
|
+
shareDialogStore.getState().openWizard(newWizardDraft("Review"));
|
|
16
|
+
const setup = await testRender(
|
|
17
|
+
<ShareDialog
|
|
18
|
+
isOpen
|
|
19
|
+
isOwner
|
|
20
|
+
agentAvailable={enabled}
|
|
21
|
+
links={[]}
|
|
22
|
+
threadName="Review"
|
|
23
|
+
theme={DARK}
|
|
24
|
+
onCreateLink={(input) => created.push(input)}
|
|
25
|
+
onUpdateLink={() => {}}
|
|
26
|
+
onDeleteLink={() => {}}
|
|
27
|
+
onCopyLink={() => {}}
|
|
28
|
+
onClose={() => {}}
|
|
29
|
+
/>,
|
|
30
|
+
{ width: 100, height: 26 },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await waitForText(setup, "link name");
|
|
35
|
+
expect(shareDialogStore.getState().wizard?.agentEnabled ?? false).toBe(false);
|
|
36
|
+
expect(setup.captureCharFrame().includes("Allow agent messages")).toBe(enabled);
|
|
37
|
+
await press(setup, "down");
|
|
38
|
+
await press(setup, "down");
|
|
39
|
+
if (enabled) {
|
|
40
|
+
await press(setup, " ");
|
|
41
|
+
await press(setup, "down");
|
|
42
|
+
}
|
|
43
|
+
await press(setup, "enter");
|
|
44
|
+
expect(created).toHaveLength(1);
|
|
45
|
+
expect(created[0]?.agentEnabled).toBe(enabled ? true : undefined);
|
|
46
|
+
} finally {
|
|
47
|
+
setup.renderer.destroy();
|
|
48
|
+
shareDialogStore.getState().reset();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
@@ -68,6 +68,7 @@ function draftForLink(link: ShareLink, threadName: string): ShareWizardDraft {
|
|
|
68
68
|
editingId: link.id,
|
|
69
69
|
name: link.name ?? threadName,
|
|
70
70
|
requireAuth: link.requireAuth,
|
|
71
|
+
agentEnabled: link.agentEnabled,
|
|
71
72
|
allowlist: link.allowlist,
|
|
72
73
|
};
|
|
73
74
|
}
|
|
@@ -259,7 +260,7 @@ function LinksList({
|
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
/** The focus ring positions inside the wizard, in vertical order. */
|
|
262
|
-
type WizardFocus = "name" | "auth" | "allowlist" | "actions";
|
|
263
|
+
type WizardFocus = "name" | "auth" | "allowlist" | "agent" | "actions";
|
|
263
264
|
|
|
264
265
|
function Wizard({
|
|
265
266
|
draft,
|
|
@@ -267,6 +268,8 @@ function Wizard({
|
|
|
267
268
|
saveDisabled,
|
|
268
269
|
onName,
|
|
269
270
|
onToggleAuth,
|
|
271
|
+
agentAvailable,
|
|
272
|
+
onToggleAgent,
|
|
270
273
|
onAllowlist,
|
|
271
274
|
onSave,
|
|
272
275
|
onCancel,
|
|
@@ -278,6 +281,8 @@ function Wizard({
|
|
|
278
281
|
saveDisabled: boolean;
|
|
279
282
|
onName: (name: string) => void;
|
|
280
283
|
onToggleAuth: () => void;
|
|
284
|
+
agentAvailable: boolean;
|
|
285
|
+
onToggleAgent: () => void;
|
|
281
286
|
onAllowlist: (logins: string[]) => void;
|
|
282
287
|
onSave: () => void;
|
|
283
288
|
onCancel: () => void;
|
|
@@ -329,6 +334,13 @@ function Wizard({
|
|
|
329
334
|
/>
|
|
330
335
|
</box>
|
|
331
336
|
) : null}
|
|
337
|
+
{agentAvailable ? (
|
|
338
|
+
<box style={{ flexDirection: "row", alignItems: "center", marginTop: 1 }}>
|
|
339
|
+
<text fg={focus === "agent" ? tokens.accent : tokens.textDim}>Allow agent messages</text>
|
|
340
|
+
<box style={{ flexGrow: 1 }} />
|
|
341
|
+
<Switch on={draft.agentEnabled ?? false} onToggle={onToggleAgent} theme={theme} />
|
|
342
|
+
</box>
|
|
343
|
+
) : null}
|
|
332
344
|
<box style={{ flexGrow: 1 }} />
|
|
333
345
|
<DialogActions
|
|
334
346
|
confirmLabel={draft.editingId ? "save" : "create"}
|
|
@@ -346,6 +358,7 @@ export interface ShareDialogProps {
|
|
|
346
358
|
threadName: string;
|
|
347
359
|
links: ShareLink[];
|
|
348
360
|
isOwner: boolean;
|
|
361
|
+
agentAvailable?: boolean;
|
|
349
362
|
onCreateLink: (input: NewShareLink) => void;
|
|
350
363
|
onUpdateLink: (id: string, input: NewShareLink) => void;
|
|
351
364
|
onDeleteLink: (id: string) => void;
|
|
@@ -359,6 +372,7 @@ export function ShareDialog({
|
|
|
359
372
|
threadName,
|
|
360
373
|
links,
|
|
361
374
|
isOwner,
|
|
375
|
+
agentAvailable = false,
|
|
362
376
|
onCreateLink,
|
|
363
377
|
onUpdateLink,
|
|
364
378
|
onDeleteLink,
|
|
@@ -384,8 +398,13 @@ export function ShareDialog({
|
|
|
384
398
|
shareDialogStore.getState().openWizard(draft);
|
|
385
399
|
};
|
|
386
400
|
|
|
387
|
-
const wizardRing = (): WizardFocus[] =>
|
|
388
|
-
|
|
401
|
+
const wizardRing = (): WizardFocus[] => [
|
|
402
|
+
"name",
|
|
403
|
+
"auth",
|
|
404
|
+
...(wizard?.requireAuth ? ["allowlist" as const] : []),
|
|
405
|
+
...(agentAvailable ? ["agent" as const] : []),
|
|
406
|
+
"actions",
|
|
407
|
+
];
|
|
389
408
|
|
|
390
409
|
const moveWizardFocus = (delta: number): void => {
|
|
391
410
|
const ring = wizardRing();
|
|
@@ -405,6 +424,7 @@ export function ShareDialog({
|
|
|
405
424
|
allowlist: wizard.allowlist,
|
|
406
425
|
};
|
|
407
426
|
|
|
427
|
+
if (agentAvailable) input.agentEnabled = wizard.agentEnabled ?? false;
|
|
408
428
|
if (wizard.editingId) onUpdateLink(wizard.editingId, input);
|
|
409
429
|
else onCreateLink(input);
|
|
410
430
|
backToList();
|
|
@@ -436,6 +456,8 @@ export function ShareDialog({
|
|
|
436
456
|
const activated = name === "return" || name === "enter";
|
|
437
457
|
|
|
438
458
|
if (wizardFocus === "auth" && (activated || name === "space")) return toggleAuth();
|
|
459
|
+
if (wizardFocus === "agent" && (activated || name === "space"))
|
|
460
|
+
return shareDialogStore.getState().setAgentEnabled(!wizard.agentEnabled);
|
|
439
461
|
if (wizardFocus === "actions" && activated) saveWizard();
|
|
440
462
|
};
|
|
441
463
|
|
|
@@ -494,6 +516,8 @@ export function ShareDialog({
|
|
|
494
516
|
>
|
|
495
517
|
{wizard ? (
|
|
496
518
|
<Wizard
|
|
519
|
+
agentAvailable={agentAvailable}
|
|
520
|
+
onToggleAgent={() => shareDialogStore.getState().setAgentEnabled(!wizard.agentEnabled)}
|
|
497
521
|
draft={wizard}
|
|
498
522
|
focus={wizardFocus}
|
|
499
523
|
saveDisabled={saveDisabled}
|
|
@@ -16,6 +16,7 @@ export interface ShareWizardDraft {
|
|
|
16
16
|
name: string;
|
|
17
17
|
requireAuth: boolean;
|
|
18
18
|
allowlist: string[];
|
|
19
|
+
agentEnabled?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ShareDialogState {
|
|
@@ -27,6 +28,7 @@ export interface ShareDialogState {
|
|
|
27
28
|
setName: (name: string) => void;
|
|
28
29
|
setRequireAuth: (requireAuth: boolean) => void;
|
|
29
30
|
setAllowlist: (allowlist: string[]) => void;
|
|
31
|
+
setAgentEnabled: (agentEnabled: boolean) => void;
|
|
30
32
|
reset: () => void;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -46,6 +48,8 @@ export const shareDialogStore = createStore<ShareDialogState>((set) => ({
|
|
|
46
48
|
set((state) => (state.wizard ? { wizard: { ...state.wizard, requireAuth } } : {})),
|
|
47
49
|
setAllowlist: (allowlist) =>
|
|
48
50
|
set((state) => (state.wizard ? { wizard: { ...state.wizard, allowlist } } : {})),
|
|
51
|
+
setAgentEnabled: (agentEnabled) =>
|
|
52
|
+
set((state) => (state.wizard ? { wizard: { ...state.wizard, agentEnabled } } : {})),
|
|
49
53
|
reset: () => set({ category: "external", wizard: null }),
|
|
50
54
|
}));
|
|
51
55
|
|
|
@@ -159,7 +159,7 @@ export interface ThreadViewProps {
|
|
|
159
159
|
isAnnotationReadOnly?: (id: string) => boolean;
|
|
160
160
|
annotationAction?: (id: string) => { label: string; run: () => void } | undefined;
|
|
161
161
|
requestedBlock?: { blockIndex: number };
|
|
162
|
-
onInvoke?: () => void;
|
|
162
|
+
onInvoke?: (commentId?: string) => void;
|
|
163
163
|
isPromptBlock?: (blockIndex: number) => boolean;
|
|
164
164
|
promptFocusRequest?: PromptFocusRequest;
|
|
165
165
|
promptRestoreRequest?: PromptRestoreRequest;
|
|
@@ -177,8 +177,8 @@ export interface ThreadViewProps {
|
|
|
177
177
|
onCursorChange?: (blockIndex: number) => void;
|
|
178
178
|
focusedAnnotationId?: string;
|
|
179
179
|
onFocusAnnotation?: (annotationId: string | undefined) => void;
|
|
180
|
-
onAnnotate: (span: TextSpan, body: string) => void;
|
|
181
|
-
onReply: (rootAnnotationId: string, body: string) => void;
|
|
180
|
+
onAnnotate: (span: TextSpan, body: string) => string | void;
|
|
181
|
+
onReply: (rootAnnotationId: string, body: string) => string | void;
|
|
182
182
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
183
183
|
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
184
184
|
onNavCommand?: (key: KeyEvent, selection: TextSpan | null) => boolean;
|
|
@@ -412,7 +412,7 @@ export function ThreadView({
|
|
|
412
412
|
blockIndex === 0 || tight ? null : <box key="lead-gap" style={{ height: 1 }} />;
|
|
413
413
|
const measuredBox = (children: React.ReactNode): React.ReactNode => (
|
|
414
414
|
<box
|
|
415
|
-
key={`discussion-block-${blockIndex}`}
|
|
415
|
+
key={isPromptBlock?.(blockIndex) ? "prompt-block" : `discussion-block-${blockIndex}`}
|
|
416
416
|
id={`discussion-block-${blockIndex}`}
|
|
417
417
|
ref={virtual.measureRef(blockIndex)}
|
|
418
418
|
style={{ flexDirection: "column" }}
|
|
@@ -195,6 +195,8 @@ export function projectThreadConversation(
|
|
|
195
195
|
display.push(...blocks);
|
|
196
196
|
sources.push(...blocks.map(() => ({ kind: "mirror" as const, commentId })));
|
|
197
197
|
annotations.push(annotation);
|
|
198
|
+
const mirrorAnnotations = [annotation];
|
|
199
|
+
|
|
198
200
|
if (commentId) {
|
|
199
201
|
const replies = [...thread.annotations, ...state.comments].filter(
|
|
200
202
|
(entry) => entry.replyTo === commentId,
|
|
@@ -210,23 +212,22 @@ export function projectThreadConversation(
|
|
|
210
212
|
};
|
|
211
213
|
|
|
212
214
|
annotations.push(replica);
|
|
215
|
+
mirrorAnnotations.push(replica);
|
|
213
216
|
mirrors.set(replica.id, { commentId: reply.id });
|
|
214
217
|
}
|
|
215
218
|
}
|
|
216
219
|
const localMarks = quote.trim()
|
|
217
|
-
? marksByDisplay(
|
|
220
|
+
? marksByDisplay(mirrorAnnotations, blocks)
|
|
218
221
|
: new Map<number, Mark[]>([
|
|
219
222
|
[
|
|
220
223
|
0,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
},
|
|
229
|
-
],
|
|
224
|
+
mirrorAnnotations.map((entry) => ({
|
|
225
|
+
start: 0,
|
|
226
|
+
end: 0,
|
|
227
|
+
role: "mark-comment",
|
|
228
|
+
annotationId: entry.id,
|
|
229
|
+
span: { start: { blockIndex: 0, char: 0 }, end: { blockIndex: 0, char: 0 } },
|
|
230
|
+
})),
|
|
230
231
|
],
|
|
231
232
|
]);
|
|
232
233
|
|