@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67
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 +6 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +79 -112
- package/src/App.plan-review.test.tsx +23 -205
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +9 -27
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +136 -31
- package/src/App.tsx +491 -322
- package/src/App.walk.test.tsx +15 -16
- package/src/App.workbench.test.tsx +390 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +61 -0
- package/src/app-screens.tsx +197 -77
- package/src/app-view-model.ts +9 -69
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +1 -1
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +239 -0
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +170 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +56 -54
- package/src/components/DiffSheet.syntax.test.tsx +24 -7
- package/src/components/DiffSheet.tsx +607 -276
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +105 -0
- package/src/components/EditorGrid.tsx +338 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +78 -0
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +188 -33
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +82 -0
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PromptDialog.tsx +8 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +114 -67
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SurfaceScrollbar.stories.tsx +34 -0
- package/src/components/SurfaceScrollbar.test.tsx +111 -0
- package/src/components/SurfaceScrollbar.tsx +93 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +62 -1352
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +70 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/diff-sheet-layout.test.ts +125 -55
- package/src/components/diff-sheet-layout.ts +3 -103
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +96 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/config.test.ts +20 -49
- package/src/config.ts +26 -28
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-syntax.test.ts +16 -0
- package/src/diff-syntax.ts +6 -3
- package/src/discussions.ts +79 -0
- package/src/intent-dispatch.test.ts +31 -11
- package/src/intent-dispatch.ts +93 -48
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +3 -25
- package/src/keymap.ts +17 -18
- package/src/mark-runs.ts +120 -0
- package/src/pty.ts +10 -2
- package/src/run.ts +17 -7
- package/src/serve.test.ts +4 -8
- package/src/session-controller.curate.test.ts +75 -0
- package/src/session-controller.share.test.ts +1 -0
- package/src/session-controller.tree.test.ts +26 -0
- package/src/session-controller.ts +160 -31
- package/src/slash-palette.ts +83 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +3 -3
- package/src/test-support.ts +39 -8
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +25 -9
- package/src/thread-chords.ts +33 -8
- package/src/use-annotation-surface.tsx +837 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +23 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
- package/src/components/ReviewPanel.stories.tsx +0 -100
- package/src/components/ReviewPanel.tsx +0 -103
- package/src/components/ReviewRail.stories.tsx +0 -232
- package/src/components/ReviewRail.tsx +0 -327
- package/src/review-panel.test.ts +0 -113
- package/src/review-panel.ts +0 -83
package/src/config.ts
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Layered TOML config: built-in defaults → user config → trusted repo
|
|
3
3
|
* config → env. Sections: [keys] action = "combo" (every action rebindable),
|
|
4
4
|
* [theme] per-token overrides, [ui] auto_close + editor + theme (a named
|
|
5
|
-
* preset) +
|
|
6
|
-
* [integrations.obsidian] notes-vault export.
|
|
5
|
+
* preset) + pins, [integrations.obsidian] notes-vault export.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -15,7 +14,6 @@ import { dirname } from "node:path";
|
|
|
15
14
|
import * as v from "valibot";
|
|
16
15
|
import { DARK, type Theme } from "./theme";
|
|
17
16
|
import { DEFAULT_THEME_NAME, isThemeName, themeForName, type ThemeName } from "./theme-presets";
|
|
18
|
-
import { REVIEW_DEFAULT_WIDTH, clampWidth, type ReviewPanelMode } from "./review-panel";
|
|
19
17
|
|
|
20
18
|
export interface KeymapConfig {
|
|
21
19
|
[action: string]: string | string[];
|
|
@@ -28,6 +26,9 @@ export interface IntegrationsConfig {
|
|
|
28
26
|
/** Post-submit behavior: "off" prompts, 0 closes instantly, N counts down. */
|
|
29
27
|
export type AutoClose = "off" | number;
|
|
30
28
|
|
|
29
|
+
/** How the Changes diff renders: one inline column, or old|new side by side (only when zoomed). */
|
|
30
|
+
export type DiffViewMode = "unified" | "split";
|
|
31
|
+
|
|
31
32
|
/** One marker-popover quick action: a preset comment body, plus optional extra lines. */
|
|
32
33
|
export interface QuickAction {
|
|
33
34
|
prompt: string;
|
|
@@ -93,17 +94,15 @@ export interface CueloopConfig {
|
|
|
93
94
|
theme: Theme;
|
|
94
95
|
/** The `[theme]` per-token overrides alone, so a live theme switch can re-compose them onto a new preset. */
|
|
95
96
|
themeOverrides: Partial<Theme>;
|
|
96
|
-
/**
|
|
97
|
-
* ui.reviewState / ui.reviewWidth are CLIENT VIEW STATE: the review panel's
|
|
98
|
-
* collapse mode and expanded-rail width, persisted so they survive restarts.
|
|
99
|
-
*/
|
|
100
97
|
ui: {
|
|
101
98
|
autoClose: AutoClose;
|
|
102
99
|
editor?: string;
|
|
103
|
-
reviewState: ReviewPanelMode;
|
|
104
|
-
reviewWidth: number;
|
|
105
100
|
/** The selected theme preset name; its tokens are the base for `theme`, before any `[theme]` overrides. */
|
|
106
101
|
theme: ThemeName;
|
|
102
|
+
/** How the Changes diff renders when zoomed: one inline column or old|new side by side. */
|
|
103
|
+
diffView: DiffViewMode;
|
|
104
|
+
/** Session ids the user has pinned to the top of the sidebar; client-local view state. */
|
|
105
|
+
pins: string[];
|
|
107
106
|
};
|
|
108
107
|
/** Planner-local author renames: identity id → display name ([authors] table). */
|
|
109
108
|
authors: Record<string, string>;
|
|
@@ -123,6 +122,9 @@ export const DEFAULT_KEYS: CueloopConfig["keys"] = {
|
|
|
123
122
|
cut: ["x"],
|
|
124
123
|
reject_hunk: ["X"],
|
|
125
124
|
restore_curation: ["u"],
|
|
125
|
+
collapse_file: ["right"],
|
|
126
|
+
expand_file: ["left"],
|
|
127
|
+
split_diff: ["s"],
|
|
126
128
|
edit: ["e"],
|
|
127
129
|
next_annotation: ["n"],
|
|
128
130
|
prev_annotation: ["p"],
|
|
@@ -132,9 +134,6 @@ export const DEFAULT_KEYS: CueloopConfig["keys"] = {
|
|
|
132
134
|
share: ["S"],
|
|
133
135
|
quit: ["q"],
|
|
134
136
|
walk: ["w"],
|
|
135
|
-
review_cycle: ["b"],
|
|
136
|
-
review_wider: ["]"],
|
|
137
|
-
review_narrower: ["["],
|
|
138
137
|
};
|
|
139
138
|
|
|
140
139
|
const ConfigDocumentSchema = v.object({
|
|
@@ -165,9 +164,9 @@ const UiSchema = v.object({
|
|
|
165
164
|
undefined,
|
|
166
165
|
),
|
|
167
166
|
editor: v.fallback(v.optional(v.string()), undefined),
|
|
168
|
-
review_width: v.fallback(v.optional(v.pipe(v.number(), v.finite())), undefined),
|
|
169
|
-
review_state: v.fallback(v.optional(v.picklist(["expanded", "compact", "hidden"])), undefined),
|
|
170
167
|
theme: v.fallback(v.optional(v.string()), undefined),
|
|
168
|
+
diff_view: v.fallback(v.optional(v.picklist(["unified", "split"])), undefined),
|
|
169
|
+
pins: v.fallback(v.optional(v.array(v.string())), undefined),
|
|
171
170
|
});
|
|
172
171
|
const ObsidianSchema = v.object({
|
|
173
172
|
vault: v.fallback(v.optional(v.string()), undefined),
|
|
@@ -252,9 +251,8 @@ function layer(
|
|
|
252
251
|
if (ui.success) {
|
|
253
252
|
if (ui.output.auto_close !== undefined) out.ui.autoClose = ui.output.auto_close;
|
|
254
253
|
if (ui.output.editor?.trim()) out.ui.editor = ui.output.editor.trim();
|
|
255
|
-
if (ui.output.
|
|
256
|
-
|
|
257
|
-
if (ui.output.review_state !== undefined) out.ui.reviewState = ui.output.review_state;
|
|
254
|
+
if (ui.output.diff_view !== undefined) out.ui.diffView = ui.output.diff_view;
|
|
255
|
+
if (ui.output.pins !== undefined) out.ui.pins = ui.output.pins;
|
|
258
256
|
}
|
|
259
257
|
if (integrations.success && integrations.output.obsidian) {
|
|
260
258
|
mergeObsidian(out.integrations.obsidian, integrations.output.obsidian);
|
|
@@ -272,9 +270,9 @@ export function loadConfig(
|
|
|
272
270
|
themeOverrides: {},
|
|
273
271
|
ui: {
|
|
274
272
|
autoClose: "off",
|
|
275
|
-
reviewState: "expanded",
|
|
276
|
-
reviewWidth: REVIEW_DEFAULT_WIDTH,
|
|
277
273
|
theme: DEFAULT_THEME_NAME,
|
|
274
|
+
diffView: "unified",
|
|
275
|
+
pins: [],
|
|
278
276
|
},
|
|
279
277
|
authors: {},
|
|
280
278
|
actions: [...DEFAULT_QUICK_ACTIONS],
|
|
@@ -403,19 +401,19 @@ export function persistAutoClose(value: AutoClose, userConfigPath?: string): voi
|
|
|
403
401
|
persistUiSetting("auto_close", value === "off" ? '"off"' : String(value), userConfigPath);
|
|
404
402
|
}
|
|
405
403
|
|
|
406
|
-
/** Persist the
|
|
407
|
-
export function
|
|
408
|
-
persistUiSetting("
|
|
404
|
+
/** Persist the selected theme preset (`[ui] theme`) into the user config. */
|
|
405
|
+
export function persistTheme(name: ThemeName, userConfigPath?: string): void {
|
|
406
|
+
persistUiSetting("theme", `"${name}"`, userConfigPath);
|
|
409
407
|
}
|
|
410
408
|
|
|
411
|
-
/** Persist the
|
|
412
|
-
export function
|
|
413
|
-
persistUiSetting("
|
|
409
|
+
/** Persist the diff-view choice (`[ui] diff_view`) into the user config. */
|
|
410
|
+
export function persistDiffView(mode: DiffViewMode, userConfigPath?: string): void {
|
|
411
|
+
persistUiSetting("diff_view", `"${mode}"`, userConfigPath);
|
|
414
412
|
}
|
|
415
413
|
|
|
416
|
-
/** Persist the
|
|
417
|
-
export function
|
|
418
|
-
persistUiSetting("
|
|
414
|
+
/** Persist the pinned-thread ids (`[ui] pins`) into the user config. */
|
|
415
|
+
export function persistPins(ids: readonly string[], userConfigPath?: string): void {
|
|
416
|
+
persistUiSetting("pins", `[${ids.map(tomlString).join(", ")}]`, userConfigPath);
|
|
419
417
|
}
|
|
420
418
|
|
|
421
419
|
function escapeRegExp(text: string): string {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Per-file collapse and full-file weave over the flat diff rows. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
import type { DiffRow } from "./view-diff";
|
|
5
|
+
import { applyFold, weaveFullFileRows } from "./diff-fold";
|
|
6
|
+
|
|
7
|
+
function row(kind: DiffRow["kind"], text: string, extra: Partial<DiffRow> = {}): DiffRow {
|
|
8
|
+
return { kind, text, file: "a.ts", ...extra };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// a two-line change in a four-line file: line 2 swapped, lines 1/3/4 unchanged
|
|
12
|
+
const base: DiffRow[] = [
|
|
13
|
+
row("file", "a.ts"),
|
|
14
|
+
row("hunk", "@@ -1,3 +1,3 @@"),
|
|
15
|
+
row("ctx", "line one", { oldLine: 1, newLine: 1 }),
|
|
16
|
+
row("del", "old two", { oldLine: 2 }),
|
|
17
|
+
row("add", "new two", { newLine: 2 }),
|
|
18
|
+
row("ctx", "line three", { oldLine: 3, newLine: 3 }),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const contents = {
|
|
22
|
+
path: "a.ts",
|
|
23
|
+
oldContents: "line one\nold two\nline three\nline four\n",
|
|
24
|
+
newContents: "line one\nnew two\nline three\nline four\n",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("applyFold", () => {
|
|
28
|
+
test("with no fold state it returns the base rows unchanged", () => {
|
|
29
|
+
expect(applyFold(base, new Set(), new Set(), [contents])).toBe(base);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a collapsed file keeps only its file band", () => {
|
|
33
|
+
// Act
|
|
34
|
+
const folded = applyFold(base, new Set(["a.ts"]), new Set(), [contents]);
|
|
35
|
+
|
|
36
|
+
// Assert
|
|
37
|
+
expect(folded).toHaveLength(1);
|
|
38
|
+
expect(folded[0]).toEqual(row("file", "a.ts"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("an expanded file weaves the unchanged tail line the hunk never showed", () => {
|
|
42
|
+
// Act
|
|
43
|
+
const folded = applyFold(base, new Set(), new Set(["a.ts"]), [contents]);
|
|
44
|
+
|
|
45
|
+
// Assert - the hunk header is gone and "line four" (outside the hunk) now shows
|
|
46
|
+
expect(folded.some((r) => r.kind === "hunk")).toBe(false);
|
|
47
|
+
const four = folded.find((r) => r.text === "line four");
|
|
48
|
+
|
|
49
|
+
expect(four).toEqual(row("ctx", "line four", { oldLine: 4, newLine: 4 }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("expand is a no-op without the file's full contents", () => {
|
|
53
|
+
// Act - no contents entry, so the file keeps its hunks
|
|
54
|
+
const folded = applyFold(base, new Set(), new Set(["a.ts"]), undefined);
|
|
55
|
+
|
|
56
|
+
// Assert
|
|
57
|
+
expect(folded).toEqual(base);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("weaveFullFileRows", () => {
|
|
62
|
+
test("keeps the change rows and fills every unchanged line as context in order", () => {
|
|
63
|
+
// Arrange - the file's code rows (no headers)
|
|
64
|
+
const codeRows = base.slice(2);
|
|
65
|
+
|
|
66
|
+
// Act
|
|
67
|
+
const woven = weaveFullFileRows("a.ts", codeRows, contents.newContents);
|
|
68
|
+
|
|
69
|
+
// Assert - full file: ctx1, del, add, ctx3, ctx4
|
|
70
|
+
expect(woven.map((r) => `${r.kind}:${r.text}`)).toEqual([
|
|
71
|
+
"ctx:line one",
|
|
72
|
+
"del:old two",
|
|
73
|
+
"add:new two",
|
|
74
|
+
"ctx:line three",
|
|
75
|
+
"ctx:line four",
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/diff-fold.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file fold state for the diff sheet, applied over the flat diff rows so the
|
|
3
|
+
* cursor, curation, and rendering all read one array. A collapsed file shows only
|
|
4
|
+
* its file band; an expanded file shows its whole contents with the diff woven in
|
|
5
|
+
* (unchanged lines become context rows), matching VSCode's "expand all lines".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DiffRow } from "./view-diff";
|
|
9
|
+
|
|
10
|
+
/** The full old/new contents a diff carries per file (curatable diffs do). */
|
|
11
|
+
export interface FileContents {
|
|
12
|
+
path: string;
|
|
13
|
+
newContents: string;
|
|
14
|
+
oldContents: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Split file contents into lines, dropping the single trailing newline the file carries. */
|
|
18
|
+
function contentLines(contents: string): string[] {
|
|
19
|
+
if (contents === "") return [];
|
|
20
|
+
|
|
21
|
+
return contents.replace(/\n$/, "").split("\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Weave a file's hunk rows into its full contents: unchanged lines between and
|
|
26
|
+
* around the hunks become context rows, so the whole file renders with the diff
|
|
27
|
+
* inline. `codeRows` are the file's ctx/add/del rows in order (no headers).
|
|
28
|
+
*/
|
|
29
|
+
export function weaveFullFileRows(
|
|
30
|
+
fileName: string,
|
|
31
|
+
codeRows: DiffRow[],
|
|
32
|
+
newContents: string,
|
|
33
|
+
): DiffRow[] {
|
|
34
|
+
const newLines = contentLines(newContents);
|
|
35
|
+
const woven: DiffRow[] = [];
|
|
36
|
+
// the next unchanged old/new line numbers to emit while filling gaps between hunks
|
|
37
|
+
let newCursor = 1;
|
|
38
|
+
let oldCursor = 1;
|
|
39
|
+
const fillTo = (newLine: number): void => {
|
|
40
|
+
while (newCursor < newLine) {
|
|
41
|
+
woven.push({
|
|
42
|
+
kind: "ctx",
|
|
43
|
+
text: newLines[newCursor - 1] ?? "",
|
|
44
|
+
file: fileName,
|
|
45
|
+
oldLine: oldCursor,
|
|
46
|
+
newLine: newCursor,
|
|
47
|
+
});
|
|
48
|
+
newCursor += 1;
|
|
49
|
+
oldCursor += 1;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const row of codeRows) {
|
|
54
|
+
if (row.newLine !== undefined) {
|
|
55
|
+
fillTo(row.newLine);
|
|
56
|
+
woven.push(row);
|
|
57
|
+
newCursor = row.newLine + 1;
|
|
58
|
+
if (row.kind === "ctx") oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
59
|
+
} else {
|
|
60
|
+
// a deletion consumes an old line only; it keeps its place before the additions
|
|
61
|
+
woven.push(row);
|
|
62
|
+
oldCursor = (row.oldLine ?? oldCursor) + 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// the unchanged tail after the last hunk
|
|
66
|
+
while (newCursor <= newLines.length) {
|
|
67
|
+
woven.push({
|
|
68
|
+
kind: "ctx",
|
|
69
|
+
text: newLines[newCursor - 1] ?? "",
|
|
70
|
+
file: fileName,
|
|
71
|
+
oldLine: oldCursor,
|
|
72
|
+
newLine: newCursor,
|
|
73
|
+
});
|
|
74
|
+
newCursor += 1;
|
|
75
|
+
oldCursor += 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return woven;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rebuild the diff rows with per-file fold state. A collapsed file keeps only its
|
|
83
|
+
* file band; an expanded file with known contents shows its whole file woven; every
|
|
84
|
+
* other file keeps its hunks. Row order and the file bands themselves are preserved.
|
|
85
|
+
*/
|
|
86
|
+
export function applyFold(
|
|
87
|
+
base: DiffRow[],
|
|
88
|
+
collapsed: ReadonlySet<string>,
|
|
89
|
+
expanded: ReadonlySet<string>,
|
|
90
|
+
files: readonly FileContents[] | undefined,
|
|
91
|
+
): DiffRow[] {
|
|
92
|
+
if (collapsed.size === 0 && expanded.size === 0) return base;
|
|
93
|
+
const contentsByPath = new Map((files ?? []).map((file) => [file.path, file]));
|
|
94
|
+
const out: DiffRow[] = [];
|
|
95
|
+
let index = 0;
|
|
96
|
+
|
|
97
|
+
while (index < base.length) {
|
|
98
|
+
const row = base[index]!;
|
|
99
|
+
|
|
100
|
+
if (row.kind !== "file") {
|
|
101
|
+
out.push(row);
|
|
102
|
+
index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
out.push(row);
|
|
106
|
+
const file = row.file;
|
|
107
|
+
let end = index + 1;
|
|
108
|
+
|
|
109
|
+
while (end < base.length && base[end]!.kind !== "file") end += 1;
|
|
110
|
+
const body = base.slice(index + 1, end);
|
|
111
|
+
const contents = contentsByPath.get(file);
|
|
112
|
+
|
|
113
|
+
if (collapsed.has(file)) {
|
|
114
|
+
// only the file band; the body is hidden
|
|
115
|
+
} else if (expanded.has(file) && contents) {
|
|
116
|
+
const codeRows = body.filter((bodyRow) => bodyRow.kind !== "hunk");
|
|
117
|
+
|
|
118
|
+
out.push(...weaveFullFileRows(file, codeRows, contents.newContents));
|
|
119
|
+
} else {
|
|
120
|
+
out.push(...body);
|
|
121
|
+
}
|
|
122
|
+
index = end;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return out;
|
|
126
|
+
}
|
package/src/diff-syntax.test.ts
CHANGED
|
@@ -41,6 +41,22 @@ describe("spansByLine", () => {
|
|
|
41
41
|
expect(lineSpans[1]).toEqual([{ start: 0, end: 2, group: "comment" }]);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test("a capture ending past the source clamps to its end instead of walking forever", () => {
|
|
45
|
+
// Arrange - offsets past the string, as byte offsets over a multi-byte line report
|
|
46
|
+
const source = "a│b\ncc";
|
|
47
|
+
const highlights: SimpleHighlight[] = [
|
|
48
|
+
[0, 9, "string"],
|
|
49
|
+
[12, 20, "comment"],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const lineSpans = spansByLine(source, highlights);
|
|
54
|
+
|
|
55
|
+
// Assert - the covered text is colored to the end and the second capture paints nothing
|
|
56
|
+
expect(lineSpans[0]).toEqual([{ start: 0, end: 3, group: "string" }]);
|
|
57
|
+
expect(lineSpans[1]).toEqual([{ start: 0, end: 2, group: "string" }]);
|
|
58
|
+
});
|
|
59
|
+
|
|
44
60
|
test("resolves overlapping captures with later winning", () => {
|
|
45
61
|
// Arrange
|
|
46
62
|
const source = "foo";
|
package/src/diff-syntax.ts
CHANGED
|
@@ -111,10 +111,13 @@ export function spansByLine(source: string, highlights: SimpleHighlight[]): Synt
|
|
|
111
111
|
return Array.from({ length: Math.max(0, end - start) }, () => undefined);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
for (const [start,
|
|
114
|
+
for (const [start, rawEnd, group] of highlights) {
|
|
115
115
|
// A capture can span newlines (block comments, template literals); color the
|
|
116
|
-
// covered columns on every line it crosses, not just the starting line.
|
|
117
|
-
|
|
116
|
+
// covered columns on every line it crosses, not just the starting line. Offsets
|
|
117
|
+
// past the source (byte offsets from a multi-byte line) clamp to its end, or the
|
|
118
|
+
// walk below would sit on the last line forever.
|
|
119
|
+
const end = Math.min(rawEnd, source.length);
|
|
120
|
+
let cursor = Math.max(0, start);
|
|
118
121
|
|
|
119
122
|
while (cursor < end) {
|
|
120
123
|
const line = lineOf(cursor);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discussions: the conversations an annotated surface hosts inline. A root
|
|
3
|
+
* annotation, its replies, and the span they mark, grouped so two roots on the
|
|
4
|
+
* very same text read as one conversation. Pure over the resolved marks, so the
|
|
5
|
+
* plan thread view and the diff sheet group the same way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Annotation, ReviewSession } from "@cueloop/schema";
|
|
9
|
+
import type { Mark } from "./view-plan";
|
|
10
|
+
import { comparePositions, type TextSpan } from "./thread-selection";
|
|
11
|
+
|
|
12
|
+
/** One conversation: a root annotation, its replies, and the span they mark. */
|
|
13
|
+
export interface Discussion {
|
|
14
|
+
key: string;
|
|
15
|
+
/** The annotation replies attach to. */
|
|
16
|
+
rootId: string;
|
|
17
|
+
/** The block the card renders under: where the span ends. */
|
|
18
|
+
blockIndex: number;
|
|
19
|
+
span: TextSpan;
|
|
20
|
+
annotations: Annotation[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function spanKey(span: TextSpan): string {
|
|
24
|
+
return `${span.start.blockIndex}:${span.start.char}:${span.end.blockIndex}:${span.end.char}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Roots group by the span they resolved to (two roots on the very same text
|
|
29
|
+
* read as one conversation); replies join the discussion their replyTo names, or
|
|
30
|
+
* stand as roots when that root is gone.
|
|
31
|
+
*/
|
|
32
|
+
export function discussionsFrom(session: ReviewSession, marks: Map<number, Mark[]>): Discussion[] {
|
|
33
|
+
const byId = new Map(session.annotations.map((annotation) => [annotation.id, annotation]));
|
|
34
|
+
const spanOf = new Map<string, TextSpan>();
|
|
35
|
+
|
|
36
|
+
for (const [displayIndex, blockMarks] of marks) {
|
|
37
|
+
for (const mark of blockMarks) {
|
|
38
|
+
if (!mark.annotationId || spanOf.has(mark.annotationId)) continue;
|
|
39
|
+
spanOf.set(
|
|
40
|
+
mark.annotationId,
|
|
41
|
+
mark.span ?? {
|
|
42
|
+
start: { blockIndex: displayIndex, char: mark.start },
|
|
43
|
+
end: { blockIndex: displayIndex, char: mark.end },
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const resolved = [...spanOf.keys()].map((id) => byId.get(id)!).filter(Boolean);
|
|
49
|
+
const isRoot = (annotation: Annotation): boolean =>
|
|
50
|
+
annotation.replyTo === undefined || !spanOf.has(annotation.replyTo);
|
|
51
|
+
const grouped = new Map<string, Discussion>();
|
|
52
|
+
const discussionOfRoot = new Map<string, Discussion>();
|
|
53
|
+
|
|
54
|
+
for (const annotation of resolved.filter(isRoot)) {
|
|
55
|
+
const span = spanOf.get(annotation.id)!;
|
|
56
|
+
const key = spanKey(span);
|
|
57
|
+
const discussion = grouped.get(key) ?? {
|
|
58
|
+
key,
|
|
59
|
+
rootId: annotation.id,
|
|
60
|
+
blockIndex: span.end.blockIndex,
|
|
61
|
+
span,
|
|
62
|
+
annotations: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
discussion.annotations.push(annotation);
|
|
66
|
+
grouped.set(key, discussion);
|
|
67
|
+
discussionOfRoot.set(annotation.id, discussion);
|
|
68
|
+
}
|
|
69
|
+
for (const annotation of resolved.filter((candidate) => !isRoot(candidate))) {
|
|
70
|
+
discussionOfRoot.get(annotation.replyTo!)?.annotations.push(annotation);
|
|
71
|
+
}
|
|
72
|
+
for (const discussion of grouped.values()) {
|
|
73
|
+
discussion.annotations.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return [...grouped.values()].toSorted((left, right) =>
|
|
77
|
+
comparePositions(left.span.start, right.span.start),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -71,10 +71,23 @@ function baseController(): ReviewController {
|
|
|
71
71
|
dismissToast: mock(),
|
|
72
72
|
display: mock(() => []),
|
|
73
73
|
rows: mock(() => []),
|
|
74
|
+
setFileCollapsed: mock(),
|
|
75
|
+
isFileCollapsed: mock(() => false),
|
|
76
|
+
setFileExpanded: mock(),
|
|
77
|
+
isFileExpanded: mock(() => false),
|
|
78
|
+
canExpandFile: mock(() => false),
|
|
79
|
+
copyFilePath: mock(),
|
|
80
|
+
fileStats: mock(() => new Map()),
|
|
74
81
|
files: mock(() => []),
|
|
75
82
|
working: mock(() => ""),
|
|
83
|
+
projectFiles: mock(() => Promise.resolve<string[]>([])),
|
|
84
|
+
readFile: mock(() => Promise.resolve<string | null>(null)),
|
|
85
|
+
repoFiles: mock(() => Promise.resolve<string[]>([])),
|
|
86
|
+
repoReadFile: mock(() => Promise.resolve<string | null>(null)),
|
|
87
|
+
repoChanges: mock(() => Promise.resolve([])),
|
|
76
88
|
open: mock(),
|
|
77
89
|
deleteSession: mock(),
|
|
90
|
+
renameSession: mock(),
|
|
78
91
|
setSelfName: mock(),
|
|
79
92
|
cut: mock(),
|
|
80
93
|
toggleRejectHunk: mock(),
|
|
@@ -106,7 +119,6 @@ function baseController(): ReviewController {
|
|
|
106
119
|
finishReview: mock(),
|
|
107
120
|
dismissCompletion: mock(),
|
|
108
121
|
optInAutoClose: mock(),
|
|
109
|
-
saveReviewPanel: mock(),
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
124
|
|
|
@@ -123,9 +135,6 @@ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDe
|
|
|
123
135
|
inboxCursor: 0,
|
|
124
136
|
mode: { type: "normal" },
|
|
125
137
|
session: null,
|
|
126
|
-
reviewMode: "expanded",
|
|
127
|
-
reviewWidth: 34,
|
|
128
|
-
terminalWidth: 120,
|
|
129
138
|
focusedAnnotationId: undefined,
|
|
130
139
|
selectedCurationId: undefined,
|
|
131
140
|
railTab: "review",
|
|
@@ -133,13 +142,11 @@ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDe
|
|
|
133
142
|
authorNames: {},
|
|
134
143
|
quickActions: [],
|
|
135
144
|
renameAuthor: mock(),
|
|
145
|
+
renameThread: mock(),
|
|
136
146
|
liveInput: { current: "" },
|
|
137
|
-
reviewWidthRef: { current: 34 },
|
|
138
147
|
setCursor: mock(),
|
|
139
148
|
setInboxCursor: mock(),
|
|
140
149
|
setMode: mock(),
|
|
141
|
-
setReviewMode: mock(),
|
|
142
|
-
setReviewWidth: mock(),
|
|
143
150
|
setRailTab: mock(),
|
|
144
151
|
setSelectedEntryId: mock(),
|
|
145
152
|
setFocusedAnnotationId: mock(),
|
|
@@ -148,6 +155,7 @@ function makeDeps(overrides: Partial<IntentDispatchDeps> = {}): IntentDispatchDe
|
|
|
148
155
|
selectCardFromDocument: mock(),
|
|
149
156
|
runEditorHandOff: mock(),
|
|
150
157
|
openCardEdit: mock(),
|
|
158
|
+
toggleDiffView: mock(),
|
|
151
159
|
...overrides,
|
|
152
160
|
};
|
|
153
161
|
}
|
|
@@ -349,17 +357,15 @@ describe("marker-actions popover", () => {
|
|
|
349
357
|
});
|
|
350
358
|
|
|
351
359
|
describe("openSubmit", () => {
|
|
352
|
-
test("
|
|
360
|
+
test("opens the submit confirm with the default verdict", () => {
|
|
353
361
|
// Arrange
|
|
354
|
-
const deps = makeDeps({ session: sessionWith([])
|
|
362
|
+
const deps = makeDeps({ session: sessionWith([]) });
|
|
355
363
|
const dispatch = createIntentDispatch(deps);
|
|
356
364
|
|
|
357
365
|
// Act
|
|
358
366
|
dispatch({ type: "openSubmit" });
|
|
359
367
|
|
|
360
368
|
// Assert
|
|
361
|
-
expect(deps.setReviewMode).toHaveBeenCalledWith("expanded");
|
|
362
|
-
expect(deps.setRailTab).toHaveBeenCalledWith("review");
|
|
363
369
|
expect(deps.setMode).toHaveBeenCalledWith({ type: "submit", verdict: "approve", summary: "" });
|
|
364
370
|
});
|
|
365
371
|
|
|
@@ -436,6 +442,20 @@ describe("restoreCuration", () => {
|
|
|
436
442
|
});
|
|
437
443
|
});
|
|
438
444
|
|
|
445
|
+
describe("toggleDiffView", () => {
|
|
446
|
+
test("delegates to the App-owned toggle", () => {
|
|
447
|
+
// Arrange
|
|
448
|
+
const deps = makeDeps();
|
|
449
|
+
const dispatch = createIntentDispatch(deps);
|
|
450
|
+
|
|
451
|
+
// Act
|
|
452
|
+
dispatch({ type: "toggleDiffView" });
|
|
453
|
+
|
|
454
|
+
// Assert
|
|
455
|
+
expect(deps.toggleDiffView).toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
439
459
|
describe("exit", () => {
|
|
440
460
|
test("calls the exit hook with 0", () => {
|
|
441
461
|
// Arrange
|