@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.103
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/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +67 -1
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/app-view-model.ts +7 -0
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +4 -0
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +41 -0
- package/src/settings/config.ts +44 -0
- package/src/settings/use-settings-dialog.test.tsx +98 -0
- package/src/settings/use-settings-dialog.tsx +38 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +31 -0
- package/src/thread/components/AgentThreadPane.tsx +12 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.ts +123 -0
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
package/src/thread/tree-view.ts
CHANGED
|
@@ -111,10 +111,12 @@ export function treeRows(history: SessionHistory): TreeRow[] {
|
|
|
111
111
|
};
|
|
112
112
|
|
|
113
113
|
if (label !== undefined) row.label = label;
|
|
114
|
+
|
|
114
115
|
rows.push(row);
|
|
115
116
|
const siblings = children.get(entry.id) ?? [];
|
|
116
117
|
|
|
117
118
|
if (siblings.length === 0) return;
|
|
119
|
+
|
|
118
120
|
const trunk = trunkChild(siblings);
|
|
119
121
|
|
|
120
122
|
for (const child of siblings) if (child !== trunk) walk(child, depth + 1, nextRound);
|
|
@@ -133,9 +135,11 @@ export function treeRows(history: SessionHistory): TreeRow[] {
|
|
|
133
135
|
*/
|
|
134
136
|
export function entryTarget(history: SessionHistory, entryId: string): EntryTarget | null {
|
|
135
137
|
if (entryId === tipOf(history)) return { kind: "here" };
|
|
138
|
+
|
|
136
139
|
const tipBranch = Object.entries(history.tips).find(([, tip]) => tip === entryId);
|
|
137
140
|
|
|
138
141
|
if (tipBranch) return { kind: "switch", branch: tipBranch[0] };
|
|
142
|
+
|
|
139
143
|
const holds = (branch: string): boolean =>
|
|
140
144
|
pathOf(history, tipOf(history, branch)).some((entry) => entry.id === entryId);
|
|
141
145
|
const branch = holds(history.branch)
|
|
@@ -38,10 +38,12 @@ export function useThreadAgent(id: string, home?: string, injected?: ThreadAgent
|
|
|
38
38
|
|
|
39
39
|
const configure = async (api: ThreadAgentClient): Promise<void> => {
|
|
40
40
|
if (!api.agentConfigure) return;
|
|
41
|
+
|
|
41
42
|
const requested = ++revision.current;
|
|
42
43
|
const configured = await api.agentConfigure({ id });
|
|
43
44
|
|
|
44
45
|
if (!cancelled && requested === revision.current) setState(configured);
|
|
46
|
+
|
|
45
47
|
await refresh(api);
|
|
46
48
|
};
|
|
47
49
|
|
|
@@ -59,6 +61,7 @@ export function useThreadAgent(id: string, home?: string, injected?: ThreadAgent
|
|
|
59
61
|
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
64
|
+
|
|
62
65
|
unsubscribe = subscribeThreadState({
|
|
63
66
|
connect: connectThreadObserver({ home, autostart: true }),
|
|
64
67
|
matches: (event) => event.event === "agent.updated" && event.sessionId === id,
|
|
@@ -98,11 +101,13 @@ export function useThreadAgent(id: string, home?: string, injected?: ThreadAgent
|
|
|
98
101
|
request: (client: ThreadAgentClient) => Promise<ThreadAgentState>,
|
|
99
102
|
): Promise<boolean> => {
|
|
100
103
|
if (!client) return false;
|
|
104
|
+
|
|
101
105
|
try {
|
|
102
106
|
const requested = ++revision.current;
|
|
103
107
|
const result = await request(client);
|
|
104
108
|
|
|
105
109
|
if (requested === revision.current) setState(result);
|
|
110
|
+
|
|
106
111
|
setError("");
|
|
107
112
|
|
|
108
113
|
return true;
|
|
@@ -19,6 +19,7 @@ const noop = (): void => {};
|
|
|
19
19
|
|
|
20
20
|
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
21
21
|
if (node.id === id) return node;
|
|
22
|
+
|
|
22
23
|
for (const child of node.getChildren()) {
|
|
23
24
|
const found = findById(child, id);
|
|
24
25
|
|
|
@@ -102,12 +103,14 @@ describe("the diff sheet's scrollbar", () => {
|
|
|
102
103
|
|
|
103
104
|
if (!(scroller instanceof ScrollBoxRenderable))
|
|
104
105
|
throw new Error("diff-scroll is not a scrollbox");
|
|
106
|
+
|
|
105
107
|
const countThumbRows = (): number =>
|
|
106
108
|
setup.captureSpans().lines.filter((line) => {
|
|
107
109
|
let column = 0;
|
|
108
110
|
|
|
109
111
|
for (const span of line.spans) {
|
|
110
112
|
column += span.text.length;
|
|
113
|
+
|
|
111
114
|
if (column >= width) return span.bg !== undefined && hex(span.bg) === DARK.textDim;
|
|
112
115
|
}
|
|
113
116
|
|
|
@@ -26,6 +26,7 @@ export interface ThumbGeometry {
|
|
|
26
26
|
|
|
27
27
|
export function thumbGeometry(state: ScrollState, track: number): ThumbGeometry | null {
|
|
28
28
|
if (track <= 0 || state.height <= state.viewport || state.viewport <= 0) return null;
|
|
29
|
+
|
|
29
30
|
const height = Math.max(1, Math.round((state.viewport / state.height) * track));
|
|
30
31
|
const maxTop = state.height - state.viewport;
|
|
31
32
|
const row = Math.round((state.top / maxTop) * (track - height));
|
|
@@ -64,7 +65,9 @@ export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): R
|
|
|
64
65
|
|
|
65
66
|
const reveal = (): void => {
|
|
66
67
|
setVisible(true);
|
|
68
|
+
|
|
67
69
|
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
|
|
70
|
+
|
|
68
71
|
hideTimerRef.current = setTimeout(() => {
|
|
69
72
|
if (!draggingRef.current) setVisible(false);
|
|
70
73
|
}, HIDE_DELAY_MS);
|
|
@@ -73,6 +76,7 @@ export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): R
|
|
|
73
76
|
// reveal on real scroll movement, never on first mount, then hide once idle
|
|
74
77
|
useEffect(() => {
|
|
75
78
|
if (prevTopRef.current !== null && state.top !== prevTopRef.current) reveal();
|
|
79
|
+
|
|
76
80
|
prevTopRef.current = state.top;
|
|
77
81
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
82
|
}, [state.top]);
|
|
@@ -91,6 +95,7 @@ export function OverlayScrollbar({ scrollbox, theme }: OverlayScrollbarProps): R
|
|
|
91
95
|
const box = scrollbox.current;
|
|
92
96
|
|
|
93
97
|
if (!box || !geometry) return;
|
|
98
|
+
|
|
94
99
|
const row = screenY - (trackRef.current?.y ?? 0) - Math.floor(geometry.height / 2);
|
|
95
100
|
const ratio = Math.min(1, Math.max(0, row / Math.max(1, track - geometry.height)));
|
|
96
101
|
|
|
@@ -45,10 +45,12 @@ export function PromptDialog({
|
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
if (!inputRef.current) return;
|
|
48
|
+
|
|
48
49
|
inputRef.current.focus();
|
|
49
50
|
inputRef.current.cursorOffset = value.length;
|
|
50
51
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
52
|
}, []);
|
|
53
|
+
|
|
52
54
|
if (!isOpen) return null;
|
|
53
55
|
|
|
54
56
|
return (
|
|
@@ -35,11 +35,13 @@ export function ScrollArea({
|
|
|
35
35
|
);
|
|
36
36
|
const attach = (node: ScrollBoxRenderable | null): void => {
|
|
37
37
|
innerRef.current = node;
|
|
38
|
+
|
|
38
39
|
if (scrollRef) scrollRef.current = node;
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
useLayoutEffect(() => {
|
|
42
43
|
if (revealId === undefined) return;
|
|
44
|
+
|
|
43
45
|
innerRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
|
|
44
46
|
}, [revealId]);
|
|
45
47
|
|
|
@@ -60,6 +60,7 @@ export function TabList({ children }: TabListProps): React.ReactNode {
|
|
|
60
60
|
const tabs = useContext(TabsContext);
|
|
61
61
|
|
|
62
62
|
if (!tabs) throw new Error("TabList must render inside Tabs");
|
|
63
|
+
|
|
63
64
|
const tokens = useComponentTheme(tabs.theme);
|
|
64
65
|
const items: { id: string; label: string }[] = [];
|
|
65
66
|
|
|
@@ -31,7 +31,9 @@ function rowGlyph(isFolder: boolean, expanded: boolean, icons: TreeIcons): strin
|
|
|
31
31
|
|
|
32
32
|
function toneColor(tone: TreeTone, theme: Theme): string {
|
|
33
33
|
if (tone === "green") return theme.green;
|
|
34
|
+
|
|
34
35
|
if (tone === "blue") return theme.blue;
|
|
36
|
+
|
|
35
37
|
if (tone === "red") return theme.red;
|
|
36
38
|
|
|
37
39
|
return theme.textDim;
|
|
@@ -90,9 +92,11 @@ export function Tree({
|
|
|
90
92
|
lastClick.current?.id === row.id && isDoubleClick(lastClick.current.stamp, stamp);
|
|
91
93
|
|
|
92
94
|
lastClick.current = { id: row.id, stamp };
|
|
95
|
+
|
|
93
96
|
if (row.isFolder) onToggle?.(row.id);
|
|
94
97
|
else {
|
|
95
98
|
onSelect?.(row.id);
|
|
99
|
+
|
|
96
100
|
if (doubleClick) onDoubleSelect?.(row.id);
|
|
97
101
|
}
|
|
98
102
|
}}
|
|
@@ -66,11 +66,15 @@ export function flattenTree(nodes: readonly TreeNode[], options: FlattenOptions)
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
if (node.status !== undefined) leaf.status = node.status;
|
|
69
|
+
|
|
69
70
|
if (node.badge !== undefined) leaf.badge = node.badge;
|
|
71
|
+
|
|
70
72
|
if (node.icon !== undefined) leaf.icon = node.icon;
|
|
73
|
+
|
|
71
74
|
rows.push(leaf);
|
|
72
75
|
continue;
|
|
73
76
|
}
|
|
77
|
+
|
|
74
78
|
const collapsed = options.flattenEmptyDirectories
|
|
75
79
|
? collapseChain(node)
|
|
76
80
|
: { label: node.label, tail: node, chainIds: [node.id] };
|
|
@@ -85,8 +89,11 @@ export function flattenTree(nodes: readonly TreeNode[], options: FlattenOptions)
|
|
|
85
89
|
};
|
|
86
90
|
|
|
87
91
|
if (node.badge !== undefined) row.badge = node.badge;
|
|
92
|
+
|
|
88
93
|
if (node.icon !== undefined) row.icon = node.icon;
|
|
94
|
+
|
|
89
95
|
rows.push(row);
|
|
96
|
+
|
|
90
97
|
if (expanded) walk(collapsed.tail.children ?? [], depth + 1);
|
|
91
98
|
}
|
|
92
99
|
};
|
|
@@ -19,6 +19,7 @@ export function filetypeFor(language?: string): string | undefined {
|
|
|
19
19
|
const info = (language ?? "").toLowerCase();
|
|
20
20
|
|
|
21
21
|
if (!info) return undefined;
|
|
22
|
+
|
|
22
23
|
const aliases: SyntaxAliases = {
|
|
23
24
|
ts: "typescript",
|
|
24
25
|
js: "javascript",
|
|
@@ -40,6 +41,7 @@ export function filetypeForPath(path: string): string | undefined {
|
|
|
40
41
|
const byBasename = basenameToFiletype.get(basename);
|
|
41
42
|
|
|
42
43
|
if (byBasename) return byBasename;
|
|
44
|
+
|
|
43
45
|
const dotIndex = basename.lastIndexOf(".");
|
|
44
46
|
|
|
45
47
|
if (dotIndex <= 0) return undefined;
|
|
@@ -90,6 +92,7 @@ export function syntaxStyleFor(theme: Theme): SyntaxStyle {
|
|
|
90
92
|
const cached = syntaxStyleCache.get(theme);
|
|
91
93
|
|
|
92
94
|
if (cached) return cached;
|
|
95
|
+
|
|
93
96
|
const style = SyntaxStyle.fromStyles(syntaxGroupStyles(theme));
|
|
94
97
|
|
|
95
98
|
syntaxStyleCache.set(theme, style);
|
|
@@ -109,6 +112,7 @@ export function referenceStyleFor(theme: Theme): ReferenceStyle {
|
|
|
109
112
|
const cached = referenceStyleCache.get(theme);
|
|
110
113
|
|
|
111
114
|
if (cached) return cached;
|
|
115
|
+
|
|
112
116
|
const entry: ReferenceStyle = {
|
|
113
117
|
style: SyntaxStyle.fromStyles({ reference: { fg: theme.accent } }),
|
|
114
118
|
styleId: 0,
|
|
@@ -126,6 +130,7 @@ export function colorForSyntaxGroup(group: string, theme: Theme): string | undef
|
|
|
126
130
|
const direct = styles[group];
|
|
127
131
|
|
|
128
132
|
if (direct) return direct.fg;
|
|
133
|
+
|
|
129
134
|
// capture names are dotted (e.g. "keyword.control"); match the broadest prefix
|
|
130
135
|
const base = group.split(".")[0]!;
|
|
131
136
|
|
|
@@ -39,6 +39,7 @@ export function subscribeToFrames(renderer: Renderer, measure: Measure): () => v
|
|
|
39
39
|
|
|
40
40
|
return () => {
|
|
41
41
|
measures.delete(measure);
|
|
42
|
+
|
|
42
43
|
if (measures.size > 0) return;
|
|
43
44
|
|
|
44
45
|
renderer.off("frame", notify);
|
|
@@ -59,6 +60,7 @@ export function useFrameMeasure<T>(
|
|
|
59
60
|
useEffect(() => {
|
|
60
61
|
// a measurement only needed while a popover is open subscribes no per-frame listener when closed
|
|
61
62
|
if (!active) return;
|
|
63
|
+
|
|
62
64
|
const measure = (): void => {
|
|
63
65
|
const next = read();
|
|
64
66
|
|
|
@@ -66,6 +68,7 @@ export function useFrameMeasure<T>(
|
|
|
66
68
|
};
|
|
67
69
|
|
|
68
70
|
measure();
|
|
71
|
+
|
|
69
72
|
if (renderer) return subscribeToFrames(renderer, measure);
|
|
70
73
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
74
|
}, [renderer, active]);
|
|
@@ -125,6 +125,7 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
|
|
|
125
125
|
const scrollbox = options.scrollbox.current;
|
|
126
126
|
|
|
127
127
|
if (!scrollbox) return null;
|
|
128
|
+
|
|
128
129
|
if (container.current?.scrollbox !== scrollbox) {
|
|
129
130
|
container.current = { scrollbox, view: asElement(scrollContainerView(scrollbox)) };
|
|
130
131
|
}
|
|
@@ -135,6 +136,7 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
|
|
|
135
136
|
const known = viewOfRenderable.current.get(renderable);
|
|
136
137
|
|
|
137
138
|
if (known) return known;
|
|
139
|
+
|
|
138
140
|
const view = asElement(rowView(renderable));
|
|
139
141
|
|
|
140
142
|
viewOfRenderable.current.set(renderable, view);
|
|
@@ -168,9 +170,11 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
|
|
|
168
170
|
const scrollbox = options.scrollbox.current;
|
|
169
171
|
|
|
170
172
|
if (!scrollbox) return;
|
|
173
|
+
|
|
171
174
|
const next = { width: scrollbox.viewport.width, height: scrollbox.viewport.height };
|
|
172
175
|
|
|
173
176
|
if (next.width === last.width && next.height === last.height) return;
|
|
177
|
+
|
|
174
178
|
last = next;
|
|
175
179
|
callback(next);
|
|
176
180
|
});
|
|
@@ -184,6 +188,7 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
|
|
|
184
188
|
const scrollbox = options.scrollbox.current;
|
|
185
189
|
|
|
186
190
|
if (!scrollbox || scrollbox.scrollTop === last) return;
|
|
191
|
+
|
|
187
192
|
last = scrollbox.scrollTop;
|
|
188
193
|
callback(last, false);
|
|
189
194
|
};
|
|
@@ -242,6 +247,7 @@ export function useTerminalVirtualizer(options: TerminalVirtualizerOptions): Ter
|
|
|
242
247
|
totalSize: virtualizer.getTotalSize(),
|
|
243
248
|
measureRef: (index) => (renderable) => {
|
|
244
249
|
if (!renderable) return;
|
|
250
|
+
|
|
245
251
|
const view = viewFor(renderable);
|
|
246
252
|
|
|
247
253
|
indexOfView.current.set(view, index);
|
|
@@ -133,15 +133,21 @@ export function ChangesFileTree({
|
|
|
133
133
|
|
|
134
134
|
useKeyboard((key) => {
|
|
135
135
|
if (!focused || rows.length === 0) return;
|
|
136
|
+
|
|
136
137
|
if (key.name === "j" || key.name === "down")
|
|
137
138
|
return setCursor(Math.min(cursorIndex + 1, rows.length - 1));
|
|
139
|
+
|
|
138
140
|
if (key.name === "k" || key.name === "up") return setCursor(Math.max(cursorIndex - 1, 0));
|
|
141
|
+
|
|
139
142
|
const row = rows[cursorIndex];
|
|
140
143
|
|
|
141
144
|
if (!row) return;
|
|
145
|
+
|
|
142
146
|
if (key.name === "return" || key.name === "enter")
|
|
143
147
|
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
148
|
+
|
|
144
149
|
if (key.name === "l" && row.isFolder && collapsedIds.has(row.id)) return toggle(row.id);
|
|
150
|
+
|
|
145
151
|
if (key.name === "h" && row.isFolder && !collapsedIds.has(row.id)) return toggle(row.id);
|
|
146
152
|
});
|
|
147
153
|
|
|
@@ -162,6 +168,7 @@ export function ChangesFileTree({
|
|
|
162
168
|
const index = rows.findIndex((row) => row.id === id);
|
|
163
169
|
|
|
164
170
|
if (index >= 0) setCursor(index);
|
|
171
|
+
|
|
165
172
|
onSelectFile(id);
|
|
166
173
|
}}
|
|
167
174
|
onDoubleSelect={(id) => onSelectFile(id, true)}
|
|
@@ -12,6 +12,7 @@ import { DARK } from "../../appearance/theme";
|
|
|
12
12
|
|
|
13
13
|
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
14
14
|
if (node.id === id) return node;
|
|
15
|
+
|
|
15
16
|
for (const child of node.getChildren()) {
|
|
16
17
|
const found = findById(child, id);
|
|
17
18
|
|
|
@@ -89,6 +90,7 @@ test("keyboard navigation reveals the selected changed file", async () => {
|
|
|
89
90
|
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
90
91
|
|
|
91
92
|
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
93
|
+
|
|
92
94
|
const scrollSteps: number[] = [];
|
|
93
95
|
|
|
94
96
|
for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
|
|
@@ -173,6 +175,7 @@ test("horizontal name scrolling reveals the final extension beside the status",
|
|
|
173
175
|
const label = findById(setup.renderer.root, `tree-label-${path}`);
|
|
174
176
|
|
|
175
177
|
if (!(label instanceof TextRenderable)) throw new Error("file label missing");
|
|
178
|
+
|
|
176
179
|
expect(label.maxScrollX).toBeGreaterThan(0);
|
|
177
180
|
label.scrollX = label.maxScrollX;
|
|
178
181
|
await settle(setup);
|
|
@@ -91,6 +91,7 @@ function EditorTabButton({
|
|
|
91
91
|
|
|
92
92
|
lastClick.current = stamp;
|
|
93
93
|
onSelect();
|
|
94
|
+
|
|
94
95
|
if (doubleClick && tab.preview) onKeep?.();
|
|
95
96
|
}}
|
|
96
97
|
onMouseOver={() => setHovered(true)}
|
|
@@ -152,6 +153,7 @@ export function visibleTabWindow(
|
|
|
152
153
|
): TabWindow {
|
|
153
154
|
// unmeasured strip: show everything, the header clips until the width is known
|
|
154
155
|
if (stripWidth <= 0) return { first: 0, end: tabs.length };
|
|
156
|
+
|
|
155
157
|
const width = (index: number): number => tabCellWidth(tabs[index]!, commentCount(tabs[index]!));
|
|
156
158
|
const fitEnd = (first: number): number => {
|
|
157
159
|
let used = first > 0 ? 1 : 0;
|
|
@@ -161,6 +163,7 @@ export function visibleTabWindow(
|
|
|
161
163
|
const remaining = end + 1 < tabs.length ? 1 : 0;
|
|
162
164
|
|
|
163
165
|
if (used + width(end) + remaining > stripWidth) break;
|
|
166
|
+
|
|
164
167
|
used += width(end);
|
|
165
168
|
end++;
|
|
166
169
|
}
|
|
@@ -255,7 +258,9 @@ function EditorGroupPane({
|
|
|
255
258
|
|
|
256
259
|
useSharedKeyboard((key) => {
|
|
257
260
|
if (!menuOpen) return;
|
|
261
|
+
|
|
258
262
|
if (key.name === "escape") return menuControl.closeMenu();
|
|
263
|
+
|
|
259
264
|
const direction = splitDirectionForKey(key.name);
|
|
260
265
|
|
|
261
266
|
if (direction && canSplit(direction)) {
|
|
@@ -266,6 +271,7 @@ function EditorGroupPane({
|
|
|
266
271
|
|
|
267
272
|
useEffect(() => {
|
|
268
273
|
if (!menuOpen) return;
|
|
274
|
+
|
|
269
275
|
setOverlay(
|
|
270
276
|
"split-menu",
|
|
271
277
|
<box
|
|
@@ -282,6 +288,7 @@ function EditorGroupPane({
|
|
|
282
288
|
<SplitMenu
|
|
283
289
|
onPick={(direction) => {
|
|
284
290
|
if (!canSplit(direction)) return;
|
|
291
|
+
|
|
285
292
|
props.onSplit(group.id, direction);
|
|
286
293
|
menuControl.closeMenu();
|
|
287
294
|
}}
|
|
@@ -313,6 +320,7 @@ function EditorGroupPane({
|
|
|
313
320
|
setWasZoomed(props.zoomed);
|
|
314
321
|
setFirstVisible(0);
|
|
315
322
|
}
|
|
323
|
+
|
|
316
324
|
const activeIndex = Math.max(
|
|
317
325
|
0,
|
|
318
326
|
group.tabs.findIndex((tab) => tab.id === active?.id),
|
|
@@ -63,6 +63,7 @@ export function FileContentsView({
|
|
|
63
63
|
</box>
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
|
+
|
|
66
67
|
if (loaded.lines === null) {
|
|
67
68
|
// a file that no longer reads in the Changes/Project view is one the working tree deleted;
|
|
68
69
|
// the bottom pad lifts the text one row so it lines up with the footer-shortened thread empty state
|
|
@@ -74,6 +75,7 @@ export function FileContentsView({
|
|
|
74
75
|
</box>
|
|
75
76
|
);
|
|
76
77
|
}
|
|
78
|
+
|
|
77
79
|
const content = loaded.lines.join("\n");
|
|
78
80
|
|
|
79
81
|
return (
|
|
@@ -141,6 +141,7 @@ export function GridTabContent(props: {
|
|
|
141
141
|
/>
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
|
+
|
|
144
145
|
if (tab.kind !== "file") {
|
|
145
146
|
return (
|
|
146
147
|
<ChangesTabBody
|
|
@@ -156,6 +157,7 @@ export function GridTabContent(props: {
|
|
|
156
157
|
/>
|
|
157
158
|
);
|
|
158
159
|
}
|
|
160
|
+
|
|
159
161
|
// a single-file tab shows that file's rows alone, so its row indices are its own: comments and
|
|
160
162
|
// the caret report back in whole-diff indices; a single-file tab omits only the collapse action
|
|
161
163
|
const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
|
|
@@ -11,6 +11,7 @@ import { DARK } from "../../appearance/theme";
|
|
|
11
11
|
|
|
12
12
|
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
13
13
|
if (node.id === id) return node;
|
|
14
|
+
|
|
14
15
|
for (const child of node.getChildren()) {
|
|
15
16
|
const found = findById(child, id);
|
|
16
17
|
|
|
@@ -64,6 +65,7 @@ test("keyboard navigation reveals the selected project row", async () => {
|
|
|
64
65
|
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
65
66
|
|
|
66
67
|
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
68
|
+
|
|
67
69
|
const scrollSteps: number[] = [];
|
|
68
70
|
|
|
69
71
|
for (const [index, key] of Array.from({ length: 15 }, () => "j").entries()) {
|
|
@@ -101,6 +103,7 @@ test("project wheel scrolling accelerates during a gesture and resets after a pa
|
|
|
101
103
|
const scrollbox = findById(setup.renderer.root, "tree-scroll");
|
|
102
104
|
|
|
103
105
|
if (!(scrollbox instanceof ScrollBoxRenderable)) throw new Error("tree scrollbox missing");
|
|
106
|
+
|
|
104
107
|
const acceleration = scrollbox.scrollAcceleration;
|
|
105
108
|
const first = acceleration.tick(1000);
|
|
106
109
|
const burst = Array.from({ length: 10 }, (_, index) => acceleration.tick(1016 + index * 16));
|
|
@@ -50,16 +50,22 @@ export function ProjectTreeView({
|
|
|
50
50
|
|
|
51
51
|
useKeyboard((key) => {
|
|
52
52
|
if (!focused || rows.length === 0) return;
|
|
53
|
+
|
|
53
54
|
if (key.name === "j" || key.name === "down")
|
|
54
55
|
return setCursorId(rows[Math.min(cursorIndex + 1, rows.length - 1)]!.id);
|
|
56
|
+
|
|
55
57
|
if (key.name === "k" || key.name === "up")
|
|
56
58
|
return setCursorId(rows[Math.max(cursorIndex - 1, 0)]!.id);
|
|
59
|
+
|
|
57
60
|
const row = rows[cursorIndex];
|
|
58
61
|
|
|
59
62
|
if (!row) return;
|
|
63
|
+
|
|
60
64
|
if (key.name === "return" || key.name === "enter")
|
|
61
65
|
return row.isFolder ? toggle(row.id) : onSelectFile(row.id, true);
|
|
66
|
+
|
|
62
67
|
if (key.name === "l" && row.isFolder && !expandedIds.has(row.id)) return toggle(row.id);
|
|
68
|
+
|
|
63
69
|
if (key.name === "h" && row.isFolder && expandedIds.has(row.id)) return toggle(row.id);
|
|
64
70
|
});
|
|
65
71
|
|
|
@@ -69,6 +75,7 @@ export function ProjectTreeView({
|
|
|
69
75
|
|
|
70
76
|
const refreshFiles = async (): Promise<void> => {
|
|
71
77
|
if (requestInFlight) return;
|
|
78
|
+
|
|
72
79
|
requestInFlight = true;
|
|
73
80
|
|
|
74
81
|
try {
|
|
@@ -112,6 +119,7 @@ export function ProjectTreeView({
|
|
|
112
119
|
</box>
|
|
113
120
|
);
|
|
114
121
|
}
|
|
122
|
+
|
|
115
123
|
if (paths.length === 0) {
|
|
116
124
|
return (
|
|
117
125
|
<box
|
|
@@ -42,6 +42,7 @@ function trailerFor(row: TreeRow): string {
|
|
|
42
42
|
const parts: string[] = [];
|
|
43
43
|
|
|
44
44
|
if (row.tips.length) parts.push(`← ${row.tips.join(", ")}`);
|
|
45
|
+
|
|
45
46
|
if (row.label !== undefined) parts.push(`⚑ ${row.label}`);
|
|
46
47
|
|
|
47
48
|
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
@@ -67,6 +68,7 @@ export function TreePane({
|
|
|
67
68
|
// the trunk draws last, so a long history opens with its tip below the fold: keep the row in view
|
|
68
69
|
useEffect(() => {
|
|
69
70
|
if (revealId === undefined) return;
|
|
71
|
+
|
|
70
72
|
try {
|
|
71
73
|
scrollRef.current?.scrollChildIntoView(`tree-row-${revealId}`);
|
|
72
74
|
} catch {
|
|
@@ -221,9 +221,11 @@ export function keepFileTab(node: EditorNode, groupId: string, tabId: string): E
|
|
|
221
221
|
/** Drop empty groups and collapse a branch that ends up with one child; null when nothing remains. */
|
|
222
222
|
export function pruneEmpty(node: EditorNode): EditorNode | null {
|
|
223
223
|
if (node.type === "group") return node.tabs.length > 0 ? node : null;
|
|
224
|
+
|
|
224
225
|
const kept = node.children.map(pruneEmpty).filter((child): child is EditorNode => child !== null);
|
|
225
226
|
|
|
226
227
|
if (kept.length === 0) return null;
|
|
228
|
+
|
|
227
229
|
if (kept.length === 1) return kept[0]!;
|
|
228
230
|
|
|
229
231
|
return { ...node, children: kept };
|
|
@@ -254,6 +256,7 @@ export function splitGroup(
|
|
|
254
256
|
const active = group.tabs.find((tab) => tab.id === group.activeTabId) ?? group.tabs[0];
|
|
255
257
|
|
|
256
258
|
if (active === undefined) return group;
|
|
259
|
+
|
|
257
260
|
const movedTab: EditorTab =
|
|
258
261
|
active.kind === "welcome" ? { ...active, id: freshId() } : { ...active, preview: false };
|
|
259
262
|
const newGroup = makeGroup([movedTab]);
|
|
@@ -77,6 +77,7 @@ export function buildFileTree(
|
|
|
77
77
|
: { id: prefix, label: segment, children: new Map() };
|
|
78
78
|
level.set(segment, node);
|
|
79
79
|
}
|
|
80
|
+
|
|
80
81
|
if (!isLeaf) level = node.children!;
|
|
81
82
|
});
|
|
82
83
|
}
|
|
@@ -104,6 +105,7 @@ export function buildPathTree(paths: readonly string[]): TreeNode[] {
|
|
|
104
105
|
: { id: prefix, label: segment, children: new Map() };
|
|
105
106
|
level.set(segment, node);
|
|
106
107
|
}
|
|
108
|
+
|
|
107
109
|
if (!isLeaf) level = node.children!;
|
|
108
110
|
});
|
|
109
111
|
}
|
|
@@ -50,6 +50,7 @@ function initialPanes(options?: ChangesWorkbenchOptions): InitialPanes {
|
|
|
50
50
|
zoomed: layout.zoomChanges && changesOpen,
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
|
|
53
54
|
const welcomeSeed = options?.seed === "welcome";
|
|
54
55
|
|
|
55
56
|
return {
|
|
@@ -96,13 +97,16 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
|
|
|
96
97
|
|
|
97
98
|
const syncSession = (sessionId: string | undefined, isDiff: boolean): void => {
|
|
98
99
|
if (sessionId === undefined || sessionId === seenSession.current) return;
|
|
100
|
+
|
|
99
101
|
seenSession.current = sessionId;
|
|
100
102
|
setProjectOpen(isDiff);
|
|
101
103
|
setChangesOpen(isDiff);
|
|
102
104
|
setProjectMode(isDiff ? (options?.layout?.projectMode ?? "changes") : "tree");
|
|
103
105
|
rememberedChanges.current = isDiff;
|
|
106
|
+
|
|
104
107
|
// a non-diff thread has no Changes editor; leaving zoom on would strand the hidden Thread pane
|
|
105
108
|
if (!isDiff) setZoomed(false);
|
|
109
|
+
|
|
106
110
|
setGrid(makeGroup([changesTab()]));
|
|
107
111
|
setFocusedGroup(null);
|
|
108
112
|
};
|
|
@@ -148,6 +152,7 @@ export function useChangesWorkbench(options?: ChangesWorkbenchOptions): ChangesW
|
|
|
148
152
|
|
|
149
153
|
return makeGroup([changesTab()]);
|
|
150
154
|
}
|
|
155
|
+
|
|
151
156
|
// re-home focus when the closed group was pruned away, so the next open has a live target
|
|
152
157
|
setFocusedGroup((current) =>
|
|
153
158
|
current !== null && containsGroup(pruned, current) ? current : firstGroupId(pruned),
|
|
@@ -24,8 +24,10 @@ export function useRememberLayout(
|
|
|
24
24
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
+
|
|
27
28
|
// only the active workbench persists: on a bare launch App's unused panes must not clobber the shell's
|
|
28
29
|
if (!active || layout === undefined) return;
|
|
30
|
+
|
|
29
31
|
persistLayout(layoutFromPanes({ threads, changesOpen, projectOpen, zoomChanges }));
|
|
30
32
|
}, [active, layout, threads, changesOpen, projectOpen, zoomChanges]);
|
|
31
33
|
}
|
|
@@ -51,6 +51,7 @@ export function useRepoChanges({
|
|
|
51
51
|
|
|
52
52
|
const refreshChanges = async (): Promise<void> => {
|
|
53
53
|
if (requestInFlight) return;
|
|
54
|
+
|
|
54
55
|
requestInFlight = true;
|
|
55
56
|
|
|
56
57
|
try {
|
|
@@ -83,6 +84,7 @@ export function useRepoChanges({
|
|
|
83
84
|
|
|
84
85
|
return () => {
|
|
85
86
|
cancelled = true;
|
|
87
|
+
|
|
86
88
|
if (timer !== null) clearInterval(timer);
|
|
87
89
|
};
|
|
88
90
|
}, [visible, diffSourceKey, refreshAutomatically]);
|