@cueloop/client 0.1.0-alpha.76 → 0.1.0-alpha.78
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.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +14 -2
- package/src/App.export.test.tsx +1 -2
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-review.test.tsx +16 -0
- package/src/App.share-choice.test.tsx +105 -0
- package/src/App.submit-confirm.test.tsx +34 -5
- package/src/App.test.tsx +2 -2
- package/src/App.toast.test.tsx +3 -1
- package/src/App.tsx +180 -5
- package/src/App.walk.test.tsx +2 -2
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +20 -3
- package/src/app-view-model.ts +4 -0
- package/src/components/AnnotatableFileView.tsx +10 -2
- package/src/components/AnnotationCards.tsx +40 -6
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +9 -5
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +2 -0
- package/src/components/ManageAccessDialog.stories.tsx +36 -0
- package/src/components/ManageAccessDialog.test.tsx +94 -0
- package/src/components/ManageAccessDialog.tsx +122 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/ShareChoiceDialog.stories.tsx +34 -0
- package/src/components/ShareChoiceDialog.test.tsx +103 -0
- package/src/components/ShareChoiceDialog.tsx +80 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadTree.tsx +11 -6
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -1
- package/src/components/ThreadView.tsx +71 -8
- package/src/components/Toast.tsx +0 -5
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +277 -61
- package/src/components/primitives/Card.tsx +3 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +8 -1
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config-identity.test.ts +78 -0
- package/src/config.ts +44 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/intent-dispatch.test.ts +17 -2
- package/src/intent-dispatch.ts +7 -1
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +1 -1
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/run.ts +31 -19
- package/src/serve.ts +1 -1
- package/src/share.ts +11 -3
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +1 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +36 -2
- package/src/thread-controller.switch-cache.test.ts +146 -0
- package/src/thread-controller.tree.test.ts +1 -0
- package/src/thread-controller.ts +171 -24
- package/src/use-annotation-surface.tsx +53 -29
- package/src/use-settings-dialog.tsx +24 -0
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import React, { useContext, useLayoutEffect, useRef, useState } from "react";
|
|
10
|
-
import
|
|
10
|
+
import { usePaste } from "@opentui/react";
|
|
11
|
+
import type {
|
|
12
|
+
BoxRenderable,
|
|
13
|
+
KeyBinding,
|
|
14
|
+
MouseEvent as TerminalMouseEvent,
|
|
15
|
+
TextareaRenderable,
|
|
16
|
+
} from "@opentui/core";
|
|
11
17
|
import type { Annotation } from "@cueloop/schema";
|
|
12
18
|
import type { Theme } from "../theme";
|
|
19
|
+
import { useTooltip } from "./Tooltip";
|
|
13
20
|
import { lighten } from "../annotation-palette";
|
|
14
21
|
import { useFrameMeasure } from "../use-frame-measure";
|
|
22
|
+
import { imagePlaceholder, looksLikeBinaryPaste } from "../pasted-image";
|
|
15
23
|
import { skillReferenceRanges, type SlashItem } from "../slash-palette";
|
|
16
24
|
import { PaletteNamesContext } from "../skills";
|
|
17
25
|
import { referenceStyleFor } from "./syntax-highlight";
|
|
@@ -29,7 +37,7 @@ const COMPOSE_KEY_BINDINGS: KeyBinding[] = [
|
|
|
29
37
|
{ name: "return", shift: true, action: "newline" },
|
|
30
38
|
];
|
|
31
39
|
|
|
32
|
-
function composeRowCount(text: string, contentWidth: number): number {
|
|
40
|
+
export function composeRowCount(text: string, contentWidth: number): number {
|
|
33
41
|
const usableWidth = contentWidth > 0 ? contentWidth : Number.MAX_SAFE_INTEGER;
|
|
34
42
|
let visualRowCount = 0;
|
|
35
43
|
|
|
@@ -48,6 +56,7 @@ export function Composer({
|
|
|
48
56
|
onSave,
|
|
49
57
|
onReady,
|
|
50
58
|
onInput,
|
|
59
|
+
placeholder,
|
|
51
60
|
}: {
|
|
52
61
|
seed: string;
|
|
53
62
|
glyph: string;
|
|
@@ -55,12 +64,23 @@ export function Composer({
|
|
|
55
64
|
onSave: (body: string) => void;
|
|
56
65
|
onReady: () => void;
|
|
57
66
|
onInput: (text: string, caret: number) => void;
|
|
67
|
+
placeholder?: string;
|
|
58
68
|
}): React.ReactNode {
|
|
59
69
|
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
70
|
+
const pastedImageCount = useRef(0);
|
|
60
71
|
const [rows, setRows] = useState(1);
|
|
61
72
|
// action and skill names whose "/name" references paint in the accent color
|
|
62
73
|
const referenceNames = useContext(PaletteNamesContext);
|
|
63
74
|
|
|
75
|
+
usePaste((event) => {
|
|
76
|
+
const editor = editorRef.current;
|
|
77
|
+
|
|
78
|
+
if (!editor || !editor.focused || !looksLikeBinaryPaste(event.bytes)) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
pastedImageCount.current += 1;
|
|
81
|
+
editor.editBuffer.insertText(imagePlaceholder(pastedImageCount.current));
|
|
82
|
+
});
|
|
83
|
+
|
|
64
84
|
const paintReferences = (editor: TextareaRenderable): void => {
|
|
65
85
|
const { styleId } = referenceStyleFor(tokens);
|
|
66
86
|
|
|
@@ -92,6 +112,8 @@ export function Composer({
|
|
|
92
112
|
ref={editorRef}
|
|
93
113
|
focused
|
|
94
114
|
initialValue={seed}
|
|
115
|
+
placeholder={placeholder}
|
|
116
|
+
placeholderColor={tokens.textDim}
|
|
95
117
|
cursorStyle={{ style: "block", blinking: true }}
|
|
96
118
|
keyBindings={COMPOSE_KEY_BINDINGS}
|
|
97
119
|
onSubmit={() => onSave(editorRef.current?.plainText ?? "")}
|
|
@@ -241,7 +263,7 @@ export function DiscussionCard({
|
|
|
241
263
|
});
|
|
242
264
|
|
|
243
265
|
return (
|
|
244
|
-
<box style={{ flexDirection: "row", marginTop:
|
|
266
|
+
<box style={{ flexDirection: "row", marginTop: 0, marginLeft: 2 }}>
|
|
245
267
|
<text selectable={false} style={{ flexShrink: 0, width: 1 }}>
|
|
246
268
|
{edgeRows.map((row, index) => (
|
|
247
269
|
<span key={index} fg={row.color}>
|
|
@@ -281,22 +303,34 @@ export function DiscussionCard({
|
|
|
281
303
|
export function CommentRow({
|
|
282
304
|
annotation,
|
|
283
305
|
tokens,
|
|
306
|
+
authorLabel,
|
|
284
307
|
}: {
|
|
285
308
|
annotation: Annotation;
|
|
286
309
|
tokens: Theme;
|
|
310
|
+
/** The author's resolved display name, shown as a tooltip when the dot is hovered. */
|
|
311
|
+
authorLabel?: string;
|
|
287
312
|
}): React.ReactNode {
|
|
288
313
|
// own comments (no author) wear the filled dot, collaborators the outline
|
|
289
314
|
const own = annotation.author === undefined;
|
|
290
315
|
const glyph = own ? "●" : "○";
|
|
291
316
|
const glyphColor = own ? tokens.text : tokens.textMuted;
|
|
317
|
+
const { showTooltip, hideTooltip } = useTooltip();
|
|
318
|
+
const dotHover = authorLabel
|
|
319
|
+
? {
|
|
320
|
+
onMouseOver: (event: TerminalMouseEvent) => showTooltip(authorLabel, event.x, event.y),
|
|
321
|
+
onMouseOut: hideTooltip,
|
|
322
|
+
}
|
|
323
|
+
: {};
|
|
292
324
|
|
|
293
325
|
return (
|
|
294
326
|
<box style={{ flexDirection: "column" }}>
|
|
295
327
|
{annotation.body.split("\n").map((line, lineIndex) => (
|
|
296
328
|
<box key={lineIndex} style={{ flexDirection: "row" }}>
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
<box style={{ flexShrink: 0 }} {...(lineIndex === 0 ? dotHover : {})}>
|
|
330
|
+
<text selectable={false} fg={glyphColor}>
|
|
331
|
+
{lineIndex === 0 ? `${glyph} ` : " "}
|
|
332
|
+
</text>
|
|
333
|
+
</box>
|
|
300
334
|
<text fg={tokens.text} style={{ wrapMode: "word", flexGrow: 1, flexShrink: 1 }}>
|
|
301
335
|
{line}
|
|
302
336
|
</text>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Story, StoryMeta } from "./story";
|
|
3
|
+
import { AnsiScreen } from "./AnsiScreen";
|
|
4
|
+
|
|
5
|
+
export const meta: StoryMeta = { title: "Primitives/AnsiScreen" };
|
|
6
|
+
|
|
7
|
+
const POSITIONED = "\x1b[2;4HHello from\x1b[4;6Hraw, positioned ANSI";
|
|
8
|
+
|
|
9
|
+
export const Positioned: Story = {
|
|
10
|
+
render: () => <AnsiScreen ansi={POSITIONED} cols={40} rows={6} />,
|
|
11
|
+
size: { width: 44, height: 8 },
|
|
12
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Paints a raw ANSI screen (cursor-positioned text) as a terminal would, for previewing SSH surfaces. */
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import type { Theme } from "../theme";
|
|
5
|
+
import { ansiScreenToLines } from "../ansi-screen";
|
|
6
|
+
import { useComponentTheme } from "./theme-context";
|
|
7
|
+
|
|
8
|
+
export interface AnsiScreenProps {
|
|
9
|
+
ansi: string;
|
|
10
|
+
cols: number;
|
|
11
|
+
rows: number;
|
|
12
|
+
theme?: Theme;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AnsiScreen({ ansi, cols, rows, theme }: AnsiScreenProps): React.ReactNode {
|
|
16
|
+
const tokens = useComponentTheme(theme);
|
|
17
|
+
const lines = ansiScreenToLines(ansi, cols, rows);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<box style={{ flexDirection: "column" }}>
|
|
21
|
+
{lines.map((line, index) => (
|
|
22
|
+
<text key={index} fg={tokens.text} selectable={false} style={{ wrapMode: "none" }}>
|
|
23
|
+
{line === "" ? " " : line}
|
|
24
|
+
</text>
|
|
25
|
+
))}
|
|
26
|
+
</box>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -5,8 +5,19 @@ import { CompletionOverlay } from "./CompletionOverlay";
|
|
|
5
5
|
|
|
6
6
|
export const meta: StoryMeta = { title: "Overlays/CompletionOverlay" };
|
|
7
7
|
|
|
8
|
+
const noop = () => {};
|
|
9
|
+
|
|
8
10
|
export const ApprovedPrompt: Story = {
|
|
9
|
-
render: () =>
|
|
11
|
+
render: () => (
|
|
12
|
+
<CompletionOverlay
|
|
13
|
+
verdict="approve"
|
|
14
|
+
completion={{ phase: "prompt" }}
|
|
15
|
+
status=""
|
|
16
|
+
onClose={noop}
|
|
17
|
+
onBackToPlan={noop}
|
|
18
|
+
onAlways={noop}
|
|
19
|
+
/>
|
|
20
|
+
),
|
|
10
21
|
expectedColors: [DARK.green],
|
|
11
22
|
};
|
|
12
23
|
|
|
@@ -17,6 +28,9 @@ export const FeedbackCounting: Story = {
|
|
|
17
28
|
completion={{ phase: "counting", remaining: 3 }}
|
|
18
29
|
status="exported to vault/cueloop/plan.md"
|
|
19
30
|
returnsTo="agent/worker-3"
|
|
31
|
+
onClose={noop}
|
|
32
|
+
onBackToPlan={noop}
|
|
33
|
+
onAlways={noop}
|
|
20
34
|
/>
|
|
21
35
|
),
|
|
22
36
|
expectedColors: [DARK.accent],
|
|
@@ -8,6 +8,8 @@ import React from "react";
|
|
|
8
8
|
import type { VerdictKind } from "@cueloop/schema";
|
|
9
9
|
import type { Theme } from "../theme";
|
|
10
10
|
import { useComponentTheme } from "./theme-context";
|
|
11
|
+
import { Toolbar } from "./primitives/Toolbar";
|
|
12
|
+
import { Button } from "./primitives/Button";
|
|
11
13
|
|
|
12
14
|
export interface CompletionOverlayProps {
|
|
13
15
|
verdict: VerdictKind;
|
|
@@ -16,6 +18,9 @@ export interface CompletionOverlayProps {
|
|
|
16
18
|
status: string;
|
|
17
19
|
/** Where focus goes on close (the agent's pane), when known. */
|
|
18
20
|
returnsTo?: string;
|
|
21
|
+
onClose: () => void;
|
|
22
|
+
onBackToPlan: () => void;
|
|
23
|
+
onAlways: () => void;
|
|
19
24
|
theme?: Theme;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -24,6 +29,9 @@ export function CompletionOverlay({
|
|
|
24
29
|
completion,
|
|
25
30
|
status,
|
|
26
31
|
returnsTo,
|
|
32
|
+
onClose,
|
|
33
|
+
onBackToPlan,
|
|
34
|
+
onAlways,
|
|
27
35
|
theme,
|
|
28
36
|
}: CompletionOverlayProps): React.ReactNode {
|
|
29
37
|
const tokens = useComponentTheme(theme);
|
|
@@ -49,12 +57,21 @@ export function CompletionOverlay({
|
|
|
49
57
|
</text>
|
|
50
58
|
{returnsTo ? <text fg={tokens.textDim}>returning to {returnsTo} on close</text> : null}
|
|
51
59
|
{status ? <text fg={tokens.textDim}>{status}</text> : null}
|
|
60
|
+
{completion.phase === "counting" ? (
|
|
61
|
+
<text fg={tokens.textDim}>closing in {completion.remaining}s</text>
|
|
62
|
+
) : null}
|
|
52
63
|
<text> </text>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
<Toolbar>
|
|
65
|
+
<Button variant="solid" marginRight={2} onPress={onClose} theme={theme}>
|
|
66
|
+
{" close "}
|
|
67
|
+
</Button>
|
|
68
|
+
<Button marginRight={2} onPress={onBackToPlan} theme={theme}>
|
|
69
|
+
{" back to plan "}
|
|
70
|
+
</Button>
|
|
71
|
+
<Button onPress={onAlways} theme={theme}>
|
|
72
|
+
{" always "}
|
|
73
|
+
</Button>
|
|
74
|
+
</Toolbar>
|
|
58
75
|
</box>
|
|
59
76
|
);
|
|
60
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { DARK } from "../theme";
|
|
3
|
+
import { DEFAULT_QUICK_ACTIONS } from "../config";
|
|
3
4
|
import type { Story, StoryMeta } from "./story";
|
|
4
5
|
import { ConfirmCard } from "./ConfirmCard";
|
|
5
6
|
|
|
@@ -10,6 +11,7 @@ const callbacks = {
|
|
|
10
11
|
onSelectVerdict: () => {},
|
|
11
12
|
onSubmit: () => {},
|
|
12
13
|
onCancel: () => {},
|
|
14
|
+
quickActions: DEFAULT_QUICK_ACTIONS,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const ApproveDefault: Story = {
|
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
* derives from the content rows through Card, so layout and render never drift.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React from "react";
|
|
8
|
+
import React, { useContext } from "react";
|
|
9
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
9
10
|
import type { VerdictKind } from "@cueloop/schema";
|
|
10
11
|
import type { Theme } from "../theme";
|
|
12
|
+
import type { QuickAction } from "../config";
|
|
13
|
+
import { SlashSkillsContext } from "../skills";
|
|
14
|
+
import { activeSlashToken, mergeSlashItems, slashFilter, slashItemsFrom } from "../slash-palette";
|
|
11
15
|
import { useComponentTheme } from "./theme-context";
|
|
12
16
|
import { Card } from "./primitives/Card";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
17
|
+
import { composeRowCount } from "./AnnotationCards";
|
|
18
|
+
import { SlashComposer } from "./SlashComposer";
|
|
19
|
+
import { DialogActions } from "./primitives/DialogActions";
|
|
20
|
+
|
|
21
|
+
const SUBMIT_CARD_MAX_WIDTH = 48;
|
|
22
|
+
const PALETTE_WINDOW = 5;
|
|
15
23
|
|
|
16
24
|
export const VERDICTS: VerdictKind[] = ["comment", "approve", "request_changes"];
|
|
17
25
|
|
|
@@ -34,6 +42,7 @@ export interface ConfirmCardProps {
|
|
|
34
42
|
onSelectVerdict: (verdict: VerdictKind) => void;
|
|
35
43
|
onSubmit: () => void;
|
|
36
44
|
onCancel: () => void;
|
|
45
|
+
quickActions: QuickAction[];
|
|
37
46
|
theme?: Theme;
|
|
38
47
|
}
|
|
39
48
|
|
|
@@ -66,7 +75,7 @@ function VerdictSelector({
|
|
|
66
75
|
const tokens = useComponentTheme(theme);
|
|
67
76
|
|
|
68
77
|
return (
|
|
69
|
-
<box style={{ flexDirection: "row", height: 1 }}>
|
|
78
|
+
<box style={{ flexDirection: "row", height: 1, width: "100%", justifyContent: "center" }}>
|
|
70
79
|
{VERDICTS.map((candidate) => (
|
|
71
80
|
<box
|
|
72
81
|
key={candidate}
|
|
@@ -92,14 +101,33 @@ export function ConfirmCard({
|
|
|
92
101
|
onSelectVerdict,
|
|
93
102
|
onSubmit,
|
|
94
103
|
onCancel,
|
|
104
|
+
quickActions,
|
|
95
105
|
theme,
|
|
96
106
|
}: ConfirmCardProps): React.ReactNode {
|
|
97
107
|
const tokens = useComponentTheme(theme);
|
|
108
|
+
const skills = useContext(SlashSkillsContext);
|
|
109
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
110
|
+
const cardWidth = Math.max(24, Math.min(terminalWidth - 6, SUBMIT_CARD_MAX_WIDTH));
|
|
111
|
+
|
|
112
|
+
const composerRows = composeRowCount(summary, cardWidth - 4);
|
|
113
|
+
const token = activeSlashToken(summary, summary.length);
|
|
114
|
+
const paletteItems =
|
|
115
|
+
token !== null
|
|
116
|
+
? slashFilter(mergeSlashItems(slashItemsFrom(quickActions), skills), token.slice(1))
|
|
117
|
+
: [];
|
|
118
|
+
const paletteRows =
|
|
119
|
+
paletteItems.length > 0
|
|
120
|
+
? Math.min(PALETTE_WINDOW, paletteItems.length) +
|
|
121
|
+
(paletteItems.length > PALETTE_WINDOW ? 1 : 0)
|
|
122
|
+
: 0;
|
|
123
|
+
const contentRows =
|
|
124
|
+
CONFIRM_CONTENT_ROWS - 1 + composerRows + paletteRows + (viewedSummary !== undefined ? 2 : 0);
|
|
98
125
|
|
|
99
126
|
return (
|
|
100
127
|
<Card
|
|
101
|
-
contentRows={
|
|
102
|
-
|
|
128
|
+
contentRows={contentRows}
|
|
129
|
+
width={cardWidth}
|
|
130
|
+
borderColor={tokens.accent}
|
|
103
131
|
backgroundColor={tokens.elevated}
|
|
104
132
|
theme={theme}
|
|
105
133
|
>
|
|
@@ -107,21 +135,20 @@ export function ConfirmCard({
|
|
|
107
135
|
{viewedSummary !== undefined ? <box style={{ height: 1 }} /> : null}
|
|
108
136
|
<VerdictSelector verdict={verdict} onSelectVerdict={onSelectVerdict} theme={theme} />
|
|
109
137
|
<box style={{ height: 1 }} />
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
<SlashComposer
|
|
139
|
+
seed={summary}
|
|
140
|
+
glyph=""
|
|
141
|
+
quickActions={quickActions}
|
|
142
|
+
tokens={tokens}
|
|
114
143
|
placeholder="summary for the agent (optional)"
|
|
144
|
+
onSubmit={(text) => {
|
|
145
|
+
onInput(text);
|
|
146
|
+
onSubmit();
|
|
147
|
+
}}
|
|
148
|
+
onInput={onInput}
|
|
115
149
|
/>
|
|
116
150
|
<box style={{ height: 1 }} />
|
|
117
|
-
<
|
|
118
|
-
<Button variant="solid" marginRight={2} onPress={onSubmit} theme={theme}>
|
|
119
|
-
{" send message "}
|
|
120
|
-
</Button>
|
|
121
|
-
<Button onPress={onCancel} theme={theme}>
|
|
122
|
-
{" Cancel "}
|
|
123
|
-
</Button>
|
|
124
|
-
</Toolbar>
|
|
151
|
+
<DialogActions confirmLabel="send" onConfirm={onSubmit} onCancel={onCancel} theme={theme} />
|
|
125
152
|
</Card>
|
|
126
153
|
);
|
|
127
154
|
}
|
|
@@ -10,8 +10,7 @@ import { useTerminalDimensions } from "@opentui/react";
|
|
|
10
10
|
import type { Theme } from "../theme";
|
|
11
11
|
import { useComponentTheme } from "./theme-context";
|
|
12
12
|
import { Dialog } from "./primitives/Dialog";
|
|
13
|
-
import {
|
|
14
|
-
import { Toolbar } from "./primitives/Toolbar";
|
|
13
|
+
import { DialogActions } from "./primitives/DialogActions";
|
|
15
14
|
|
|
16
15
|
export interface ConfirmDialogProps {
|
|
17
16
|
isOpen: boolean;
|
|
@@ -28,8 +27,8 @@ export function ConfirmDialog({
|
|
|
28
27
|
isOpen,
|
|
29
28
|
title,
|
|
30
29
|
message,
|
|
31
|
-
confirmLabel = "
|
|
32
|
-
cancelLabel = "
|
|
30
|
+
confirmLabel = "delete",
|
|
31
|
+
cancelLabel = "cancel",
|
|
33
32
|
onConfirm,
|
|
34
33
|
onCancel,
|
|
35
34
|
theme,
|
|
@@ -45,6 +44,7 @@ export function ConfirmDialog({
|
|
|
45
44
|
title={title}
|
|
46
45
|
width={Math.min(54, terminalWidth - 6)}
|
|
47
46
|
height={7}
|
|
47
|
+
background={tokens.elevated}
|
|
48
48
|
onDismiss={onCancel}
|
|
49
49
|
theme={theme}
|
|
50
50
|
>
|
|
@@ -59,14 +59,13 @@ export function ConfirmDialog({
|
|
|
59
59
|
>
|
|
60
60
|
<text fg={tokens.text}>{message}</text>
|
|
61
61
|
<box style={{ flexGrow: 1 }} />
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
</Toolbar>
|
|
62
|
+
<DialogActions
|
|
63
|
+
confirmLabel={confirmLabel}
|
|
64
|
+
cancelLabel={cancelLabel}
|
|
65
|
+
onConfirm={onConfirm}
|
|
66
|
+
onCancel={onCancel}
|
|
67
|
+
theme={theme}
|
|
68
|
+
/>
|
|
70
69
|
</box>
|
|
71
70
|
</Dialog>
|
|
72
71
|
);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { createTextAttributes, type KeyEvent, type ScrollBoxRenderable } from "@opentui/core";
|
|
13
|
-
import type { Thread } from "@cueloop/schema";
|
|
13
|
+
import type { Annotation, 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";
|
|
@@ -18,7 +18,7 @@ import type { Theme } from "../theme";
|
|
|
18
18
|
import { useComponentTheme } from "./theme-context";
|
|
19
19
|
import { IconButton } from "./primitives/IconButton";
|
|
20
20
|
import { NERD } from "./primitives/icons";
|
|
21
|
-
import {
|
|
21
|
+
import { createIntralineResolver, type IntralineRun } from "../diff-intraline";
|
|
22
22
|
import { highlightDiffRows, type SyntaxSpan } from "../diff-syntax";
|
|
23
23
|
import { splitDiffRows, type SplitLine, type SplitRow } from "../split-diff";
|
|
24
24
|
import { UNDERLINE, type AnnotationPalette } from "../annotation-palette";
|
|
@@ -95,6 +95,8 @@ export interface DiffContentViewProps {
|
|
|
95
95
|
onAnnotate: (span: TextSpan, body: string) => void;
|
|
96
96
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
97
97
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
98
|
+
/** The author's display name for a comment's hover tooltip. */
|
|
99
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
98
100
|
leaderCombos?: readonly string[];
|
|
99
101
|
onLeaderCommand?: (key: KeyEvent) => void;
|
|
100
102
|
onExit: () => void;
|
|
@@ -489,6 +491,7 @@ export function DiffContentView({
|
|
|
489
491
|
onAnnotate,
|
|
490
492
|
onReply,
|
|
491
493
|
onUpdateAnnotation,
|
|
494
|
+
resolveAuthorLabel,
|
|
492
495
|
leaderCombos,
|
|
493
496
|
onLeaderCommand,
|
|
494
497
|
onExit,
|
|
@@ -524,6 +527,7 @@ export function DiffContentView({
|
|
|
524
527
|
onAnnotate,
|
|
525
528
|
onReply,
|
|
526
529
|
onUpdateAnnotation,
|
|
530
|
+
resolveAuthorLabel,
|
|
527
531
|
leaderCombos,
|
|
528
532
|
onLeaderCommand,
|
|
529
533
|
onExit,
|
|
@@ -531,7 +535,7 @@ export function DiffContentView({
|
|
|
531
535
|
const { palette } = surface;
|
|
532
536
|
const numberWidth = useMemo(() => lineNumberWidth(rows), [rows]);
|
|
533
537
|
const unifiedGutterColumns = numberWidth * 2 + UNIFIED_GUTTER_CHROME;
|
|
534
|
-
const
|
|
538
|
+
const intralineResolver = useMemo(() => createIntralineResolver(rows), [rows]);
|
|
535
539
|
const syntaxByRow = useSyntaxHighlights(rows);
|
|
536
540
|
const splitRows = useMemo(() => (split ? splitDiffRows(rows) : []), [split, rows]);
|
|
537
541
|
const localStats = useMemo(() => fileChangeCounts(rows), [rows]);
|
|
@@ -591,13 +595,13 @@ export function DiffContentView({
|
|
|
591
595
|
const fgByColumn = foregroundColumns(
|
|
592
596
|
text,
|
|
593
597
|
rowBaseColor(row, tokens),
|
|
594
|
-
|
|
598
|
+
intralineResolver.runsForRow(rowIndex),
|
|
595
599
|
syntaxByRow.get(rowIndex),
|
|
596
600
|
tokens,
|
|
597
601
|
);
|
|
598
602
|
const emphasisBgByColumn = emphasisBackgroundColumns(
|
|
599
603
|
text,
|
|
600
|
-
|
|
604
|
+
intralineResolver.runsForRow(rowIndex),
|
|
601
605
|
rowEmphasisBackground(row, tokens),
|
|
602
606
|
);
|
|
603
607
|
const rowBg = rowBackground(row, tokens);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A Changes/Project file tab in contents mode: a workspace file rendered read-only with line
|
|
2
|
-
// numbers, for surfaces with no
|
|
2
|
+
// numbers, for surfaces with no thread to anchor notes to (the bare-launch welcome shell).
|
|
3
3
|
// Loads on mount and whenever the path changes; a null read renders a centered "File deleted".
|
|
4
4
|
// Syntax highlighting comes from the native code renderable (tree-sitter), the language
|
|
5
5
|
// auto-detected from the path; unknown languages simply render unstyled.
|
|
@@ -34,6 +34,7 @@ export type DiffSurfaceProps = Pick<
|
|
|
34
34
|
| "onAnnotate"
|
|
35
35
|
| "onReply"
|
|
36
36
|
| "onUpdateAnnotation"
|
|
37
|
+
| "resolveAuthorLabel"
|
|
37
38
|
| "leaderCombos"
|
|
38
39
|
| "onLeaderCommand"
|
|
39
40
|
| "onExit"
|
|
@@ -121,6 +122,7 @@ export function GridTabContent(props: {
|
|
|
121
122
|
onAddComment={(anchor, body) => props.onAddFileComment(filePath, anchor, body)}
|
|
122
123
|
onReply={props.surface.onReply}
|
|
123
124
|
onUpdateAnnotation={props.surface.onUpdateAnnotation}
|
|
125
|
+
resolveAuthorLabel={props.surface.resolveAuthorLabel}
|
|
124
126
|
onExit={props.surface.onExit}
|
|
125
127
|
theme={props.theme}
|
|
126
128
|
/>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { ManageAccessDialog } from "./ManageAccessDialog";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Dialogs/ManageAccess" };
|
|
7
|
+
|
|
8
|
+
export const WithHandles: Story = {
|
|
9
|
+
render: () => (
|
|
10
|
+
<ManageAccessDialog
|
|
11
|
+
isOpen
|
|
12
|
+
logins={["octocat", "hubot"]}
|
|
13
|
+
onAdd={() => {}}
|
|
14
|
+
onRemove={() => {}}
|
|
15
|
+
onCreateLink={() => {}}
|
|
16
|
+
onClose={() => {}}
|
|
17
|
+
theme={DARK}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
size: { width: 60, height: 18 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Empty: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<ManageAccessDialog
|
|
26
|
+
isOpen
|
|
27
|
+
logins={[]}
|
|
28
|
+
onAdd={() => {}}
|
|
29
|
+
onRemove={() => {}}
|
|
30
|
+
onCreateLink={() => {}}
|
|
31
|
+
onClose={() => {}}
|
|
32
|
+
theme={DARK}
|
|
33
|
+
/>
|
|
34
|
+
),
|
|
35
|
+
size: { width: 60, height: 18 },
|
|
36
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DARK } from "../theme";
|
|
5
|
+
import { clickText, locateTextInFrame, settle, typeText } from "../test-support";
|
|
6
|
+
import { ManageAccessDialog } from "./ManageAccessDialog";
|
|
7
|
+
|
|
8
|
+
function AccessHarness({ initial }: { initial: string[] }): React.ReactNode {
|
|
9
|
+
const [logins, setLogins] = useState<string[]>(initial);
|
|
10
|
+
const [open, setOpen] = useState(true);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ width: 60, height: 18 }}>
|
|
14
|
+
<ManageAccessDialog
|
|
15
|
+
isOpen={open}
|
|
16
|
+
logins={logins}
|
|
17
|
+
onAdd={(login) => setLogins((prev) => [...prev, login])}
|
|
18
|
+
onRemove={(login) => setLogins((prev) => prev.filter((entry) => entry !== login))}
|
|
19
|
+
onCreateLink={() => {}}
|
|
20
|
+
onClose={() => setOpen(false)}
|
|
21
|
+
theme={DARK}
|
|
22
|
+
/>
|
|
23
|
+
</box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function renderAccess(initial: string[]): Promise<Awaited<ReturnType<typeof testRender>>> {
|
|
28
|
+
const setup = await testRender(<AccessHarness initial={initial} />, { width: 60, height: 18 });
|
|
29
|
+
|
|
30
|
+
await settle(setup);
|
|
31
|
+
|
|
32
|
+
return setup;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("ManageAccessDialog", () => {
|
|
36
|
+
test("renders the allowlist of GitHub handles", async () => {
|
|
37
|
+
const setup = await renderAccess(["octocat", "hubot"]);
|
|
38
|
+
const frame = setup.captureCharFrame();
|
|
39
|
+
|
|
40
|
+
expect(locateTextInFrame(frame, "@octocat")).not.toBeNull();
|
|
41
|
+
expect(locateTextInFrame(frame, "@hubot")).not.toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("clicking a row removes that handle from the allowlist", async () => {
|
|
45
|
+
const setup = await renderAccess(["octocat", "hubot"]);
|
|
46
|
+
|
|
47
|
+
await clickText(setup, "@octocat");
|
|
48
|
+
|
|
49
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@octocat")).toBeNull();
|
|
50
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@hubot")).not.toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("typing a handle and pressing add appends it to the allowlist", async () => {
|
|
54
|
+
const setup = await renderAccess([]);
|
|
55
|
+
|
|
56
|
+
await typeText(setup, "octocat");
|
|
57
|
+
await clickText(setup, "add");
|
|
58
|
+
|
|
59
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "@octocat")).not.toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("clicking create link publishes the private link", async () => {
|
|
63
|
+
let created = false;
|
|
64
|
+
const setup = await testRender(
|
|
65
|
+
<box style={{ width: 60, height: 18 }}>
|
|
66
|
+
<ManageAccessDialog
|
|
67
|
+
isOpen
|
|
68
|
+
logins={["octocat"]}
|
|
69
|
+
onAdd={() => {}}
|
|
70
|
+
onRemove={() => {}}
|
|
71
|
+
onCreateLink={() => {
|
|
72
|
+
created = true;
|
|
73
|
+
}}
|
|
74
|
+
onClose={() => {}}
|
|
75
|
+
theme={DARK}
|
|
76
|
+
/>
|
|
77
|
+
</box>,
|
|
78
|
+
{ width: 60, height: 18 },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await settle(setup);
|
|
82
|
+
await clickText(setup, "create link");
|
|
83
|
+
|
|
84
|
+
expect(created).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("clicking close dismisses the dialog", async () => {
|
|
88
|
+
const setup = await renderAccess([]);
|
|
89
|
+
|
|
90
|
+
await clickText(setup, "close");
|
|
91
|
+
|
|
92
|
+
expect(locateTextInFrame(setup.captureCharFrame(), "Manage access")).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
});
|