@cueloop/client 0.1.0-alpha.6 → 0.1.0-alpha.61
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +37 -33
- package/src/App.appearance.test.tsx +51 -0
- package/src/App.autoclose.test.tsx +73 -47
- package/src/App.diff.test.tsx +169 -22
- package/src/App.export.test.tsx +49 -37
- package/src/App.plan-review.test.tsx +714 -0
- package/src/App.prototype.test.tsx +194 -0
- package/src/App.readonly.test.tsx +69 -35
- package/src/App.rendering.test.tsx +59 -31
- package/src/App.review-panel.test.tsx +143 -0
- package/src/App.submit-confirm.test.tsx +229 -0
- package/src/App.test.tsx +139 -68
- package/src/App.toast.test.tsx +77 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +270 -0
- package/src/app-view-model.ts +307 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +250 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +129 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffSheet.scroll.test.tsx +135 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +65 -0
- package/src/components/DiffSheet.tsx +374 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +64 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +59 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +109 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +75 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +422 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +80 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +450 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +103 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +318 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +147 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-sheet-layout.test.ts +128 -0
- package/src/components/diff-sheet-layout.ts +184 -0
- package/src/components/plan-sheet-run-style.ts +90 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +138 -0
- package/src/components/stories-app.tsx +124 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +75 -0
- package/src/components/syntax-highlight.ts +102 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +193 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/config.test.ts +448 -31
- package/src/config.ts +339 -57
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +253 -0
- package/src/diff-intraline.test.ts +165 -0
- package/src/diff-intraline.ts +146 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +176 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +135 -9
- package/src/ghostty-terminal.test.ts +84 -0
- package/src/ghostty-terminal.ts +176 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +17 -21
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +499 -0
- package/src/intent-dispatch.ts +585 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +452 -0
- package/src/keymap.test.ts +635 -0
- package/src/keymap.ts +404 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +289 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +237 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +16 -5
- package/src/serve.test.ts +25 -9
- package/src/serve.ts +19 -8
- package/src/session-controller.curate.test.ts +260 -0
- package/src/session-controller.share.test.ts +320 -0
- package/src/session-controller.ts +993 -0
- package/src/share.ts +120 -0
- package/src/test-support.test.ts +60 -0
- package/src/test-support.ts +197 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +187 -0
- package/src/theme.ts +64 -17
- package/src/use-settings-dialog.tsx +275 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +22 -8
- package/src/view-diff.ts +39 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +554 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/src/herdr.ts
CHANGED
|
@@ -1,33 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* First-class herdr return
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* best-effort: focus failures never block closing the review.
|
|
2
|
+
* First-class herdr return: a review opened beside an agent hands focus back
|
|
3
|
+
* to the agent's pane when it closes, not leaving the reviewer on a dead tab.
|
|
4
|
+
* The env contract and the return-target rule live in @cueloop/schema; this
|
|
5
|
+
* module owns the one reviewer-side action that spawns the herdr CLI. Focus
|
|
6
|
+
* is best-effort - a failure never blocks closing the review.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
export function insideHerdr(): boolean {
|
|
11
|
-
return process.env.HERDR_ENV === "1";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function returnPaneFor(sessionHerdrPane?: string): string | undefined {
|
|
15
|
-
if (!insideHerdr()) return undefined;
|
|
16
|
-
const pane = process.env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
|
|
17
|
-
// returning to our own pane would be a no-op at best
|
|
18
|
-
return pane && pane !== process.env.HERDR_PANE_ID ? pane : undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
9
|
/** Focus the tab holding the target pane. Synchronous: runs just before exit. */
|
|
22
|
-
export function focusHerdrPane(paneId: string): boolean {
|
|
23
|
-
const bin = process.env.HERDR_BIN_PATH ?? "herdr";
|
|
10
|
+
export function focusHerdrPane(binPath: string, paneId: string): boolean {
|
|
24
11
|
try {
|
|
25
|
-
const got = Bun.spawnSync([
|
|
12
|
+
const got = Bun.spawnSync([binPath, "pane", "get", paneId], {
|
|
13
|
+
stdout: "pipe",
|
|
14
|
+
stderr: "ignore",
|
|
15
|
+
});
|
|
16
|
+
|
|
26
17
|
if (got.exitCode !== 0) return false;
|
|
27
18
|
const parsed = JSON.parse(got.stdout.toString()) as { result?: { pane?: { tab_id?: string } } };
|
|
28
19
|
const tab = parsed.result?.pane?.tab_id;
|
|
20
|
+
|
|
29
21
|
if (!tab) return false;
|
|
30
|
-
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
Bun.spawnSync([binPath, "tab", "focus", tab], { stdout: "ignore", stderr: "ignore" })
|
|
25
|
+
.exitCode === 0
|
|
26
|
+
);
|
|
31
27
|
} catch {
|
|
32
28
|
return false;
|
|
33
29
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
export { runClient, type RunClientOptions } from "./run";
|
|
2
2
|
export { serveClient, type ServeHandle, type ServeOptions } from "./serve";
|
|
3
|
+
export { App, type AppProps } from "./App";
|
|
4
|
+
export {
|
|
5
|
+
collaboratorAnnotations,
|
|
6
|
+
publishShare,
|
|
7
|
+
pullShare,
|
|
8
|
+
pushShare,
|
|
9
|
+
shareIdFromLine,
|
|
10
|
+
type ShareResult,
|
|
11
|
+
type ShareTarget,
|
|
12
|
+
} from "./share";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { OBSIDIAN_DEFAULTS } from "@cueloop/integration-obsidian";
|
|
3
|
+
import { loadBundledExporters } from "./integrations";
|
|
4
|
+
import type { IntegrationsConfig } from "./config";
|
|
5
|
+
|
|
6
|
+
function integrations(overrides: Partial<typeof OBSIDIAN_DEFAULTS> = {}): IntegrationsConfig {
|
|
7
|
+
return { obsidian: { ...OBSIDIAN_DEFAULTS, ...overrides } };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("loadBundledExporters", () => {
|
|
11
|
+
test("composes the obsidian integration into one named exporter", async () => {
|
|
12
|
+
// Act
|
|
13
|
+
const exporters = await loadBundledExporters(integrations());
|
|
14
|
+
|
|
15
|
+
// Assert
|
|
16
|
+
expect(exporters.map((exporter) => exporter.name)).toEqual(["obsidian"]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("runsOn mirrors the configured export policy", async () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const onApprove = (await loadBundledExporters(integrations({ exportOn: "approve" })))[0]!;
|
|
22
|
+
|
|
23
|
+
// Assert
|
|
24
|
+
expect(onApprove.runsOn("approve")).toBeTrue();
|
|
25
|
+
expect(onApprove.runsOn("request_changes")).toBeFalse();
|
|
26
|
+
|
|
27
|
+
// Arrange
|
|
28
|
+
const onResolve = (await loadBundledExporters(integrations({ exportOn: "resolve" })))[0]!;
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
expect(onResolve.runsOn("approve")).toBeTrue();
|
|
32
|
+
expect(onResolve.runsOn("request_changes")).toBeTrue();
|
|
33
|
+
|
|
34
|
+
// Arrange
|
|
35
|
+
const manual = (await loadBundledExporters(integrations({ exportOn: "manual" })))[0]!;
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(manual.runsOn("approve")).toBeFalse();
|
|
39
|
+
expect(manual.runsOn("request_changes")).toBeFalse();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled integration wiring: composes the configured integrations into
|
|
3
|
+
* generic exporters plus their run policy, so the session controller depends
|
|
4
|
+
* only on the extension seam, never on an integration's own config type. This
|
|
5
|
+
* and config.ts are the only client modules that name a concrete integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Registry, type Exporter } from "@cueloop/extension-api";
|
|
9
|
+
import { createObsidianExtension, shouldExport } from "@cueloop/integration-obsidian";
|
|
10
|
+
import type { VerdictKind } from "@cueloop/schema";
|
|
11
|
+
import type { IntegrationsConfig } from "./config";
|
|
12
|
+
|
|
13
|
+
/** An exporter plus the policy deciding which verdicts trigger it. */
|
|
14
|
+
export interface BundledExporter {
|
|
15
|
+
name: string;
|
|
16
|
+
run: Exporter;
|
|
17
|
+
runsOn(verdict: VerdictKind): boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Load the bundled integrations from config as generic exporters. */
|
|
21
|
+
export async function loadBundledExporters(
|
|
22
|
+
integrations: IntegrationsConfig,
|
|
23
|
+
): Promise<BundledExporter[]> {
|
|
24
|
+
const registry = new Registry();
|
|
25
|
+
const obsidian = integrations.obsidian;
|
|
26
|
+
const record = await registry.load("obsidian", createObsidianExtension(obsidian));
|
|
27
|
+
const run = record.exporters.get("obsidian");
|
|
28
|
+
|
|
29
|
+
if (!run) return [];
|
|
30
|
+
|
|
31
|
+
return [{ name: "obsidian", run, runsOn: (verdict) => shouldExport(obsidian.exportOn, verdict) }];
|
|
32
|
+
}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The intent reducer in isolation: with the App component's dependencies
|
|
3
|
+
* mocked, each Intent maps to exactly the controller call or state update it
|
|
4
|
+
* should - the navigation clamps, the annotation wrap-around, the verdict
|
|
5
|
+
* cycle, and the submit-reveal fix, none of which needed a rendered TUI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
9
|
+
import type { ReviewSession } from "@cueloop/schema";
|
|
10
|
+
import { createIntentDispatch, type IntentDispatchDeps } from "./intent-dispatch";
|
|
11
|
+
import type { DisplayBlock } from "./view-plan";
|
|
12
|
+
|
|
13
|
+
function block(text: string): DisplayBlock {
|
|
14
|
+
return { text, kind: "text", work: true } as unknown as DisplayBlock;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sessionWith(annotationIds: string[]): ReviewSession {
|
|
18
|
+
return {
|
|
19
|
+
annotations: annotationIds.map((id) => ({ id })),
|
|
20
|
+
workingCopy: undefined,
|
|
21
|
+
} as unknown as ReviewSession;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A controller where every verb is a mock; annotate returns undefined by default. */
|
|
25
|
+
function baseController() {
|
|
26
|
+
return {
|
|
27
|
+
setStatus: mock(),
|
|
28
|
+
open: mock(),
|
|
29
|
+
deleteSession: mock(),
|
|
30
|
+
setSelfName: mock(),
|
|
31
|
+
cut: mock(),
|
|
32
|
+
curationItems: mock(() => []),
|
|
33
|
+
restoreCuration: mock(),
|
|
34
|
+
annotate: mock(),
|
|
35
|
+
annotatePrototype: mock(),
|
|
36
|
+
updateAnnotation: mock(),
|
|
37
|
+
removeAnnotation: mock(),
|
|
38
|
+
walkStart: mock(),
|
|
39
|
+
walkForward: mock(),
|
|
40
|
+
walkBack: mock(),
|
|
41
|
+
walkLeave: mock(),
|
|
42
|
+
submit: mock(),
|
|
43
|
+
share: mock(),
|
|
44
|
+
finishReview: mock(),
|
|
45
|
+
optInAutoClose: mock(),
|
|
46
|
+
dismissCompletion: mock(),
|
|
47
|
+
saveReviewPanel: mock(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A deps bag where every effect is a mock and every read has a plain default. */
|
|
52
|
+
function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDeps {
|
|
53
|
+
const controller = baseController();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
controller: controller as unknown as IntentDispatchDeps["controller"],
|
|
57
|
+
onExit: mock(),
|
|
58
|
+
isDiff: false,
|
|
59
|
+
display: [],
|
|
60
|
+
rows: [],
|
|
61
|
+
cursor: 0,
|
|
62
|
+
inbox: null,
|
|
63
|
+
inboxCursor: 0,
|
|
64
|
+
mode: { type: "normal" },
|
|
65
|
+
session: null,
|
|
66
|
+
reviewMode: "expanded",
|
|
67
|
+
reviewWidth: 34,
|
|
68
|
+
terminalWidth: 120,
|
|
69
|
+
focusedAnnotationId: undefined,
|
|
70
|
+
selectedCurationId: undefined,
|
|
71
|
+
authorNames: {},
|
|
72
|
+
quickActions: [],
|
|
73
|
+
renameAuthor: mock(),
|
|
74
|
+
liveInput: { current: "" },
|
|
75
|
+
reviewWidthRef: { current: 34 },
|
|
76
|
+
planSheetRef: { current: null },
|
|
77
|
+
setCursor: mock(),
|
|
78
|
+
setInboxCursor: mock(),
|
|
79
|
+
setMode: mock(),
|
|
80
|
+
setReviewMode: mock(),
|
|
81
|
+
setReviewWidth: mock(),
|
|
82
|
+
setRailTab: mock(),
|
|
83
|
+
setFocusedAnnotationId: mock(),
|
|
84
|
+
setSelectedCurationId: mock(),
|
|
85
|
+
setPulsedAnnotationId: mock(),
|
|
86
|
+
selectCardFromDocument: mock(),
|
|
87
|
+
runEditorHandOff: mock(),
|
|
88
|
+
openCardEdit: mock(),
|
|
89
|
+
...overrides,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("move", () => {
|
|
94
|
+
test("down clamps at the last row and up clamps at the first", () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
const deps = makeDeps({ display: [block("a"), block("b"), block("c")] });
|
|
97
|
+
const dispatch = createIntentDispatch(deps);
|
|
98
|
+
|
|
99
|
+
// Act
|
|
100
|
+
dispatch({ type: "move", to: "down" });
|
|
101
|
+
|
|
102
|
+
// Assert
|
|
103
|
+
const advance = (deps.setCursor as ReturnType<typeof mock>).mock.calls[0]![0] as (
|
|
104
|
+
current: number,
|
|
105
|
+
) => number;
|
|
106
|
+
|
|
107
|
+
expect(advance(2)).toBe(2); // already at the end, stays
|
|
108
|
+
expect(advance(0)).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("bottom jumps to the last navigable row", () => {
|
|
112
|
+
// Arrange
|
|
113
|
+
const deps = makeDeps({ display: [block("a"), block("b"), block("c")] });
|
|
114
|
+
const dispatch = createIntentDispatch(deps);
|
|
115
|
+
|
|
116
|
+
// Act
|
|
117
|
+
dispatch({ type: "move", to: "bottom" });
|
|
118
|
+
|
|
119
|
+
// Assert
|
|
120
|
+
expect(deps.setCursor).toHaveBeenCalledWith(2);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("annotation navigation", () => {
|
|
125
|
+
test("nextAnnotation wraps from the last annotation back to the first", () => {
|
|
126
|
+
// Arrange
|
|
127
|
+
const deps = makeDeps({ session: sessionWith(["a1", "a2", "a3"]), focusedAnnotationId: "a3" });
|
|
128
|
+
const dispatch = createIntentDispatch(deps);
|
|
129
|
+
|
|
130
|
+
// Act
|
|
131
|
+
dispatch({ type: "nextAnnotation" });
|
|
132
|
+
|
|
133
|
+
// Assert
|
|
134
|
+
expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a1");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("prevAnnotation wraps from the first annotation to the last", () => {
|
|
138
|
+
// Arrange
|
|
139
|
+
const deps = makeDeps({ session: sessionWith(["a1", "a2", "a3"]), focusedAnnotationId: "a1" });
|
|
140
|
+
const dispatch = createIntentDispatch(deps);
|
|
141
|
+
|
|
142
|
+
// Act
|
|
143
|
+
dispatch({ type: "prevAnnotation" });
|
|
144
|
+
|
|
145
|
+
// Assert
|
|
146
|
+
expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("cycling skips annotations a revision already addressed", () => {
|
|
150
|
+
// Arrange: a2 is addressed, so next from a1 lands on a3
|
|
151
|
+
const session = sessionWith(["a1", "a2", "a3"]);
|
|
152
|
+
|
|
153
|
+
(session.annotations[1] as { resolution?: object }).resolution = {
|
|
154
|
+
revision: 2,
|
|
155
|
+
source: "agent",
|
|
156
|
+
};
|
|
157
|
+
const deps = makeDeps({ session, focusedAnnotationId: "a1" });
|
|
158
|
+
const dispatch = createIntentDispatch(deps);
|
|
159
|
+
|
|
160
|
+
// Act
|
|
161
|
+
dispatch({ type: "nextAnnotation" });
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
expect(deps.selectCardFromDocument).toHaveBeenCalledWith("a3");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("cycleVerdict", () => {
|
|
169
|
+
test("advances through the verdict list in the submit overlay", () => {
|
|
170
|
+
// Arrange
|
|
171
|
+
const deps = makeDeps({ mode: { type: "submit", verdict: "approve", summary: "" } });
|
|
172
|
+
const dispatch = createIntentDispatch(deps);
|
|
173
|
+
|
|
174
|
+
// Act
|
|
175
|
+
dispatch({ type: "cycleVerdict", direction: 1 });
|
|
176
|
+
|
|
177
|
+
// Assert
|
|
178
|
+
expect(deps.setMode).toHaveBeenCalledWith({
|
|
179
|
+
type: "submit",
|
|
180
|
+
verdict: "request_changes",
|
|
181
|
+
summary: "",
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("marker-actions popover", () => {
|
|
187
|
+
const span = { displayIndex: 3, wordIndex: 0, wordEnd: 0, start: 2, end: 9 };
|
|
188
|
+
|
|
189
|
+
test("spanCut cuts the span's block and closes the popover", () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const deps = makeDeps({ mode: { type: "span", span } });
|
|
192
|
+
const dispatch = createIntentDispatch(deps);
|
|
193
|
+
|
|
194
|
+
// Act
|
|
195
|
+
dispatch({ type: "spanCut" });
|
|
196
|
+
|
|
197
|
+
// Assert
|
|
198
|
+
expect(deps.controller.cut).toHaveBeenCalledWith(3);
|
|
199
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("openSpanActions enters the list at index 0", () => {
|
|
203
|
+
// Arrange
|
|
204
|
+
const deps = makeDeps({ mode: { type: "span", span } });
|
|
205
|
+
const dispatch = createIntentDispatch(deps);
|
|
206
|
+
|
|
207
|
+
// Act
|
|
208
|
+
dispatch({ type: "openSpanActions" });
|
|
209
|
+
|
|
210
|
+
// Assert
|
|
211
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 0 });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("moveSpanAction clamps within the actions list", () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const deps = makeDeps({
|
|
217
|
+
mode: { type: "spanActions", span, index: 1 },
|
|
218
|
+
quickActions: [{ prompt: "one" }, { prompt: "two" }],
|
|
219
|
+
});
|
|
220
|
+
const dispatch = createIntentDispatch(deps);
|
|
221
|
+
|
|
222
|
+
// Act
|
|
223
|
+
dispatch({ type: "moveSpanAction", direction: 1 });
|
|
224
|
+
|
|
225
|
+
// Assert - already at the last row, so the index holds
|
|
226
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "spanActions", span, index: 1 });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("pickSpanAction inserts the preset comment and returns to normal", () => {
|
|
230
|
+
// Arrange
|
|
231
|
+
const annotate = mock(() => "an_new");
|
|
232
|
+
const deps = makeDeps({
|
|
233
|
+
mode: { type: "spanActions", span, index: 1 },
|
|
234
|
+
session: sessionWith([]),
|
|
235
|
+
quickActions: [{ prompt: "Needs a test" }, { prompt: "YAGNI", metadata: "cut scope" }],
|
|
236
|
+
controller: { ...baseController(), annotate } as unknown as IntentDispatchDeps["controller"],
|
|
237
|
+
});
|
|
238
|
+
const dispatch = createIntentDispatch(deps);
|
|
239
|
+
|
|
240
|
+
// Act
|
|
241
|
+
dispatch({ type: "pickSpanAction" });
|
|
242
|
+
|
|
243
|
+
// Assert - metadata joins the prompt with a blank line
|
|
244
|
+
expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "YAGNI\n\ncut scope");
|
|
245
|
+
expect(deps.setFocusedAnnotationId).toHaveBeenCalledWith("an_new");
|
|
246
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("pickSpanAction honors an explicit index (mouse click)", () => {
|
|
250
|
+
// Arrange
|
|
251
|
+
const annotate = mock(() => "an_new");
|
|
252
|
+
const deps = makeDeps({
|
|
253
|
+
mode: { type: "spanActions", span, index: 0 },
|
|
254
|
+
session: sessionWith([]),
|
|
255
|
+
quickActions: [{ prompt: "Needs a test" }, { prompt: "Extract the duplication" }],
|
|
256
|
+
controller: { ...baseController(), annotate } as unknown as IntentDispatchDeps["controller"],
|
|
257
|
+
});
|
|
258
|
+
const dispatch = createIntentDispatch(deps);
|
|
259
|
+
|
|
260
|
+
// Act
|
|
261
|
+
dispatch({ type: "pickSpanAction", index: 1 });
|
|
262
|
+
|
|
263
|
+
// Assert
|
|
264
|
+
expect(annotate).toHaveBeenCalledWith("comment", 3, 2, 9, "Extract the duplication");
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("closeSpanActions returns to the span toolbar", () => {
|
|
268
|
+
// Arrange
|
|
269
|
+
const deps = makeDeps({ mode: { type: "spanActions", span, index: 2 } });
|
|
270
|
+
const dispatch = createIntentDispatch(deps);
|
|
271
|
+
|
|
272
|
+
// Act
|
|
273
|
+
dispatch({ type: "closeSpanActions" });
|
|
274
|
+
|
|
275
|
+
// Assert
|
|
276
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "span", span });
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("openSubmit", () => {
|
|
281
|
+
test("force-opens the review rail so the confirm card can never be hidden", () => {
|
|
282
|
+
// Arrange
|
|
283
|
+
const deps = makeDeps({ session: sessionWith([]), reviewMode: "hidden" });
|
|
284
|
+
const dispatch = createIntentDispatch(deps);
|
|
285
|
+
|
|
286
|
+
// Act
|
|
287
|
+
dispatch({ type: "openSubmit" });
|
|
288
|
+
|
|
289
|
+
// Assert
|
|
290
|
+
expect(deps.setReviewMode).toHaveBeenCalledWith("expanded");
|
|
291
|
+
expect(deps.setRailTab).toHaveBeenCalledWith("review");
|
|
292
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", verdict: "approve", summary: "" });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("does nothing without a session", () => {
|
|
296
|
+
// Arrange
|
|
297
|
+
const deps = makeDeps({ session: null });
|
|
298
|
+
const dispatch = createIntentDispatch(deps);
|
|
299
|
+
|
|
300
|
+
// Act
|
|
301
|
+
dispatch({ type: "openSubmit" });
|
|
302
|
+
|
|
303
|
+
// Assert
|
|
304
|
+
expect(deps.setMode).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("share", () => {
|
|
309
|
+
test("dispatches to the controller's share", () => {
|
|
310
|
+
// Arrange
|
|
311
|
+
const deps = makeDeps();
|
|
312
|
+
const dispatch = createIntentDispatch(deps);
|
|
313
|
+
|
|
314
|
+
// Act
|
|
315
|
+
dispatch({ type: "share" });
|
|
316
|
+
|
|
317
|
+
// Assert
|
|
318
|
+
expect(deps.controller.share).toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("restoreCuration", () => {
|
|
323
|
+
const items = [
|
|
324
|
+
{
|
|
325
|
+
id: "diff:f#0#hunk",
|
|
326
|
+
source: "diff" as const,
|
|
327
|
+
label: "f:1 - hunk",
|
|
328
|
+
preview: [],
|
|
329
|
+
revealIndex: 1,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "diff:f#1#2",
|
|
333
|
+
source: "diff" as const,
|
|
334
|
+
label: "f:9 - change",
|
|
335
|
+
preview: [],
|
|
336
|
+
revealIndex: 9,
|
|
337
|
+
},
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
test("restores the selected item and clears the selection", () => {
|
|
341
|
+
// Arrange
|
|
342
|
+
const deps = makeDeps({ selectedCurationId: "diff:f#1#2" });
|
|
343
|
+
|
|
344
|
+
(deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue(items);
|
|
345
|
+
const dispatch = createIntentDispatch(deps);
|
|
346
|
+
|
|
347
|
+
// Act
|
|
348
|
+
dispatch({ type: "restoreCuration" });
|
|
349
|
+
|
|
350
|
+
// Assert
|
|
351
|
+
expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
|
|
352
|
+
expect(deps.setSelectedCurationId).toHaveBeenCalledWith(undefined);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("with nothing selected, undoes the last removal", () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const deps = makeDeps({ selectedCurationId: undefined });
|
|
358
|
+
|
|
359
|
+
(deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue(items);
|
|
360
|
+
const dispatch = createIntentDispatch(deps);
|
|
361
|
+
|
|
362
|
+
// Act
|
|
363
|
+
dispatch({ type: "restoreCuration" });
|
|
364
|
+
|
|
365
|
+
// Assert
|
|
366
|
+
expect(deps.controller.restoreCuration).toHaveBeenCalledWith("diff:f#1#2");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("does nothing when there is nothing curated out", () => {
|
|
370
|
+
// Arrange
|
|
371
|
+
const deps = makeDeps();
|
|
372
|
+
|
|
373
|
+
(deps.controller.curationItems as ReturnType<typeof mock>).mockReturnValue([]);
|
|
374
|
+
const dispatch = createIntentDispatch(deps);
|
|
375
|
+
|
|
376
|
+
// Act
|
|
377
|
+
dispatch({ type: "restoreCuration" });
|
|
378
|
+
|
|
379
|
+
// Assert
|
|
380
|
+
expect(deps.controller.restoreCuration).not.toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("exit", () => {
|
|
385
|
+
test("calls the exit hook with 0", () => {
|
|
386
|
+
// Arrange
|
|
387
|
+
const deps = makeDeps();
|
|
388
|
+
const dispatch = createIntentDispatch(deps);
|
|
389
|
+
|
|
390
|
+
// Act
|
|
391
|
+
dispatch({ type: "exit" });
|
|
392
|
+
|
|
393
|
+
// Assert
|
|
394
|
+
expect(deps.onExit).toHaveBeenCalledWith(0);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("inbox delete", () => {
|
|
399
|
+
test("requestDeleteSession opens the confirm on the cursor row", () => {
|
|
400
|
+
// Arrange
|
|
401
|
+
const inbox = [{ id: "ses_1", artifact: { meta: { title: "Plan A" } } }] as never;
|
|
402
|
+
const deps = makeDeps({ inbox, inboxCursor: 0 });
|
|
403
|
+
|
|
404
|
+
// Act
|
|
405
|
+
createIntentDispatch(deps)({ type: "requestDeleteSession" });
|
|
406
|
+
|
|
407
|
+
// Assert
|
|
408
|
+
expect(deps.setMode).toHaveBeenCalledWith({
|
|
409
|
+
type: "confirmDelete",
|
|
410
|
+
sessionId: "ses_1",
|
|
411
|
+
title: "Plan A",
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("confirmDialog deletes the session and closes", () => {
|
|
416
|
+
// Arrange
|
|
417
|
+
const deps = makeDeps({ mode: { type: "confirmDelete", sessionId: "ses_1", title: "Plan A" } });
|
|
418
|
+
|
|
419
|
+
// Act
|
|
420
|
+
createIntentDispatch(deps)({ type: "confirmDialog" });
|
|
421
|
+
|
|
422
|
+
// Assert
|
|
423
|
+
expect(deps.controller.deleteSession).toHaveBeenCalledWith("ses_1");
|
|
424
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("rename author", () => {
|
|
429
|
+
test("openRename seeds the prompt with the author's current local name", () => {
|
|
430
|
+
// Arrange
|
|
431
|
+
const session = { annotations: [{ id: "a1", author: "SHA256:x" }] } as never;
|
|
432
|
+
const deps = makeDeps({
|
|
433
|
+
session,
|
|
434
|
+
focusedAnnotationId: "a1",
|
|
435
|
+
authorNames: { "SHA256:x": "Alex" },
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Act
|
|
439
|
+
createIntentDispatch(deps)({ type: "openRename" });
|
|
440
|
+
|
|
441
|
+
// Assert
|
|
442
|
+
expect(deps.setMode).toHaveBeenCalledWith({
|
|
443
|
+
type: "rename",
|
|
444
|
+
authorId: "SHA256:x",
|
|
445
|
+
text: "Alex",
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("openRename on your own note does nothing but explain", () => {
|
|
450
|
+
// Arrange
|
|
451
|
+
const session = { annotations: [{ id: "a1" }] } as never;
|
|
452
|
+
const deps = makeDeps({ session, focusedAnnotationId: "a1" });
|
|
453
|
+
|
|
454
|
+
// Act
|
|
455
|
+
createIntentDispatch(deps)({ type: "openRename" });
|
|
456
|
+
|
|
457
|
+
// Assert
|
|
458
|
+
expect(deps.controller.setStatus).toHaveBeenCalled();
|
|
459
|
+
expect(deps.setMode).not.toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("confirmDialog in rename mode persists the trimmed name and closes", () => {
|
|
463
|
+
// Arrange
|
|
464
|
+
const deps = makeDeps({ mode: { type: "rename", authorId: "SHA256:x", text: " Alex " } });
|
|
465
|
+
|
|
466
|
+
// Act
|
|
467
|
+
createIntentDispatch(deps)({ type: "confirmDialog" });
|
|
468
|
+
|
|
469
|
+
// Assert
|
|
470
|
+
expect(deps.renameAuthor).toHaveBeenCalledWith("SHA256:x", "Alex");
|
|
471
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("collaborator self-name", () => {
|
|
476
|
+
test("confirmDialog in nameSelf mode records the trimmed name and closes", () => {
|
|
477
|
+
// Arrange
|
|
478
|
+
const deps = makeDeps({ mode: { type: "nameSelf", text: " Robin " } });
|
|
479
|
+
|
|
480
|
+
// Act
|
|
481
|
+
createIntentDispatch(deps)({ type: "confirmDialog" });
|
|
482
|
+
|
|
483
|
+
// Assert
|
|
484
|
+
expect(deps.controller.setSelfName).toHaveBeenCalledWith("Robin");
|
|
485
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("an empty name still closes - the collaborator stays anonymous", () => {
|
|
489
|
+
// Arrange
|
|
490
|
+
const deps = makeDeps({ mode: { type: "nameSelf", text: " " } });
|
|
491
|
+
|
|
492
|
+
// Act
|
|
493
|
+
createIntentDispatch(deps)({ type: "confirmDialog" });
|
|
494
|
+
|
|
495
|
+
// Assert
|
|
496
|
+
expect(deps.controller.setSelfName).toHaveBeenCalledWith("");
|
|
497
|
+
expect(deps.setMode).toHaveBeenCalledWith({ type: "normal" });
|
|
498
|
+
});
|
|
499
|
+
});
|