@cueloop/client 0.1.0-alpha.84 → 0.1.0-alpha.86
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.diff.test.tsx +63 -0
- package/src/App.tsx +68 -4
- package/src/app-screens.tsx +0 -1
- package/src/app-view-model.ts +1 -0
- package/src/components/AnnotationCards.tsx +1 -1
- package/src/components/DiffContentView.scroll.test.tsx +29 -1
- package/src/components/DiffContentView.tsx +33 -4
- package/src/components/ThreadView.selection.test.tsx +23 -0
- package/src/components/ThreadView.tsx +2 -2
- package/src/components/WelcomePlayground.stories.tsx +1 -1
- package/src/components/WelcomePlayground.test.tsx +10 -8
- package/src/components/WelcomePlayground.tsx +2 -5
- package/src/components/primitives/Button.tsx +5 -2
- package/src/config.test.ts +62 -1
- package/src/config.ts +66 -0
- package/src/index.ts +16 -1
- package/src/intent-dispatch.test.ts +1 -0
- package/src/launch-layout.test.ts +16 -1
- package/src/launch-layout.ts +6 -1
- package/src/prototype-browser.test.ts +19 -2
- package/src/prototype-browser.ts +7 -7
- package/src/theme-presets.ts +5 -0
- package/src/theme.ts +4 -0
- package/src/thread-controller.ts +13 -0
- package/src/use-settings-dialog.tsx +44 -0
- package/src/view-diff.test.ts +145 -3
- package/src/view-diff.ts +201 -11
- package/src/view-plan.ts +2 -0
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.86",
|
|
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.86",
|
|
31
|
+
"@cueloop/extension-api": "0.1.0-alpha.86",
|
|
32
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.86",
|
|
33
|
+
"@cueloop/schema": "0.1.0-alpha.86",
|
|
34
34
|
"@opentui/core": "^0.5.11",
|
|
35
35
|
"@opentui/keymap": "0.5.11",
|
|
36
36
|
"@opentui/react": "^0.5.11",
|
package/src/App.diff.test.tsx
CHANGED
|
@@ -8,6 +8,8 @@ import React from "react";
|
|
|
8
8
|
import { DaemonServer } from "@cueloop/daemon";
|
|
9
9
|
import type { Thread } from "@cueloop/schema";
|
|
10
10
|
import { App } from "./App";
|
|
11
|
+
import { pullRequestReviewLayout } from "./launch-layout";
|
|
12
|
+
import { DARK } from "./theme";
|
|
11
13
|
import { NERD } from "./components/primitives/icons";
|
|
12
14
|
import {
|
|
13
15
|
clickText,
|
|
@@ -66,6 +68,67 @@ async function renderApp(sessionId = session.id) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
describe("diff review", () => {
|
|
71
|
+
test("pull request review shows its brief beside Changes and signals a moved head", async () => {
|
|
72
|
+
const pullRequest = server.core.sessionCreate({
|
|
73
|
+
workspace: { repoRoot: "/repo", branch: "detached" },
|
|
74
|
+
artifact: {
|
|
75
|
+
type: "diff",
|
|
76
|
+
content: PATCH,
|
|
77
|
+
meta: {
|
|
78
|
+
title: "Fix store",
|
|
79
|
+
pr: "org/repo#42",
|
|
80
|
+
prBrief: "# Fix store\n\n## PR description\n\nUse a map.",
|
|
81
|
+
prHeadSha: "head-1",
|
|
82
|
+
prRefreshHeadSha: "head-2",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const setup = await renderReadyApp(
|
|
87
|
+
<App home={home} sessionId={pullRequest.id} layout={pullRequestReviewLayout()} />,
|
|
88
|
+
{ width: 160, height: 30 },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await waitForText(setup, "Use a map.");
|
|
92
|
+
await waitForText(setup, "new Map()");
|
|
93
|
+
await waitForText(setup, "refresh");
|
|
94
|
+
const refreshSpan = setup
|
|
95
|
+
.captureSpans()
|
|
96
|
+
.lines.flatMap((line) => line.spans)
|
|
97
|
+
.find((span) => span.text.includes("refresh"));
|
|
98
|
+
const refreshColor = refreshSpan?.fg
|
|
99
|
+
.toInts()
|
|
100
|
+
.slice(0, 3)
|
|
101
|
+
.map((part) => part.toString(16).padStart(2, "0"))
|
|
102
|
+
.join("");
|
|
103
|
+
|
|
104
|
+
expect(`#${refreshColor}`).toBe(DARK.warning);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("a resolved pull request review does not offer refresh", async () => {
|
|
108
|
+
const pullRequest = server.core.sessionCreate({
|
|
109
|
+
workspace: { repoRoot: "/repo", branch: "detached" },
|
|
110
|
+
artifact: {
|
|
111
|
+
type: "diff",
|
|
112
|
+
content: PATCH,
|
|
113
|
+
meta: {
|
|
114
|
+
title: "Fix store",
|
|
115
|
+
pr: "org/repo#42",
|
|
116
|
+
prHeadSha: "head-1",
|
|
117
|
+
prRefreshHeadSha: "head-2",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
server.core.sessionSendMessage(pullRequest.id, "approved", "Ready to merge.");
|
|
123
|
+
const setup = await renderReadyApp(
|
|
124
|
+
<App home={home} sessionId={pullRequest.id} layout={pullRequestReviewLayout()} />,
|
|
125
|
+
{ width: 160, height: 30 },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await waitForText(setup, "new Map()");
|
|
129
|
+
expect(setup.captureCharFrame()).not.toContain("refresh");
|
|
130
|
+
});
|
|
131
|
+
|
|
69
132
|
test("renders file header, hunks, and signed lines", async () => {
|
|
70
133
|
// Arrange
|
|
71
134
|
const setup = await renderApp();
|
package/src/App.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import React, {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
10
10
|
import type { Clock, KeyEvent } from "@opentui/core";
|
|
11
|
-
import { marksByDisplay, type Mark } from "./view-plan";
|
|
11
|
+
import { buildDisplay, marksByDisplay, type Mark } from "./view-plan";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_KEYS,
|
|
14
14
|
DEFAULT_QUICK_ACTIONS,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type DiffViewMode,
|
|
22
22
|
type IdentityConfig,
|
|
23
23
|
type QuickAction,
|
|
24
|
+
type ReviewWorkspaceMode,
|
|
24
25
|
} from "./config";
|
|
25
26
|
import { resolveDisplayName } from "./attribution";
|
|
26
27
|
import { resolveGithubIdentity } from "./github-identity";
|
|
@@ -252,6 +253,30 @@ function ownerThreadActions(actions: {
|
|
|
252
253
|
);
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
function refreshPullRequestAction(onRefresh: () => void, theme: Theme): React.ReactNode {
|
|
257
|
+
return (
|
|
258
|
+
<Toolbar>
|
|
259
|
+
<Button onPress={onRefresh} foreground={theme.warning} theme={theme}>
|
|
260
|
+
{" refresh "}
|
|
261
|
+
</Button>
|
|
262
|
+
</Toolbar>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Present pull request context with the ordinary read-only Thread renderer. */
|
|
267
|
+
function pullRequestBriefThread(session: Thread): Thread {
|
|
268
|
+
return {
|
|
269
|
+
...session,
|
|
270
|
+
artifact: {
|
|
271
|
+
...session.artifact,
|
|
272
|
+
type: "reply",
|
|
273
|
+
content: session.artifact.meta.prBrief ?? "",
|
|
274
|
+
},
|
|
275
|
+
workingCopy: undefined,
|
|
276
|
+
annotations: [],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
255
280
|
/** Pick the thread pane's body: the pixel prototype, the diff placeholder, the inline editor, or the read-only view. */
|
|
256
281
|
function chooseThreadBody(choice: {
|
|
257
282
|
isPixelPrototype: boolean;
|
|
@@ -603,6 +628,12 @@ export function App({
|
|
|
603
628
|
const [identity, setIdentity] = useState<IdentityConfig>({ provider: "typed" });
|
|
604
629
|
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
605
630
|
const [skills, setSkills] = useState<SlashItem[]>([]);
|
|
631
|
+
const [reviewSkill, setReviewSkill] = useState("code-review");
|
|
632
|
+
const [reviewWorkspace, setReviewWorkspace] = useState<ReviewWorkspaceMode>("worktree");
|
|
633
|
+
const reviewSkillOptions = useMemo(
|
|
634
|
+
() => [...new Set([reviewSkill, "code-review", ...skills.map((skill) => skill.name)])],
|
|
635
|
+
[reviewSkill, skills],
|
|
636
|
+
);
|
|
606
637
|
const paletteNames = useMemo(
|
|
607
638
|
() => new Set(mergeSlashItems(slashItemsFrom(quickActions), skills).map((item) => item.name)),
|
|
608
639
|
[quickActions, skills],
|
|
@@ -623,6 +654,8 @@ export function App({
|
|
|
623
654
|
setIdentity(config.identity);
|
|
624
655
|
setQuickActions(config.actions);
|
|
625
656
|
setSkills(loadSkills(config.skillsPath));
|
|
657
|
+
setReviewSkill(config.review.skill);
|
|
658
|
+
setReviewWorkspace(config.review.workspace);
|
|
626
659
|
setAutoClose(config.ui.autoClose);
|
|
627
660
|
setDiffView(config.ui.diffView);
|
|
628
661
|
setDefaultMessage(config.ui.defaultMessage);
|
|
@@ -707,6 +740,11 @@ export function App({
|
|
|
707
740
|
setMenuDialog(null);
|
|
708
741
|
setMode({ type: "renameSelf", text: identity.name ?? "" });
|
|
709
742
|
},
|
|
743
|
+
reviewSkill,
|
|
744
|
+
reviewSkillOptions,
|
|
745
|
+
setReviewSkill,
|
|
746
|
+
reviewWorkspace,
|
|
747
|
+
setReviewWorkspace,
|
|
710
748
|
});
|
|
711
749
|
|
|
712
750
|
// ── derived view model ──────────────────────
|
|
@@ -1099,7 +1137,13 @@ export function App({
|
|
|
1099
1137
|
setMode,
|
|
1100
1138
|
dispatch,
|
|
1101
1139
|
});
|
|
1102
|
-
const {
|
|
1140
|
+
const {
|
|
1141
|
+
showOwnerActions,
|
|
1142
|
+
showPullRequestRefresh,
|
|
1143
|
+
prototypeCanComment,
|
|
1144
|
+
chromeHidden,
|
|
1145
|
+
prototypePath,
|
|
1146
|
+
} = buildRenderFlags({
|
|
1103
1147
|
session: activeSession,
|
|
1104
1148
|
isOwner,
|
|
1105
1149
|
isDiff,
|
|
@@ -1181,7 +1225,9 @@ export function App({
|
|
|
1181
1225
|
onShare: () => dispatch({ type: "share" }),
|
|
1182
1226
|
theme,
|
|
1183
1227
|
})
|
|
1184
|
-
:
|
|
1228
|
+
: showPullRequestRefresh
|
|
1229
|
+
? refreshPullRequestAction(() => void controller.refreshPullRequest(), theme)
|
|
1230
|
+
: undefined
|
|
1185
1231
|
}
|
|
1186
1232
|
threadPanel={
|
|
1187
1233
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
@@ -1199,7 +1245,25 @@ export function App({
|
|
|
1199
1245
|
hidden={chromeHidden}
|
|
1200
1246
|
/>
|
|
1201
1247
|
),
|
|
1202
|
-
diffPlaceholder: (
|
|
1248
|
+
diffPlaceholder: activeSession.artifact.meta.prBrief ? (
|
|
1249
|
+
<ThreadView
|
|
1250
|
+
session={pullRequestBriefThread(activeSession)}
|
|
1251
|
+
display={buildDisplay(activeSession.artifact.meta.prBrief)}
|
|
1252
|
+
marks={new Map()}
|
|
1253
|
+
quickActions={quickActions}
|
|
1254
|
+
suspended={threadViewSuspended}
|
|
1255
|
+
resolved
|
|
1256
|
+
observer
|
|
1257
|
+
onComposingChange={() => {}}
|
|
1258
|
+
onObserverBlocked={() => {}}
|
|
1259
|
+
onCursorChange={() => {}}
|
|
1260
|
+
onAnnotate={() => {}}
|
|
1261
|
+
onReply={() => {}}
|
|
1262
|
+
onUpdateAnnotation={() => {}}
|
|
1263
|
+
resolveAuthorLabel={() => undefined}
|
|
1264
|
+
onExit={() => onExit?.(0)}
|
|
1265
|
+
/>
|
|
1266
|
+
) : (
|
|
1203
1267
|
<box
|
|
1204
1268
|
style={{
|
|
1205
1269
|
flexGrow: 1,
|
package/src/app-screens.tsx
CHANGED
|
@@ -319,7 +319,6 @@ export function NoThreadShell(props: {
|
|
|
319
319
|
renderTab={(tab) =>
|
|
320
320
|
tab.kind === "welcome" ? (
|
|
321
321
|
<WelcomePlayground
|
|
322
|
-
version={CLIENT_VERSION}
|
|
323
322
|
quickActions={quickActions}
|
|
324
323
|
onComposingChange={onWelcomeComposingChange}
|
|
325
324
|
suspended={focusedPane !== "changes" || menuControl.openMenuId !== null}
|
package/src/app-view-model.ts
CHANGED
|
@@ -115,6 +115,7 @@ export function buildRenderFlags(params: {
|
|
|
115
115
|
|
|
116
116
|
return {
|
|
117
117
|
showOwnerActions: isOwner && !isDiff && !resolved,
|
|
118
|
+
showPullRequestRefresh: !resolved && session.artifact.meta.prRefreshHeadSha !== undefined,
|
|
118
119
|
prototypeCanComment: isOwner && !resolved,
|
|
119
120
|
chromeHidden: menuDialog !== null,
|
|
120
121
|
prototypePath: session.artifact.meta.prototypePath ?? "",
|
|
@@ -313,7 +313,7 @@ export function CommentRow({
|
|
|
313
313
|
// own comments (no author) wear the filled dot, collaborators the outline
|
|
314
314
|
const own = annotation.author === undefined;
|
|
315
315
|
const glyph = own ? "●" : "○";
|
|
316
|
-
const glyphColor = own ? tokens.text : tokens.textMuted;
|
|
316
|
+
const glyphColor = annotation.orphan ? tokens.warning : own ? tokens.text : tokens.textMuted;
|
|
317
317
|
const { showTooltip, hideTooltip } = useTooltip();
|
|
318
318
|
const dotHover = authorLabel
|
|
319
319
|
? {
|
|
@@ -9,7 +9,7 @@ import { DiffContentView } from "./DiffContentView";
|
|
|
9
9
|
import { diffRows, marksByRows } from "../view-diff";
|
|
10
10
|
import { DARK } from "../theme";
|
|
11
11
|
import { annotationPaletteFor } from "../annotation-palette";
|
|
12
|
-
import { press } from "../test-support";
|
|
12
|
+
import { press, settle, typeText } from "../test-support";
|
|
13
13
|
import { fixtureDiffSession } from "./story-fixtures";
|
|
14
14
|
|
|
15
15
|
const noop = (): void => {};
|
|
@@ -119,3 +119,31 @@ test("walking the caret down past wrapped discussion cards keeps it on screen an
|
|
|
119
119
|
|
|
120
120
|
setup.renderer.destroy();
|
|
121
121
|
}, 25000);
|
|
122
|
+
|
|
123
|
+
test("opening a comment on the diff's last line reveals its composer", async () => {
|
|
124
|
+
const rows = diffRows(tallPatch(24));
|
|
125
|
+
const setup = await testRender(
|
|
126
|
+
<DiffContentView
|
|
127
|
+
rows={rows}
|
|
128
|
+
session={fixtureDiffSession()}
|
|
129
|
+
marks={marksByRows([], rows)}
|
|
130
|
+
quickActions={[]}
|
|
131
|
+
observer={false}
|
|
132
|
+
onAnnotate={noop}
|
|
133
|
+
onReply={noop}
|
|
134
|
+
onUpdateAnnotation={noop}
|
|
135
|
+
onExit={noop}
|
|
136
|
+
/>,
|
|
137
|
+
{ width: 60, height: 10 },
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await settle(setup);
|
|
141
|
+
for (let step = 0; step < rows.length + 2; step++) {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
await press(setup, "down");
|
|
144
|
+
}
|
|
145
|
+
await typeText(setup, "bottom comment");
|
|
146
|
+
|
|
147
|
+
expect(setup.captureCharFrame()).toContain("● bottom comment");
|
|
148
|
+
setup.renderer.destroy();
|
|
149
|
+
}, 25000);
|
|
@@ -509,9 +509,32 @@ export function DiffContentView({
|
|
|
509
509
|
textAt: (rowIndex) => diffRowText(rows[rowIndex]!),
|
|
510
510
|
annotatable: (rowIndex) => isCodeRow(rows[rowIndex]),
|
|
511
511
|
};
|
|
512
|
+
const outdatedIds = useMemo(
|
|
513
|
+
() =>
|
|
514
|
+
new Set(
|
|
515
|
+
[...marks.values()]
|
|
516
|
+
.flat()
|
|
517
|
+
.flatMap((mark) =>
|
|
518
|
+
mark.outdated && mark.annotationId !== undefined ? [mark.annotationId] : [],
|
|
519
|
+
),
|
|
520
|
+
),
|
|
521
|
+
[marks],
|
|
522
|
+
);
|
|
523
|
+
const displaySession = useMemo(
|
|
524
|
+
() =>
|
|
525
|
+
outdatedIds.size === 0
|
|
526
|
+
? session
|
|
527
|
+
: {
|
|
528
|
+
...session,
|
|
529
|
+
annotations: session.annotations.map((annotation) =>
|
|
530
|
+
outdatedIds.has(annotation.id) ? { ...annotation, orphan: true } : annotation,
|
|
531
|
+
),
|
|
532
|
+
},
|
|
533
|
+
[outdatedIds, session],
|
|
534
|
+
);
|
|
512
535
|
const surface = useAnnotationSurface({
|
|
513
536
|
source,
|
|
514
|
-
session,
|
|
537
|
+
session: displaySession,
|
|
515
538
|
marks,
|
|
516
539
|
quickActions,
|
|
517
540
|
tokens,
|
|
@@ -528,7 +551,11 @@ export function DiffContentView({
|
|
|
528
551
|
onReply,
|
|
529
552
|
onUpdateAnnotation,
|
|
530
553
|
dragViewport: () => scrollBoxDragViewport(scrollRef.current),
|
|
531
|
-
resolveAuthorLabel
|
|
554
|
+
resolveAuthorLabel: (annotation) => {
|
|
555
|
+
const label = resolveAuthorLabel?.(annotation);
|
|
556
|
+
|
|
557
|
+
return annotation.orphan ? `outdated${label ? ` - ${label}` : ""}` : label;
|
|
558
|
+
},
|
|
532
559
|
onNavCommand,
|
|
533
560
|
onExit,
|
|
534
561
|
});
|
|
@@ -573,9 +600,11 @@ export function DiffContentView({
|
|
|
573
600
|
const revealItem = layout.itemOfRow[surface.revealBlockIndex];
|
|
574
601
|
|
|
575
602
|
useEffect(() => {
|
|
576
|
-
if (revealItem !== undefined)
|
|
603
|
+
if (revealItem !== undefined) {
|
|
604
|
+
virtual.scrollToIndex(revealItem, surface.compose ? "end" : "auto");
|
|
605
|
+
}
|
|
577
606
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
578
|
-
}, [surface.revealBlockIndex]);
|
|
607
|
+
}, [surface.revealBlockIndex, surface.compose]);
|
|
579
608
|
|
|
580
609
|
/**
|
|
581
610
|
* The visual lines of one code row painted with gutter, colors, and marks; cards collected after.
|
|
@@ -492,6 +492,29 @@ describe("marking across rows", () => {
|
|
|
492
492
|
expect(setup.captureCharFrame()).not.toContain("# Plan");
|
|
493
493
|
expect(highlightedText().length).toBeGreaterThan("daemon".length);
|
|
494
494
|
});
|
|
495
|
+
|
|
496
|
+
test("opening a comment on the document's last line reveals its composer", async () => {
|
|
497
|
+
setup.renderer.destroy();
|
|
498
|
+
await mount([], 10);
|
|
499
|
+
|
|
500
|
+
for (let step = 0; step < 20; step++) {
|
|
501
|
+
setup.mockInput.pressKey("ARROW_DOWN");
|
|
502
|
+
// eslint-disable-next-line no-await-in-loop
|
|
503
|
+
await settle(setup);
|
|
504
|
+
}
|
|
505
|
+
const lastLine = locate(BULLETS[1]!);
|
|
506
|
+
|
|
507
|
+
await setup.mockMouse.drag(
|
|
508
|
+
lastLine.column,
|
|
509
|
+
lastLine.row,
|
|
510
|
+
lastLine.column + "schema.ts".length,
|
|
511
|
+
lastLine.row,
|
|
512
|
+
);
|
|
513
|
+
await settle(setup);
|
|
514
|
+
await typeText(setup, "bottom comment");
|
|
515
|
+
|
|
516
|
+
expect(setup.captureCharFrame()).toContain("● bottom comment");
|
|
517
|
+
});
|
|
495
518
|
});
|
|
496
519
|
|
|
497
520
|
describe("marking across blocks", () => {
|
|
@@ -244,9 +244,9 @@ export function ThreadView({
|
|
|
244
244
|
});
|
|
245
245
|
|
|
246
246
|
useEffect(() => {
|
|
247
|
-
virtual.scrollToIndex(surface.revealBlockIndex);
|
|
247
|
+
virtual.scrollToIndex(surface.revealBlockIndex, surface.compose ? "end" : "auto");
|
|
248
248
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
249
|
-
}, [surface.revealBlockIndex]);
|
|
249
|
+
}, [surface.revealBlockIndex, surface.compose]);
|
|
250
250
|
|
|
251
251
|
interface LineContext {
|
|
252
252
|
blockIndex: number;
|
|
@@ -6,7 +6,7 @@ import { WelcomePlayground } from "./WelcomePlayground";
|
|
|
6
6
|
export const meta: StoryMeta = { title: "Surfaces/WelcomePlayground" };
|
|
7
7
|
|
|
8
8
|
export const Welcome: Story = {
|
|
9
|
-
render: () => <WelcomePlayground
|
|
9
|
+
render: () => <WelcomePlayground quickActions={[]} theme={DARK} />,
|
|
10
10
|
expectedColors: [DARK.textDim],
|
|
11
11
|
size: { width: 72, height: 20 },
|
|
12
12
|
};
|
|
@@ -9,29 +9,31 @@ import { testRender } from "@opentui/react/test-utils";
|
|
|
9
9
|
import React from "react";
|
|
10
10
|
import { WelcomePlayground } from "./WelcomePlayground";
|
|
11
11
|
import { DARK } from "../theme";
|
|
12
|
+
import { CLIENT_VERSION } from "../version";
|
|
12
13
|
import { dragText, typeText, pressKey, waitForText } from "../test-support";
|
|
13
14
|
|
|
14
15
|
describe("WelcomePlayground", () => {
|
|
15
16
|
test("the copy prompts the select-and-type and the slash gestures", async () => {
|
|
16
|
-
const setup = await testRender(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
);
|
|
17
|
+
const setup = await testRender(<WelcomePlayground quickActions={[]} theme={DARK} />, {
|
|
18
|
+
width: 80,
|
|
19
|
+
height: 24,
|
|
20
|
+
});
|
|
20
21
|
|
|
21
22
|
await waitForText(setup, "Getting started");
|
|
22
23
|
const frame = setup.captureCharFrame();
|
|
23
24
|
|
|
24
25
|
expect(frame).toContain("start typing to leave your first comment");
|
|
25
26
|
expect(frame).toContain('type "/"');
|
|
27
|
+
expect(frame).toContain(`cueloop v${CLIENT_VERSION}`);
|
|
26
28
|
|
|
27
29
|
setup.renderer.destroy();
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
test("selecting the copy and typing leaves a comment inline, exactly like a file", async () => {
|
|
31
|
-
const setup = await testRender(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
33
|
+
const setup = await testRender(<WelcomePlayground quickActions={[]} theme={DARK} />, {
|
|
34
|
+
width: 80,
|
|
35
|
+
height: 24,
|
|
36
|
+
});
|
|
35
37
|
|
|
36
38
|
// Act - select the practice line, type a comment, send it
|
|
37
39
|
await waitForText(setup, "quick brown fox");
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
} from "@cueloop/schema";
|
|
15
15
|
import type { Theme } from "../theme";
|
|
16
16
|
import type { QuickAction } from "../config";
|
|
17
|
+
import { CLIENT_VERSION } from "../version";
|
|
17
18
|
import { DiffContentView } from "./DiffContentView";
|
|
18
19
|
import { diffRowBlocks, fileContentsRows, marksByRows } from "../view-diff";
|
|
19
20
|
|
|
20
21
|
export interface WelcomePlaygroundProps {
|
|
21
|
-
version: string;
|
|
22
22
|
quickActions: QuickAction[];
|
|
23
23
|
/** Reports whether the playground composer is open, so the shell suspends its inbox keys while typing. */
|
|
24
24
|
onComposingChange?: (composing: boolean) => void;
|
|
@@ -72,10 +72,7 @@ function playgroundThread(annotations: Annotation[]): Thread {
|
|
|
72
72
|
|
|
73
73
|
export function WelcomePlayground(props: WelcomePlaygroundProps): React.ReactNode {
|
|
74
74
|
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
|
75
|
-
const rows = useMemo(
|
|
76
|
-
() => fileContentsRows("welcome", welcomeCopy(props.version)),
|
|
77
|
-
[props.version],
|
|
78
|
-
);
|
|
75
|
+
const rows = useMemo(() => fileContentsRows("welcome", welcomeCopy(CLIENT_VERSION)), []);
|
|
79
76
|
const session = playgroundThread(annotations);
|
|
80
77
|
|
|
81
78
|
return (
|
|
@@ -15,6 +15,7 @@ export interface ButtonProps {
|
|
|
15
15
|
isDisabled?: boolean;
|
|
16
16
|
/** solid = accent-filled call to action; plain = quiet word-button. */
|
|
17
17
|
variant?: "solid" | "plain" | "accent-text";
|
|
18
|
+
foreground?: string;
|
|
18
19
|
marginRight?: number;
|
|
19
20
|
theme?: Theme;
|
|
20
21
|
children: string;
|
|
@@ -24,6 +25,7 @@ export function Button({
|
|
|
24
25
|
onPress,
|
|
25
26
|
isDisabled = false,
|
|
26
27
|
variant = "plain",
|
|
28
|
+
foreground: foregroundOverride,
|
|
27
29
|
marginRight,
|
|
28
30
|
theme,
|
|
29
31
|
children,
|
|
@@ -33,13 +35,14 @@ export function Button({
|
|
|
33
35
|
const backgroundColor =
|
|
34
36
|
variant === "solid" ? (isDisabled ? tokens.border : tokens.accent) : undefined;
|
|
35
37
|
const foreground =
|
|
36
|
-
|
|
38
|
+
foregroundOverride ??
|
|
39
|
+
(variant === "solid"
|
|
37
40
|
? isDisabled
|
|
38
41
|
? tokens.textMuted
|
|
39
42
|
: tokens.accentInk
|
|
40
43
|
: variant === "accent-text"
|
|
41
44
|
? tokens.accent
|
|
42
|
-
: tokens.textDim;
|
|
45
|
+
: tokens.textDim);
|
|
43
46
|
|
|
44
47
|
return (
|
|
45
48
|
<box
|
package/src/config.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
persistActions,
|
|
12
12
|
persistLayout,
|
|
13
13
|
persistPins,
|
|
14
|
+
persistReviewSkill,
|
|
15
|
+
persistReviewWorkspace,
|
|
14
16
|
persistTheme,
|
|
15
17
|
quickActionBody,
|
|
16
18
|
resolveQuickAction,
|
|
@@ -19,6 +21,65 @@ import { DARK } from "./theme";
|
|
|
19
21
|
import { themeForName } from "./theme-presets";
|
|
20
22
|
|
|
21
23
|
describe("loadConfig", () => {
|
|
24
|
+
test("review defaults use the bundled skill in an isolated worktree", () => {
|
|
25
|
+
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|
|
26
|
+
|
|
27
|
+
expect(config.review).toEqual({ skill: "code-review", workspace: "worktree" });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("review settings load and persist without changing other tables", () => {
|
|
31
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-review-config-"));
|
|
32
|
+
const path = join(dir, "config.toml");
|
|
33
|
+
|
|
34
|
+
writeFileSync(path, '[ui]\ntheme = "cueloop"\n\n[review]\nskill = "my-review"\n');
|
|
35
|
+
try {
|
|
36
|
+
persistReviewSkill("code-review", path);
|
|
37
|
+
persistReviewWorkspace("current", path);
|
|
38
|
+
|
|
39
|
+
expect(loadConfig({ userConfigPath: path }).review).toEqual({
|
|
40
|
+
skill: "code-review",
|
|
41
|
+
workspace: "current",
|
|
42
|
+
});
|
|
43
|
+
expect(readFileSync(path, "utf8")).toContain('[ui]\ntheme = "cueloop"');
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("review settings update a table whose header has a comment", () => {
|
|
50
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-review-commented-config-"));
|
|
51
|
+
const path = join(dir, "config.toml");
|
|
52
|
+
|
|
53
|
+
writeFileSync(path, '[review] # preferred settings\nskill = "my-review"\n');
|
|
54
|
+
try {
|
|
55
|
+
persistReviewSkill("code-review", path);
|
|
56
|
+
|
|
57
|
+
const text = readFileSync(path, "utf8");
|
|
58
|
+
|
|
59
|
+
expect(loadConfig({ userConfigPath: path }).review.skill).toBe("code-review");
|
|
60
|
+
expect(text.match(/^\[review\]/gm)).toHaveLength(1);
|
|
61
|
+
expect(text).toContain("[review] # preferred settings");
|
|
62
|
+
} finally {
|
|
63
|
+
rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("review settings update a CRLF table without duplicating it", () => {
|
|
68
|
+
const dir = mkdtempSync(join(tmpdir(), "cueloop-review-crlf-config-"));
|
|
69
|
+
const path = join(dir, "config.toml");
|
|
70
|
+
|
|
71
|
+
writeFileSync(path, '[review]\r\nskill = "code-review"\r\nworkspace = "worktree"\r\n');
|
|
72
|
+
try {
|
|
73
|
+
persistReviewWorkspace("current", path);
|
|
74
|
+
|
|
75
|
+
const text = readFileSync(path, "utf8");
|
|
76
|
+
|
|
77
|
+
expect(loadConfig({ userConfigPath: path }).review.workspace).toBe("current");
|
|
78
|
+
expect(text.match(/^\[review\]/gm)).toHaveLength(1);
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(dir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
22
83
|
test("defaults when no file exists", () => {
|
|
23
84
|
// Act
|
|
24
85
|
const config = loadConfig({ userConfigPath: "/nonexistent/config.toml" });
|