@cueloop/client 0.1.0-alpha.63 → 0.1.0-alpha.65
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 +7 -6
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.export.test.tsx +2 -2
- package/src/App.plan-review.test.tsx +76 -247
- package/src/App.readonly.test.tsx +80 -35
- package/src/App.rendering.test.tsx +5 -6
- package/src/App.review-panel.test.tsx +29 -32
- package/src/App.submit-confirm.test.tsx +27 -12
- package/src/App.test.tsx +73 -49
- package/src/App.toast.test.tsx +34 -22
- package/src/App.tree.test.tsx +224 -0
- package/src/App.tsx +134 -87
- package/src/app-screens.tsx +27 -1
- package/src/app-view-model.ts +7 -109
- package/src/components/AnnotationCard.tsx +2 -2
- package/src/components/DiffSheet.scroll.test.tsx +5 -2
- package/src/components/DiffSheet.tsx +1 -6
- package/src/components/KeybindsDialog.tsx +61 -17
- package/src/components/PrototypeSheet.tsx +5 -9
- package/src/components/ReviewPanel.stories.tsx +14 -0
- package/src/components/ReviewRail.stories.tsx +14 -0
- package/src/components/ReviewRail.tsx +15 -6
- package/src/components/ThreadView.selection.test.tsx +812 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +1620 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +156 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +120 -172
- package/src/components/primitives/Tabs.tsx +1 -1
- package/src/components/story-fixtures.ts +6 -2
- package/src/components/story.ts +16 -4
- package/src/components/syntax-highlight.ts +10 -2
- package/src/components/terminal-pane.test.tsx +1 -1
- package/src/components/terminal-pane.ts +7 -1
- package/src/config.ts +112 -99
- package/src/diff-hunk-curate.ts +4 -104
- package/src/editor.ts +5 -1
- package/src/ghostty-terminal.test.ts +1 -1
- package/src/herdr.ts +10 -2
- package/src/index.ts +1 -0
- package/src/intent-dispatch.test.ts +235 -55
- package/src/intent-dispatch.ts +104 -34
- package/src/key-bindings.test.ts +1 -1
- package/src/key-bindings.ts +16 -7
- package/src/keymap.test.ts +9 -11
- package/src/keymap.ts +14 -7
- package/src/kitty-image.test.ts +3 -3
- package/src/prototype-browser.ts +7 -6
- package/src/serve.ts +1 -1
- package/src/session-controller.curate.test.ts +106 -4
- package/src/session-controller.share.test.ts +220 -70
- package/src/session-controller.tree.test.ts +228 -0
- package/src/session-controller.ts +370 -94
- package/src/share.ts +117 -17
- package/src/test-support.test.ts +193 -1
- package/src/test-support.ts +69 -1
- package/src/theme-presets.ts +9 -2
- package/src/thread-chords.test.ts +154 -0
- package/src/thread-chords.ts +144 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/view-diff.ts +1 -4
- package/src/view-plan.ts +35 -19
- package/src/components/PlanSheet.stories.tsx +0 -113
- package/src/components/PlanSheet.tsx +0 -422
- package/src/components/plan-sheet-run-style.ts +0 -90
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
appendEntry,
|
|
4
|
+
createBranch,
|
|
5
|
+
historyFromLinear,
|
|
6
|
+
labelTip,
|
|
7
|
+
navigateTo,
|
|
8
|
+
switchBranch,
|
|
9
|
+
tipOf,
|
|
10
|
+
type SessionHistory,
|
|
11
|
+
} from "@cueloop/schema";
|
|
12
|
+
import { entryTarget, treeRows } from "./tree-view";
|
|
13
|
+
|
|
14
|
+
const AT = "2026-09-01T10:00:00.000Z";
|
|
15
|
+
const LATER = "2026-09-01T11:00:00.000Z";
|
|
16
|
+
|
|
17
|
+
/** rev1 -> comment a1 on main; a branch "alt" off a1 with its own comment; main continues with rev2. */
|
|
18
|
+
function forked(): SessionHistory {
|
|
19
|
+
let history = historyFromLinear({
|
|
20
|
+
id: "ses_1",
|
|
21
|
+
revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
|
|
22
|
+
annotations: [],
|
|
23
|
+
verdict: null,
|
|
24
|
+
createdAt: AT,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
|
|
28
|
+
history = labelTip(history, "start");
|
|
29
|
+
history = createBranch(history, "alt");
|
|
30
|
+
history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
|
|
31
|
+
history = switchBranch(history, "main");
|
|
32
|
+
history = appendEntry(history, {
|
|
33
|
+
type: "revision",
|
|
34
|
+
by: "agent",
|
|
35
|
+
content: "v2",
|
|
36
|
+
createdAt: LATER,
|
|
37
|
+
}).history;
|
|
38
|
+
|
|
39
|
+
return history;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("treeRows", () => {
|
|
43
|
+
test("the current branch is the trunk; other segments indent under the entry they grew from", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const rows = treeRows(forked());
|
|
46
|
+
|
|
47
|
+
// Assert: rev1, a1 (labelled, alt hangs off it), the alt comment indented, then rev2 as main's tip
|
|
48
|
+
expect(rows.map((row) => [row.depth, row.text, row.onPath])).toEqual([
|
|
49
|
+
[0, "revision 1", true],
|
|
50
|
+
[0, "comment", true],
|
|
51
|
+
[1, "comment", false],
|
|
52
|
+
[0, "revision 2", true],
|
|
53
|
+
]);
|
|
54
|
+
expect(rows[1]!.label).toBe("start");
|
|
55
|
+
expect(rows[2]!.tips).toEqual(["alt"]);
|
|
56
|
+
expect(rows[3]!.tips).toEqual(["main"]);
|
|
57
|
+
expect(rows[3]!.isCurrentTip).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("on the other branch the trunk follows it and main's continuation indents", () => {
|
|
61
|
+
// Act
|
|
62
|
+
const rows = treeRows(switchBranch(forked(), "alt"));
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
65
|
+
expect(rows.map((row) => [row.depth, row.text, row.onPath])).toEqual([
|
|
66
|
+
[0, "revision 1", true],
|
|
67
|
+
[0, "comment", true],
|
|
68
|
+
[1, "revision 2", false],
|
|
69
|
+
[0, "comment", true],
|
|
70
|
+
]);
|
|
71
|
+
expect(rows[3]!.isCurrentTip).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a branch summary reads as a return with its text", () => {
|
|
75
|
+
// Arrange
|
|
76
|
+
const history = navigateTo(forked(), forked().entries[0]!.id, { summary: "too early" });
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const rows = treeRows(history);
|
|
80
|
+
|
|
81
|
+
// Assert: the summary hangs under rev1 as the trunk, the abandoned segment indents
|
|
82
|
+
expect(rows.find((row) => row.glyph === "↩")?.text).toBe('"too early"');
|
|
83
|
+
expect(rows.find((row) => row.glyph === "↩")?.isCurrentTip).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("entryTarget", () => {
|
|
88
|
+
test("the tip is here, another tip is a switch, an ancestor is a navigate on its branch", () => {
|
|
89
|
+
// Arrange
|
|
90
|
+
const history = forked();
|
|
91
|
+
const [rev1, a1, altComment, rev2] = history.entries;
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(entryTarget(history, rev2!.id)).toEqual({ kind: "here" });
|
|
95
|
+
expect(entryTarget(history, altComment!.id)).toEqual({ kind: "switch", branch: "alt" });
|
|
96
|
+
expect(entryTarget(history, a1!.id)).toEqual({
|
|
97
|
+
kind: "navigate",
|
|
98
|
+
entryId: a1!.id,
|
|
99
|
+
branch: "main",
|
|
100
|
+
});
|
|
101
|
+
expect(entryTarget(history, rev1!.id)?.kind).toBe("navigate");
|
|
102
|
+
expect(tipOf(history)).toBe(rev2!.id);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("an entry only another branch reaches is a navigate on that branch", () => {
|
|
106
|
+
// Arrange: alt gets a second comment so its first is no longer a tip
|
|
107
|
+
let history = switchBranch(forked(), "alt");
|
|
108
|
+
|
|
109
|
+
history = appendEntry(history, {
|
|
110
|
+
type: "comment",
|
|
111
|
+
annotationId: "b2",
|
|
112
|
+
createdAt: LATER,
|
|
113
|
+
}).history;
|
|
114
|
+
const altFirst = history.entries.find(
|
|
115
|
+
(entry) => entry.type === "comment" && entry.annotationId === "b1",
|
|
116
|
+
)!;
|
|
117
|
+
|
|
118
|
+
history = switchBranch(history, "main");
|
|
119
|
+
|
|
120
|
+
// Assert
|
|
121
|
+
expect(entryTarget(history, altFirst.id)).toEqual({
|
|
122
|
+
kind: "navigate",
|
|
123
|
+
entryId: altFirst.id,
|
|
124
|
+
branch: "alt",
|
|
125
|
+
});
|
|
126
|
+
expect(entryTarget(history, "e_nope")).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/tree-view.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session tree as rows for the rail's Tree tab. The trunk is the current
|
|
3
|
+
* branch's path (falling back to main), drawn at depth 0; every other segment
|
|
4
|
+
* indents under the entry it grew from. Rows know whether they sit on the
|
|
5
|
+
* active path, which branch tips rest on them, and the checkpoint name they
|
|
6
|
+
* carry, so the pane only paints. Pure: no controller, no daemon.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
MAIN_BRANCH,
|
|
11
|
+
pathOf,
|
|
12
|
+
tipOf,
|
|
13
|
+
type SessionEntry,
|
|
14
|
+
type SessionHistory,
|
|
15
|
+
} from "@cueloop/schema";
|
|
16
|
+
|
|
17
|
+
export interface TreeRow {
|
|
18
|
+
entryId: string;
|
|
19
|
+
/** Branch nesting: 0 is the trunk, each segment off it one deeper. */
|
|
20
|
+
depth: number;
|
|
21
|
+
/** One glyph naming the entry kind, for the left column. */
|
|
22
|
+
glyph: string;
|
|
23
|
+
/** What the entry is, in a few words. */
|
|
24
|
+
text: string;
|
|
25
|
+
/** On the current branch's path: painted bright; everything else dim. */
|
|
26
|
+
onPath: boolean;
|
|
27
|
+
/** Branches whose tip rests on this entry. */
|
|
28
|
+
tips: string[];
|
|
29
|
+
/** The current branch's tip: where the next entry lands. */
|
|
30
|
+
isCurrentTip: boolean;
|
|
31
|
+
/** The checkpoint name, when labelled. */
|
|
32
|
+
label?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What going to an entry means from where the reviewer stands. */
|
|
36
|
+
export type EntryTarget =
|
|
37
|
+
| { kind: "here" }
|
|
38
|
+
| { kind: "switch"; branch: string }
|
|
39
|
+
| { kind: "navigate"; entryId: string; branch: string };
|
|
40
|
+
|
|
41
|
+
/** The one-glyph, few-words reading of an entry a row shows. */
|
|
42
|
+
export interface EntryDescription {
|
|
43
|
+
glyph: string;
|
|
44
|
+
text: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The entry's kind, said briefly; agent revisions count their round along the path. */
|
|
48
|
+
export function describeEntry(entry: SessionEntry, round?: number): EntryDescription {
|
|
49
|
+
switch (entry.type) {
|
|
50
|
+
case "revision":
|
|
51
|
+
return entry.by === "agent"
|
|
52
|
+
? { glyph: "◉", text: round === undefined ? "revision" : `revision ${round}` }
|
|
53
|
+
: { glyph: "○", text: "edit" };
|
|
54
|
+
case "comment":
|
|
55
|
+
return { glyph: "·", text: "comment" };
|
|
56
|
+
case "comment-removed":
|
|
57
|
+
return { glyph: "·", text: "comment removed" };
|
|
58
|
+
case "verdict":
|
|
59
|
+
return {
|
|
60
|
+
glyph: entry.verdict.kind === "approve" ? "✓" : "✗",
|
|
61
|
+
text: entry.verdict.kind.replace("_", " "),
|
|
62
|
+
};
|
|
63
|
+
case "branch-summary":
|
|
64
|
+
return { glyph: "↩", text: entry.text ? `"${entry.text}"` : "returned" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Rows in drawing order: trunk first at each fork, older segments before newer. */
|
|
69
|
+
export function treeRows(history: SessionHistory): TreeRow[] {
|
|
70
|
+
const children = new Map<string | null, SessionEntry[]>();
|
|
71
|
+
|
|
72
|
+
for (const entry of history.entries) {
|
|
73
|
+
const siblings = children.get(entry.parentId) ?? [];
|
|
74
|
+
|
|
75
|
+
siblings.push(entry);
|
|
76
|
+
children.set(entry.parentId, siblings);
|
|
77
|
+
}
|
|
78
|
+
for (const siblings of children.values()) {
|
|
79
|
+
siblings.sort(
|
|
80
|
+
(left, right) =>
|
|
81
|
+
left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const onPath = new Set(pathOf(history).map((entry) => entry.id));
|
|
85
|
+
const onMain = new Set(pathOf(history, tipOf(history, MAIN_BRANCH)).map((entry) => entry.id));
|
|
86
|
+
const tipsByEntry = new Map<string, string[]>();
|
|
87
|
+
|
|
88
|
+
for (const [branch, tip] of Object.entries(history.tips)) {
|
|
89
|
+
tipsByEntry.set(tip, [...(tipsByEntry.get(tip) ?? []), branch]);
|
|
90
|
+
}
|
|
91
|
+
const currentTip = tipOf(history);
|
|
92
|
+
const rows: TreeRow[] = [];
|
|
93
|
+
// the trunk continues through the child on the active path, else on main, else the oldest
|
|
94
|
+
const trunkChild = (siblings: SessionEntry[]): SessionEntry =>
|
|
95
|
+
siblings.find((entry) => onPath.has(entry.id)) ??
|
|
96
|
+
siblings.find((entry) => onMain.has(entry.id)) ??
|
|
97
|
+
siblings[0]!;
|
|
98
|
+
const walk = (entry: SessionEntry, depth: number, round: number): void => {
|
|
99
|
+
const nextRound = entry.type === "revision" && entry.by === "agent" ? round + 1 : round;
|
|
100
|
+
const described = describeEntry(
|
|
101
|
+
entry,
|
|
102
|
+
entry.type === "revision" && entry.by === "agent" ? nextRound : undefined,
|
|
103
|
+
);
|
|
104
|
+
const label = history.labels[entry.id];
|
|
105
|
+
const row: TreeRow = {
|
|
106
|
+
entryId: entry.id,
|
|
107
|
+
depth,
|
|
108
|
+
glyph: described.glyph,
|
|
109
|
+
text: described.text,
|
|
110
|
+
onPath: onPath.has(entry.id),
|
|
111
|
+
tips: tipsByEntry.get(entry.id) ?? [],
|
|
112
|
+
isCurrentTip: entry.id === currentTip,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (label !== undefined) row.label = label;
|
|
116
|
+
rows.push(row);
|
|
117
|
+
const siblings = children.get(entry.id) ?? [];
|
|
118
|
+
|
|
119
|
+
if (siblings.length === 0) return;
|
|
120
|
+
const trunk = trunkChild(siblings);
|
|
121
|
+
|
|
122
|
+
for (const child of siblings) if (child !== trunk) walk(child, depth + 1, nextRound);
|
|
123
|
+
walk(trunk, depth, nextRound);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (const root of children.get(null) ?? []) walk(root, 0, 0);
|
|
127
|
+
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Going to an entry: nothing when it is the current tip; a switch when it is
|
|
133
|
+
* another branch's tip; otherwise a navigate on the branch whose path holds
|
|
134
|
+
* it - the current branch when it can, else the first branch that reaches it.
|
|
135
|
+
*/
|
|
136
|
+
export function entryTarget(history: SessionHistory, entryId: string): EntryTarget | null {
|
|
137
|
+
if (entryId === tipOf(history)) return { kind: "here" };
|
|
138
|
+
const tipBranch = Object.entries(history.tips).find(([, tip]) => tip === entryId);
|
|
139
|
+
|
|
140
|
+
if (tipBranch) return { kind: "switch", branch: tipBranch[0] };
|
|
141
|
+
const holds = (branch: string): boolean =>
|
|
142
|
+
pathOf(history, tipOf(history, branch)).some((entry) => entry.id === entryId);
|
|
143
|
+
const branch = holds(history.branch)
|
|
144
|
+
? history.branch
|
|
145
|
+
: Object.keys(history.tips).find((candidate) => holds(candidate));
|
|
146
|
+
|
|
147
|
+
return branch === undefined ? null : { kind: "navigate", entryId, branch };
|
|
148
|
+
}
|
package/src/view-diff.ts
CHANGED
|
@@ -73,10 +73,7 @@ function fileLabel(file: FileDiffMetadata): string {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Quote-primary anchor for a diff row: neighbors as context selectors. */
|
|
76
|
-
export function diffRowAnchor(
|
|
77
|
-
rows: DiffRow[],
|
|
78
|
-
rowIndex: number,
|
|
79
|
-
): { quote: string; prefix: string; suffix: string } {
|
|
76
|
+
export function diffRowAnchor(rows: DiffRow[], rowIndex: number) {
|
|
80
77
|
const row = rows[rowIndex]!;
|
|
81
78
|
const prev = rows[rowIndex - 1];
|
|
82
79
|
const next = rows[rowIndex + 1];
|
package/src/view-plan.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type Block,
|
|
18
18
|
} from "@cueloop/schema";
|
|
19
19
|
import { wordLevelChanges } from "./diff-intraline";
|
|
20
|
+
import { spanRangeInBlock, type TextSpan } from "./thread-selection";
|
|
20
21
|
|
|
21
22
|
// ── display model ─────────────────────────────
|
|
22
23
|
|
|
@@ -182,6 +183,8 @@ export interface Mark {
|
|
|
182
183
|
end: number;
|
|
183
184
|
role: RunRole;
|
|
184
185
|
annotationId?: string;
|
|
186
|
+
/** The whole anchored stretch in display coordinates; one thread per span. */
|
|
187
|
+
span?: TextSpan;
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
/**
|
|
@@ -207,18 +210,32 @@ export function marksByDisplay(
|
|
|
207
210
|
const resolved = resolveAnchor(annotation.anchor, workBlocks);
|
|
208
211
|
|
|
209
212
|
if (!resolved) continue;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
const startEntry = workEntries[resolved.blockIndex];
|
|
214
|
+
const endEntry = workEntries[resolved.endBlockIndex];
|
|
215
|
+
|
|
216
|
+
if (!startEntry || !endEntry) continue;
|
|
217
|
+
const span: TextSpan = {
|
|
218
|
+
start: { blockIndex: startEntry.displayIndex, char: resolved.start },
|
|
219
|
+
end: { blockIndex: endEntry.displayIndex, char: resolved.end },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// a span paints on every block it covers; the mark carries the whole span
|
|
223
|
+
for (let workIndex = resolved.blockIndex; workIndex <= resolved.endBlockIndex; workIndex++) {
|
|
224
|
+
const entry = workEntries[workIndex]!;
|
|
225
|
+
const range = spanRangeInBlock(span, entry.displayIndex, entry.block.work!.text.length);
|
|
226
|
+
|
|
227
|
+
if (!range) continue;
|
|
228
|
+
const marks = marksByIndex.get(entry.displayIndex) ?? [];
|
|
229
|
+
|
|
230
|
+
marks.push({
|
|
231
|
+
start: range.start,
|
|
232
|
+
end: range.end,
|
|
233
|
+
role: annotation.id === focusedId ? "mark-focus" : "mark-comment",
|
|
234
|
+
annotationId: annotation.id,
|
|
235
|
+
span,
|
|
236
|
+
});
|
|
237
|
+
marksByIndex.set(entry.displayIndex, marks);
|
|
238
|
+
}
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
return marksByIndex;
|
|
@@ -285,12 +302,14 @@ export function inlineStyleRuns(text: string, base: number): StyleRun[] {
|
|
|
285
302
|
|
|
286
303
|
for (const run of inlineRuns(text)) {
|
|
287
304
|
if (run.role === "marker" || run.start === null) continue;
|
|
288
|
-
|
|
305
|
+
const styleRun: StyleRun = {
|
|
289
306
|
text: run.text,
|
|
290
307
|
role: run.role === "text" ? "plain" : run.role,
|
|
291
308
|
start: base + run.start,
|
|
292
|
-
|
|
293
|
-
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (run.href !== undefined) styleRun.href = run.href;
|
|
312
|
+
runs.push(styleRun);
|
|
294
313
|
}
|
|
295
314
|
|
|
296
315
|
return runs;
|
|
@@ -437,10 +456,7 @@ export function workRangeForRendered(
|
|
|
437
456
|
}
|
|
438
457
|
|
|
439
458
|
/** Line delta between two revision contents, for the sheet-header summary. */
|
|
440
|
-
export function revisionDelta(
|
|
441
|
-
previousContent: string,
|
|
442
|
-
nextContent: string,
|
|
443
|
-
): { added: number; removed: number } {
|
|
459
|
+
export function revisionDelta(previousContent: string, nextContent: string) {
|
|
444
460
|
const ops = lcsDiff(previousContent.split("\n"), nextContent.split("\n"));
|
|
445
461
|
let added = 0;
|
|
446
462
|
let removed = 0;
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { DARK } from "../theme";
|
|
3
|
-
import type { Story, StoryMeta } from "./story";
|
|
4
|
-
import { buildDisplay } from "../view-plan";
|
|
5
|
-
import { PlanSheet } from "./PlanSheet";
|
|
6
|
-
import { FIXTURE_PLAN, fixtureDisplay, fixtureMarks, fixturePlanSession } from "./story-fixtures";
|
|
7
|
-
|
|
8
|
-
export const meta: StoryMeta = { title: "PlanSheet" };
|
|
9
|
-
|
|
10
|
-
const callbacks = { onLineActivate: () => {} };
|
|
11
|
-
|
|
12
|
-
// a working copy with the context paragraph cut, so it renders as a del block
|
|
13
|
-
const CUT_WORKING = FIXTURE_PLAN.replace("The daemon persists sessions to disk atomically.\n", "");
|
|
14
|
-
|
|
15
|
-
export const AnnotatedPlan: Story = {
|
|
16
|
-
render: () => (
|
|
17
|
-
<PlanSheet
|
|
18
|
-
session={fixturePlanSession()}
|
|
19
|
-
display={fixtureDisplay()}
|
|
20
|
-
marks={fixtureMarks()}
|
|
21
|
-
cursor={2}
|
|
22
|
-
activeSpan={null}
|
|
23
|
-
compose={null}
|
|
24
|
-
popover={null}
|
|
25
|
-
editOrphanCount={0}
|
|
26
|
-
{...callbacks}
|
|
27
|
-
/>
|
|
28
|
-
),
|
|
29
|
-
expectedColors: [DARK.markCommentBackground, DARK.cursorBackground],
|
|
30
|
-
size: { width: 90, height: 28 },
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const SpanSelection: Story = {
|
|
34
|
-
render: () => (
|
|
35
|
-
<PlanSheet
|
|
36
|
-
session={fixturePlanSession({ annotations: [] })}
|
|
37
|
-
display={fixtureDisplay()}
|
|
38
|
-
marks={new Map()}
|
|
39
|
-
cursor={2}
|
|
40
|
-
activeSpan={{ displayIndex: 2, start: 0, end: 10 }}
|
|
41
|
-
compose={null}
|
|
42
|
-
popover={null}
|
|
43
|
-
editOrphanCount={0}
|
|
44
|
-
{...callbacks}
|
|
45
|
-
/>
|
|
46
|
-
),
|
|
47
|
-
expectedColors: [DARK.accent],
|
|
48
|
-
size: { width: 90, height: 28 },
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const InlineCompose: Story = {
|
|
52
|
-
render: () => (
|
|
53
|
-
<PlanSheet
|
|
54
|
-
session={fixturePlanSession({ annotations: [] })}
|
|
55
|
-
display={fixtureDisplay()}
|
|
56
|
-
marks={new Map()}
|
|
57
|
-
cursor={2}
|
|
58
|
-
activeSpan={{ displayIndex: 2, start: 0, end: 10 }}
|
|
59
|
-
compose={{
|
|
60
|
-
kind: "comment",
|
|
61
|
-
displayIndex: 2,
|
|
62
|
-
quote: "The daemon",
|
|
63
|
-
draft: { text: "Which daemon?", onInput: () => {}, onSave: () => {}, onCancel: () => {} },
|
|
64
|
-
}}
|
|
65
|
-
popover={null}
|
|
66
|
-
editOrphanCount={0}
|
|
67
|
-
{...callbacks}
|
|
68
|
-
/>
|
|
69
|
-
),
|
|
70
|
-
size: { width: 90, height: 30 },
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/** Span mode shows the marker-actions toolbar inline below the marked block. */
|
|
74
|
-
// The marker popover floats as an absolute overlay; the virtual terminal used
|
|
75
|
-
// for story snapshots cannot composite it, so it is covered by the standalone
|
|
76
|
-
// MarkerPopover stories and a functional App test rather than a PlanSheet snapshot.
|
|
77
|
-
|
|
78
|
-
/** A cut block renders struck-through and grayed inline - no red, no [cut] tag. */
|
|
79
|
-
export const WithCutBlock: Story = {
|
|
80
|
-
render: () => (
|
|
81
|
-
<PlanSheet
|
|
82
|
-
session={fixturePlanSession()}
|
|
83
|
-
display={buildDisplay(FIXTURE_PLAN, CUT_WORKING)}
|
|
84
|
-
marks={new Map()}
|
|
85
|
-
cursor={0}
|
|
86
|
-
activeSpan={null}
|
|
87
|
-
compose={null}
|
|
88
|
-
popover={null}
|
|
89
|
-
editOrphanCount={0}
|
|
90
|
-
{...callbacks}
|
|
91
|
-
/>
|
|
92
|
-
),
|
|
93
|
-
expectedColors: [DARK.textDim],
|
|
94
|
-
size: { width: 90, height: 28 },
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export const OrphanBanner: Story = {
|
|
98
|
-
render: () => (
|
|
99
|
-
<PlanSheet
|
|
100
|
-
session={fixturePlanSession()}
|
|
101
|
-
display={fixtureDisplay()}
|
|
102
|
-
marks={new Map()}
|
|
103
|
-
cursor={0}
|
|
104
|
-
activeSpan={null}
|
|
105
|
-
compose={null}
|
|
106
|
-
popover={null}
|
|
107
|
-
editOrphanCount={1}
|
|
108
|
-
{...callbacks}
|
|
109
|
-
/>
|
|
110
|
-
),
|
|
111
|
-
expectedColors: [DARK.red],
|
|
112
|
-
size: { width: 90, height: 28 },
|
|
113
|
-
};
|