@cueloop/client 0.1.0-alpha.66 → 0.1.0-alpha.68
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 +71 -39
- package/src/App.plan-review.test.tsx +20 -21
- package/src/App.readonly.test.tsx +7 -3
- package/src/App.test.tsx +73 -14
- package/src/App.tsx +375 -129
- package/src/App.walk.test.tsx +14 -15
- 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 +176 -45
- package/src/app-view-model.ts +7 -65
- 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 +47 -42
- package/src/components/AppShell.tsx +212 -100
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/ChangesColumn.tsx +49 -26
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +55 -53
- 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.tsx +159 -30
- 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.tsx +8 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.tsx +2 -1
- 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 +2 -2
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +12 -7
- package/src/components/ThreadView.tsx +62 -1440
- package/src/components/ThreadsSidebar.tsx +14 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +509 -559
- 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 +15 -0
- package/src/components/file-tree.ts +59 -4
- package/src/components/pages/Workspace.stories.tsx +2 -2
- package/src/components/primitives/IconButton.tsx +21 -2
- package/src/components/primitives/Tree.tsx +13 -4
- package/src/components/primitives/icons.ts +27 -4
- package/src/components/session-tree.test.ts +20 -0
- package/src/components/session-tree.ts +16 -2
- package/src/components/stories-app.tsx +2 -2
- 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/ghostty-terminal.test.ts +2 -1
- package/src/ghostty-terminal.ts +27 -4
- 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 +153 -29
- 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 +15 -3
- package/src/test-support.ts +66 -15
- 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 +14 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- 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/App.tsx
CHANGED
|
@@ -26,7 +26,10 @@ import {
|
|
|
26
26
|
DEFAULT_QUICK_ACTIONS,
|
|
27
27
|
loadConfig,
|
|
28
28
|
persistAuthorName,
|
|
29
|
+
persistDiffView,
|
|
30
|
+
persistPins,
|
|
29
31
|
type AutoClose,
|
|
32
|
+
type DiffViewMode,
|
|
30
33
|
type QuickAction,
|
|
31
34
|
} from "./config";
|
|
32
35
|
import {
|
|
@@ -39,35 +42,37 @@ import {
|
|
|
39
42
|
import type { Theme } from "./theme";
|
|
40
43
|
import { createReviewController, type ShareTransport } from "./session-controller";
|
|
41
44
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
42
|
-
import { createIntentDispatch, type Mode } from "./intent-dispatch";
|
|
45
|
+
import { createIntentDispatch, type Mode, type RailTab } from "./intent-dispatch";
|
|
43
46
|
import { reduceKey, type KeyState } from "./keymap";
|
|
44
47
|
import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
45
48
|
import { ThemeProvider } from "./components/theme-context";
|
|
46
49
|
import { Button } from "./components/primitives/Button";
|
|
47
50
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
52
|
-
import {
|
|
51
|
+
import { groupInbox, projectName, threadTitle } from "./components/session-tree";
|
|
52
|
+
import { InboxList } from "./components/InboxList";
|
|
53
|
+
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
54
|
+
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
55
|
+
import { FileContentsView } from "./components/FileContentsView";
|
|
56
|
+
import { AppShell, type ProjectPanelMode } from "./components/AppShell";
|
|
57
|
+
import { EditorGrid } from "./components/EditorGrid";
|
|
58
|
+
import type { EditorTab } from "./components/editor-grid";
|
|
59
|
+
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
53
60
|
import { ThreadFooter } from "./components/ThreadFooter";
|
|
54
61
|
import { ConfirmCard } from "./components/ConfirmCard";
|
|
55
|
-
import { Breadcrumb } from "./components/Breadcrumb";
|
|
56
62
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
57
63
|
import {
|
|
64
|
+
DIFF_CHORD_ENTRIES,
|
|
58
65
|
RAIL_CHORD_ENTRIES,
|
|
59
66
|
resolveThreadChord,
|
|
60
67
|
THREAD_CHORD_ENTRIES,
|
|
61
68
|
TREE_CHORD_ENTRIES,
|
|
62
69
|
} from "./thread-chords";
|
|
63
|
-
import { DiffSheet } from "./components/DiffSheet";
|
|
70
|
+
import { DiffSheet, type DiffFoldControls, type DiffSheetProps } from "./components/DiffSheet";
|
|
71
|
+
import { commentCountsByFile, marksByRows, type DiffRow } from "./view-diff";
|
|
72
|
+
import type { DiffFileContents } from "@cueloop/schema";
|
|
64
73
|
import { PrototypeSheet } from "./components/PrototypeSheet";
|
|
65
74
|
import type { PrototypeElement } from "./prototype-browser";
|
|
66
|
-
import { type RailTab, type ReviewRailHandle } from "./components/ReviewRail";
|
|
67
|
-
import { REVIEW_DEFAULT_WIDTH, type ReviewPanelMode } from "./review-panel";
|
|
68
75
|
import {
|
|
69
|
-
buildDiffComposeState,
|
|
70
|
-
buildHeaderItems,
|
|
71
76
|
buildRenderFlags,
|
|
72
77
|
buildSubmitConfirmState,
|
|
73
78
|
computeRoleCapabilities,
|
|
@@ -82,17 +87,18 @@ import {
|
|
|
82
87
|
CompletionScreen,
|
|
83
88
|
ConnectingScreen,
|
|
84
89
|
ErrorScreen,
|
|
85
|
-
InboxScreen,
|
|
86
90
|
MenuChrome,
|
|
91
|
+
NoThreadShell,
|
|
87
92
|
TrailingOverlays,
|
|
88
93
|
} from "./app-screens";
|
|
89
|
-
|
|
90
94
|
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
91
95
|
const TOAST_DISMISS_MS = 4000;
|
|
92
96
|
|
|
93
97
|
export interface AppProps {
|
|
94
98
|
home?: string;
|
|
95
99
|
sessionId?: string;
|
|
100
|
+
/** The launch directory whose git repo backs the no-session welcome tree; defaults to process.cwd(). */
|
|
101
|
+
cwd?: string;
|
|
96
102
|
/**
|
|
97
103
|
* Observer mode (SSH-served connections): every mutating primitive is ignored and
|
|
98
104
|
* answers "observer - read-only" in the status line; navigation still works.
|
|
@@ -138,9 +144,179 @@ function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean)
|
|
|
138
144
|
return isOwner && !resolved && !observer;
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
/** The
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
/** The inline-commenting props the diff sheet shares with the thread view, wired once by the app. */
|
|
148
|
+
type DiffSurfaceProps = Pick<
|
|
149
|
+
DiffSheetProps,
|
|
150
|
+
| "session"
|
|
151
|
+
| "quickActions"
|
|
152
|
+
| "observer"
|
|
153
|
+
| "resolved"
|
|
154
|
+
| "suspended"
|
|
155
|
+
| "onComposingChange"
|
|
156
|
+
| "onObserverBlocked"
|
|
157
|
+
| "onCursorChange"
|
|
158
|
+
| "focusedAnnotationId"
|
|
159
|
+
| "onFocusAnnotation"
|
|
160
|
+
| "onAnnotate"
|
|
161
|
+
| "onReply"
|
|
162
|
+
| "onUpdateAnnotation"
|
|
163
|
+
| "onExit"
|
|
164
|
+
>;
|
|
165
|
+
|
|
166
|
+
/** The Changes tab body: the whole diff in one scroll container, or a bare hint when nothing changed. */
|
|
167
|
+
function ChangesTabBody(props: {
|
|
168
|
+
rows: DiffRow[];
|
|
169
|
+
surface: DiffSurfaceProps;
|
|
170
|
+
rejectedRows: Set<number>;
|
|
171
|
+
fold?: DiffFoldControls;
|
|
172
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
173
|
+
split?: boolean;
|
|
174
|
+
dimmed: boolean;
|
|
175
|
+
theme: Theme;
|
|
176
|
+
}): React.ReactNode {
|
|
177
|
+
const marks = useMemo(
|
|
178
|
+
() =>
|
|
179
|
+
marksByRows(props.surface.session.annotations, props.rows, props.surface.focusedAnnotationId),
|
|
180
|
+
[props.surface.session.annotations, props.rows, props.surface.focusedAnnotationId],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (props.rows.length === 0) {
|
|
184
|
+
return (
|
|
185
|
+
<box style={{ flexGrow: 1, paddingLeft: 1, paddingTop: 1 }}>
|
|
186
|
+
<text fg={props.theme.textDim}>No changes</text>
|
|
187
|
+
</box>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<DiffSheet
|
|
193
|
+
rows={props.rows}
|
|
194
|
+
marks={marks}
|
|
195
|
+
{...props.surface}
|
|
196
|
+
rejectedRows={props.rejectedRows}
|
|
197
|
+
fold={props.fold}
|
|
198
|
+
fileStats={props.fileStats}
|
|
199
|
+
split={props.split}
|
|
200
|
+
theme={props.dimmed ? dimmedTheme(props.theme) : undefined}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** An editor tab's body: the whole diff for the Changes tab, a file's diff or contents for a file tab. */
|
|
206
|
+
function GridTabContent(props: {
|
|
207
|
+
tab: EditorTab;
|
|
208
|
+
rows: DiffRow[];
|
|
209
|
+
surface: DiffSurfaceProps;
|
|
210
|
+
rejectedRows: Set<number>;
|
|
211
|
+
fold?: DiffFoldControls;
|
|
212
|
+
fileStats?: ReadonlyMap<string, { additions: number; deletions: number }>;
|
|
213
|
+
split?: boolean;
|
|
214
|
+
dimmed: boolean;
|
|
215
|
+
readFile: (path: string) => Promise<string | null>;
|
|
216
|
+
theme: Theme;
|
|
217
|
+
}): React.ReactNode {
|
|
218
|
+
const { tab } = props;
|
|
219
|
+
if (tab.kind === "file" && tab.fileView === "contents" && tab.path !== undefined) {
|
|
220
|
+
return <FileContentsView path={tab.path} loadContents={props.readFile} theme={props.theme} />;
|
|
221
|
+
}
|
|
222
|
+
if (tab.kind !== "file") {
|
|
223
|
+
return (
|
|
224
|
+
<ChangesTabBody
|
|
225
|
+
rows={props.rows}
|
|
226
|
+
surface={props.surface}
|
|
227
|
+
rejectedRows={props.rejectedRows}
|
|
228
|
+
fold={props.fold}
|
|
229
|
+
fileStats={props.fileStats}
|
|
230
|
+
split={props.split}
|
|
231
|
+
dimmed={props.dimmed}
|
|
232
|
+
theme={props.theme}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
// a single-file tab shows that file's rows alone, so its row indices are its own: comments and
|
|
237
|
+
// the caret report back in whole-diff indices, and the fold controls (a band to fold) do not apply
|
|
238
|
+
const fileRowIndices = props.rows.flatMap((row, index) => (row.file === tab.path ? [index] : []));
|
|
239
|
+
const rows = fileRowIndices.map((index) => props.rows[index]!);
|
|
240
|
+
const wholeIndex = (rowIndex: number): number => fileRowIndices[rowIndex] ?? rowIndex;
|
|
241
|
+
const rejectedRows = new Set(
|
|
242
|
+
fileRowIndices.flatMap((index, rowIndex) => (props.rejectedRows.has(index) ? [rowIndex] : [])),
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<ChangesTabBody
|
|
247
|
+
rows={rows}
|
|
248
|
+
surface={{
|
|
249
|
+
...props.surface,
|
|
250
|
+
onCursorChange: (rowIndex) => props.surface.onCursorChange?.(wholeIndex(rowIndex)),
|
|
251
|
+
onAnnotate: (span, body) =>
|
|
252
|
+
props.surface.onAnnotate(
|
|
253
|
+
{
|
|
254
|
+
start: { blockIndex: wholeIndex(span.start.blockIndex), char: span.start.char },
|
|
255
|
+
end: { blockIndex: wholeIndex(span.end.blockIndex), char: span.end.char },
|
|
256
|
+
},
|
|
257
|
+
body,
|
|
258
|
+
),
|
|
259
|
+
}}
|
|
260
|
+
rejectedRows={rejectedRows}
|
|
261
|
+
fileStats={props.fileStats}
|
|
262
|
+
split={props.split}
|
|
263
|
+
dimmed={props.dimmed}
|
|
264
|
+
theme={props.theme}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The Project pane body: the resolved repo's changed files in changes mode, its full tree otherwise. */
|
|
270
|
+
function ProjectPanelBody(props: {
|
|
271
|
+
mode: ProjectPanelMode;
|
|
272
|
+
loadChanges: () => Promise<readonly DiffFileContents[]>;
|
|
273
|
+
loadProjectFiles: () => Promise<string[]>;
|
|
274
|
+
reloadKey: string;
|
|
275
|
+
onOpenChangedFile: (path: string) => void;
|
|
276
|
+
onOpenProjectFile: (path: string) => void;
|
|
277
|
+
commentCounts?: ReadonlyMap<string, number>;
|
|
278
|
+
theme: Theme;
|
|
279
|
+
}): React.ReactNode {
|
|
280
|
+
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
281
|
+
const loadRef = useRef(props.loadChanges);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
loadRef.current = props.loadChanges;
|
|
284
|
+
});
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
let alive = true;
|
|
287
|
+
|
|
288
|
+
void loadRef.current().then(
|
|
289
|
+
(files) => {
|
|
290
|
+
if (alive) setChanges(files);
|
|
291
|
+
},
|
|
292
|
+
() => {
|
|
293
|
+
if (alive) setChanges([]);
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
return () => {
|
|
298
|
+
alive = false;
|
|
299
|
+
};
|
|
300
|
+
}, [props.reloadKey, props.mode]);
|
|
301
|
+
|
|
302
|
+
if (props.mode === "changes") {
|
|
303
|
+
return (
|
|
304
|
+
<ChangesFileTree
|
|
305
|
+
files={changes}
|
|
306
|
+
onSelectFile={props.onOpenChangedFile}
|
|
307
|
+
commentCounts={props.commentCounts}
|
|
308
|
+
theme={props.theme}
|
|
309
|
+
/>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return (
|
|
313
|
+
<ProjectTreeView
|
|
314
|
+
key={props.reloadKey}
|
|
315
|
+
loadFiles={props.loadProjectFiles}
|
|
316
|
+
onSelectFile={props.onOpenProjectFile}
|
|
317
|
+
theme={props.theme}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
144
320
|
}
|
|
145
321
|
|
|
146
322
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
@@ -154,6 +330,7 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
154
330
|
return [
|
|
155
331
|
...THREAD_VIEW_CHEATSHEET,
|
|
156
332
|
{ title: "Session", entries: [...THREAD_CHORD_ENTRIES] },
|
|
333
|
+
{ title: "Diff", entries: [...DIFF_CHORD_ENTRIES] },
|
|
157
334
|
{ title: "Rail", entries: [...RAIL_CHORD_ENTRIES] },
|
|
158
335
|
{ title: "Tree", entries: [...TREE_CHORD_ENTRIES] },
|
|
159
336
|
...base.filter((section) => section.title === "Agent terminal"),
|
|
@@ -163,6 +340,7 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
163
340
|
export function App({
|
|
164
341
|
home,
|
|
165
342
|
sessionId,
|
|
343
|
+
cwd,
|
|
166
344
|
readOnly = false,
|
|
167
345
|
onExit,
|
|
168
346
|
clock,
|
|
@@ -178,6 +356,7 @@ export function App({
|
|
|
178
356
|
createReviewController({
|
|
179
357
|
home,
|
|
180
358
|
sessionId,
|
|
359
|
+
cwd,
|
|
181
360
|
readOnly: observer,
|
|
182
361
|
onExit,
|
|
183
362
|
clock,
|
|
@@ -217,32 +396,46 @@ export function App({
|
|
|
217
396
|
const [cursor, setCursor] = useState(0);
|
|
218
397
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
219
398
|
// Projects and Threads grouping; ordered is the flat sequence the inbox cursor walks
|
|
220
|
-
|
|
399
|
+
// pinned threads are client-local view state, seeded from the user config and
|
|
400
|
+
// persisted on toggle; a pin lifts the thread into the sidebar's Pinned section
|
|
401
|
+
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set());
|
|
402
|
+
const grouped = useMemo(() => groupInbox(inbox ?? [], pinnedIds), [inbox, pinnedIds]);
|
|
403
|
+
const togglePin = (id: string): void =>
|
|
404
|
+
setPinnedIds((current) => {
|
|
405
|
+
const next = new Set(current);
|
|
406
|
+
|
|
407
|
+
if (next.has(id)) next.delete(id);
|
|
408
|
+
else next.add(id);
|
|
409
|
+
persistPins([...next]);
|
|
410
|
+
|
|
411
|
+
return next;
|
|
412
|
+
});
|
|
221
413
|
// the left Projects and Threads column; collapsed by default per the context
|
|
222
414
|
// matrix (the thread owns the width), toggled open to jump between threads
|
|
223
|
-
|
|
415
|
+
// one sidebar-open state across the no-thread shell and the thread view, so
|
|
416
|
+
// picking a thread keeps the sidebar as it was. It opens when the app lands with
|
|
417
|
+
// nothing selected (pick a thread) and stays collapsed on a direct thread open,
|
|
418
|
+
// where the thread owns the width.
|
|
419
|
+
const [sidebarOpen, setSidebarOpen] = useState(sessionId === undefined);
|
|
420
|
+
// the Changes + Project right region and its editor grid (tabs, splits, zoom)
|
|
421
|
+
const workbench = useChangesWorkbench();
|
|
224
422
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
225
423
|
// the top-left settings gear drop-down and the centered dialog it opens
|
|
226
424
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
227
425
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
426
|
+
// unified or side-by-side diff; split only lays out when the Changes pane is zoomed
|
|
427
|
+
const [diffView, setDiffView] = useState<DiffViewMode>("unified");
|
|
228
428
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
229
429
|
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
230
430
|
const [railTab, setRailTab] = useState<RailTab>("review");
|
|
231
|
-
// the tree row the reviewer stands on in the
|
|
431
|
+
// the tree row the reviewer stands on in the session tree
|
|
232
432
|
const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>(undefined);
|
|
233
|
-
//
|
|
234
|
-
// from and persisted to the user config so they survive a restart. The ref
|
|
235
|
-
// mirrors the width so the drag-end persist reads the latest value.
|
|
236
|
-
const [reviewMode, setReviewMode] = useState<ReviewPanelMode>("expanded");
|
|
237
|
-
const [reviewWidth, setReviewWidth] = useState(REVIEW_DEFAULT_WIDTH);
|
|
238
|
-
const reviewWidthRef = useRef(REVIEW_DEFAULT_WIDTH);
|
|
239
|
-
// ~2s focus pulse on the document highlight when a rail card is activated
|
|
433
|
+
// ~2s focus pulse on the document highlight when a card is activated
|
|
240
434
|
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
241
435
|
const pulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
242
436
|
// live mirror of overlay input text: refs commit synchronously, so the
|
|
243
437
|
// RETURN handler never reads a stale value mid-typing
|
|
244
438
|
const liveInput = useRef("");
|
|
245
|
-
const railRef = useRef<ReviewRailHandle | null>(null);
|
|
246
439
|
// keymap from layered config; the loaded theme swaps the provider value
|
|
247
440
|
const keysRef = useRef(DEFAULT_KEYS);
|
|
248
441
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
@@ -260,12 +453,11 @@ export function App({
|
|
|
260
453
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
261
454
|
setThemeName(config.ui.theme);
|
|
262
455
|
setThemeOverrides(config.themeOverrides);
|
|
263
|
-
setReviewMode(config.ui.reviewState);
|
|
264
|
-
setReviewWidth(config.ui.reviewWidth);
|
|
265
|
-
reviewWidthRef.current = config.ui.reviewWidth;
|
|
266
456
|
setAuthorNames(config.authors);
|
|
267
457
|
setQuickActions(config.actions);
|
|
268
458
|
setAutoClose(config.ui.autoClose);
|
|
459
|
+
setDiffView(config.ui.diffView);
|
|
460
|
+
setPinnedIds(new Set(config.ui.pins));
|
|
269
461
|
controller.applyConfig(config);
|
|
270
462
|
}, [session?.workspace.repoRoot, controller, keyBindings, appearance]);
|
|
271
463
|
useEffect(
|
|
@@ -307,22 +499,20 @@ export function App({
|
|
|
307
499
|
appearance,
|
|
308
500
|
autoClose,
|
|
309
501
|
setAutoClose,
|
|
310
|
-
|
|
311
|
-
|
|
502
|
+
diffView,
|
|
503
|
+
setDiffView,
|
|
312
504
|
themeName,
|
|
313
505
|
setThemeName,
|
|
314
506
|
themeOverrides,
|
|
315
507
|
setTheme,
|
|
316
508
|
quickActions,
|
|
317
509
|
setQuickActions,
|
|
318
|
-
controller,
|
|
319
510
|
setMenuDialog,
|
|
320
511
|
});
|
|
321
512
|
|
|
322
513
|
// ── derived view model ──────────────────────
|
|
323
514
|
const display = controller.display();
|
|
324
515
|
const rows = controller.rows();
|
|
325
|
-
const diffColumns = useDiffColumns({ session, rows, cursor, setCursor });
|
|
326
516
|
const rejectedRows = controller.rejectedRows();
|
|
327
517
|
const marks = useMemo(
|
|
328
518
|
() =>
|
|
@@ -342,8 +532,16 @@ export function App({
|
|
|
342
532
|
return ids;
|
|
343
533
|
}, [marks]);
|
|
344
534
|
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
345
|
-
//
|
|
346
|
-
const
|
|
535
|
+
// comments per changed-file path, for the changed-files tree and tab badges
|
|
536
|
+
const diffCommentCounts = useMemo(
|
|
537
|
+
() => (session && isDiff ? commentCountsByFile(session, rows) : undefined),
|
|
538
|
+
[session, isDiff, rows],
|
|
539
|
+
);
|
|
540
|
+
// entering a diff opens the right region in changed-files mode; a plan or reply opens it closed
|
|
541
|
+
workbench.syncSession(session?.id, isDiff);
|
|
542
|
+
// plans and replies open in the thread view, diffs in the diff sheet: both drive the shared
|
|
543
|
+
// annotation surface and own the document grammar; only the prototype keeps the keymap
|
|
544
|
+
const threadViewActive = session !== null && !isPrototype;
|
|
347
545
|
const [threadComposing, setThreadComposing] = useState(false);
|
|
348
546
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
349
547
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
@@ -357,7 +555,6 @@ export function App({
|
|
|
357
555
|
// ── selection symmetry: one selected id, both sides ──
|
|
358
556
|
const selectCardFromDocument = (annotationId: string): void => {
|
|
359
557
|
setFocusedAnnotationId(annotationId);
|
|
360
|
-
railRef.current?.revealCard(annotationId);
|
|
361
558
|
};
|
|
362
559
|
|
|
363
560
|
const openCardEdit = (annotationId: string): void => {
|
|
@@ -403,9 +600,6 @@ export function App({
|
|
|
403
600
|
inboxCursor,
|
|
404
601
|
mode,
|
|
405
602
|
session,
|
|
406
|
-
reviewMode,
|
|
407
|
-
reviewWidth,
|
|
408
|
-
terminalWidth,
|
|
409
603
|
focusedAnnotationId,
|
|
410
604
|
selectedCurationId,
|
|
411
605
|
railTab,
|
|
@@ -416,13 +610,11 @@ export function App({
|
|
|
416
610
|
persistAuthorName(id, name);
|
|
417
611
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
418
612
|
},
|
|
613
|
+
renameThread: (id: string, title: string) => controller.renameSession(id, title),
|
|
419
614
|
liveInput,
|
|
420
|
-
reviewWidthRef,
|
|
421
615
|
setCursor,
|
|
422
616
|
setInboxCursor,
|
|
423
617
|
setMode,
|
|
424
|
-
setReviewMode,
|
|
425
|
-
setReviewWidth,
|
|
426
618
|
setRailTab,
|
|
427
619
|
setSelectedEntryId,
|
|
428
620
|
setFocusedAnnotationId,
|
|
@@ -431,6 +623,14 @@ export function App({
|
|
|
431
623
|
selectCardFromDocument,
|
|
432
624
|
runEditorHandOff,
|
|
433
625
|
openCardEdit,
|
|
626
|
+
toggleDiffView: () => {
|
|
627
|
+
const next: DiffViewMode = diffView === "unified" ? "split" : "unified";
|
|
628
|
+
|
|
629
|
+
setDiffView(next);
|
|
630
|
+
persistDiffView(next);
|
|
631
|
+
if (next === "split" && !workbench.zoomed)
|
|
632
|
+
controller.setStatus("split diff shows when zoomed");
|
|
633
|
+
},
|
|
434
634
|
});
|
|
435
635
|
|
|
436
636
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
@@ -451,6 +651,7 @@ export function App({
|
|
|
451
651
|
isOwner,
|
|
452
652
|
resolved,
|
|
453
653
|
treeActive: railTab === "tree",
|
|
654
|
+
isDiff,
|
|
454
655
|
});
|
|
455
656
|
|
|
456
657
|
if (chord) dispatch(chord);
|
|
@@ -520,7 +721,7 @@ export function App({
|
|
|
520
721
|
if (error) return <ErrorScreen error={error} theme={theme} />;
|
|
521
722
|
if (!session)
|
|
522
723
|
return inbox ? (
|
|
523
|
-
<
|
|
724
|
+
<NoThreadShell
|
|
524
725
|
rows={grouped.rows}
|
|
525
726
|
inboxCursor={inboxCursor}
|
|
526
727
|
mode={mode}
|
|
@@ -529,6 +730,11 @@ export function App({
|
|
|
529
730
|
setMode={setMode}
|
|
530
731
|
menuChrome={menuChrome}
|
|
531
732
|
onOpenMenu={() => setMenuDialog("settings")}
|
|
733
|
+
sidebarOpen={sidebarOpen}
|
|
734
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
735
|
+
pinnedIds={pinnedIds}
|
|
736
|
+
onPin={togglePin}
|
|
737
|
+
onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
|
|
532
738
|
/>
|
|
533
739
|
) : (
|
|
534
740
|
<ConnectingScreen theme={theme} />
|
|
@@ -547,14 +753,15 @@ export function App({
|
|
|
547
753
|
/>
|
|
548
754
|
);
|
|
549
755
|
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
756
|
+
const diffFold: DiffFoldControls = {
|
|
757
|
+
isCollapsed: (file) => controller.isFileCollapsed(file),
|
|
758
|
+
isExpanded: (file) => controller.isFileExpanded(file),
|
|
759
|
+
canExpand: (file) => controller.canExpandFile(file),
|
|
760
|
+
onToggleCollapse: (file) =>
|
|
761
|
+
controller.setFileCollapsed(file, !controller.isFileCollapsed(file)),
|
|
762
|
+
onToggleExpand: (file) => controller.setFileExpanded(file, !controller.isFileExpanded(file)),
|
|
763
|
+
onCopyPath: (file) => controller.copyFilePath(file),
|
|
764
|
+
};
|
|
558
765
|
const submitConfirmState = buildSubmitConfirmState({
|
|
559
766
|
mode,
|
|
560
767
|
isDiff,
|
|
@@ -565,12 +772,6 @@ export function App({
|
|
|
565
772
|
setMode,
|
|
566
773
|
dispatch,
|
|
567
774
|
});
|
|
568
|
-
const headerItems = buildHeaderItems({
|
|
569
|
-
session: activeSession,
|
|
570
|
-
resolved,
|
|
571
|
-
observer,
|
|
572
|
-
role,
|
|
573
|
-
});
|
|
574
775
|
const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath } = buildRenderFlags({
|
|
575
776
|
session: activeSession,
|
|
576
777
|
isOwner,
|
|
@@ -605,61 +806,41 @@ export function App({
|
|
|
605
806
|
|
|
606
807
|
return (
|
|
607
808
|
<ThemeProvider theme={theme}>
|
|
608
|
-
<
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
<box onMouseUp={() => setMenuDialog("settings")} style={{ paddingRight: 2 }}>
|
|
626
|
-
<text fg={theme.textMuted}>{NERD.settings}</text>
|
|
627
|
-
</box>
|
|
628
|
-
<IconButton
|
|
629
|
-
glyph={sidebarToggleGlyph(sidebarOpen)}
|
|
630
|
-
onPress={() => setSidebarOpen((open) => !open)}
|
|
631
|
-
marginRight={2}
|
|
632
|
-
theme={theme}
|
|
633
|
-
/>
|
|
634
|
-
<Breadcrumb items={headerItems} />
|
|
635
|
-
<box style={{ flexGrow: 1 }} />
|
|
636
|
-
{showOwnerActions ? (
|
|
637
|
-
<Toolbar>
|
|
638
|
-
<Button onPress={onEditRequest} theme={theme}>
|
|
639
|
-
{" Edit "}
|
|
640
|
-
</Button>
|
|
641
|
-
<Button onPress={onShareRequest} theme={theme}>
|
|
642
|
-
{" Share "}
|
|
643
|
-
</Button>
|
|
644
|
-
</Toolbar>
|
|
645
|
-
) : null}
|
|
646
|
-
<DiffChangesToggle
|
|
647
|
-
isDiff={isDiff}
|
|
648
|
-
open={diffColumns.changesOpen}
|
|
649
|
-
onToggle={diffColumns.toggleChanges}
|
|
809
|
+
<AppShell
|
|
810
|
+
sidebarOpen={sidebarOpen}
|
|
811
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
812
|
+
onOpenMenu={() => setMenuDialog("settings")}
|
|
813
|
+
threadsPanel={
|
|
814
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
815
|
+
<InboxList
|
|
816
|
+
rows={grouped.rows}
|
|
817
|
+
cursor={inboxCursor}
|
|
818
|
+
activeId={activeSession.id}
|
|
819
|
+
pinnedIds={pinnedIds}
|
|
820
|
+
width={30}
|
|
821
|
+
onSelect={(id) => controller.open(id)}
|
|
822
|
+
onPin={togglePin}
|
|
823
|
+
onRename={(id, title) =>
|
|
824
|
+
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
825
|
+
}
|
|
650
826
|
theme={theme}
|
|
651
827
|
/>
|
|
652
|
-
</
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
828
|
+
</scrollbox>
|
|
829
|
+
}
|
|
830
|
+
threadTitle={threadTitle(activeSession)}
|
|
831
|
+
threadActions={
|
|
832
|
+
showOwnerActions ? (
|
|
833
|
+
<Toolbar>
|
|
834
|
+
<Button onPress={onEditRequest} theme={theme}>
|
|
835
|
+
{" edit "}
|
|
836
|
+
</Button>
|
|
837
|
+
<Button onPress={onShareRequest} theme={theme}>
|
|
838
|
+
{" share "}
|
|
839
|
+
</Button>
|
|
840
|
+
</Toolbar>
|
|
841
|
+
) : undefined
|
|
842
|
+
}
|
|
843
|
+
threadPanel={
|
|
663
844
|
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
664
845
|
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
665
846
|
{isPrototype ? (
|
|
@@ -672,16 +853,9 @@ export function App({
|
|
|
672
853
|
hidden={chromeHidden}
|
|
673
854
|
/>
|
|
674
855
|
) : isDiff ? (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
cursor={cursor}
|
|
679
|
-
annotations={activeSession.annotations}
|
|
680
|
-
focusedAnnotationId={focusedAnnotationId}
|
|
681
|
-
rejectedRows={rejectedRows}
|
|
682
|
-
compose={diffComposeState}
|
|
683
|
-
theme={walking ? dimmedTheme(theme) : undefined}
|
|
684
|
-
/>
|
|
856
|
+
<box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
|
|
857
|
+
<text fg={theme.textDim}>review the changes on the right</text>
|
|
858
|
+
</box>
|
|
685
859
|
) : (
|
|
686
860
|
<ThreadView
|
|
687
861
|
session={activeSession}
|
|
@@ -729,14 +903,86 @@ export function App({
|
|
|
729
903
|
theme={theme}
|
|
730
904
|
/>
|
|
731
905
|
</box>
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
906
|
+
}
|
|
907
|
+
changesOpen={workbench.changesOpen}
|
|
908
|
+
projectOpen={workbench.projectOpen}
|
|
909
|
+
onToggleChanges={workbench.toggleChanges}
|
|
910
|
+
onToggleProject={workbench.toggleProject}
|
|
911
|
+
onToggleRight={workbench.toggleRight}
|
|
912
|
+
projectMode={workbench.projectMode}
|
|
913
|
+
zoomHideThread={workbench.zoomed}
|
|
914
|
+
changesPanel={
|
|
915
|
+
<EditorGrid
|
|
916
|
+
tree={workbench.grid}
|
|
917
|
+
focusedGroupId={workbench.activeGroup}
|
|
918
|
+
commentCounts={diffCommentCounts}
|
|
919
|
+
onFocusGroup={workbench.focusGroup}
|
|
920
|
+
onActivateTab={workbench.activate}
|
|
921
|
+
onCloseTab={workbench.close}
|
|
922
|
+
onSplit={workbench.split}
|
|
923
|
+
onZoom={workbench.toggleZoom}
|
|
924
|
+
zoomed={workbench.zoomed}
|
|
925
|
+
renderTab={(tab, groupFocused) => (
|
|
926
|
+
<GridTabContent
|
|
927
|
+
tab={tab}
|
|
928
|
+
rows={rows}
|
|
929
|
+
surface={{
|
|
930
|
+
session: activeSession,
|
|
931
|
+
quickActions,
|
|
932
|
+
observer,
|
|
933
|
+
resolved,
|
|
934
|
+
suspended: threadViewSuspended || !groupFocused,
|
|
935
|
+
onComposingChange: setThreadComposing,
|
|
936
|
+
onObserverBlocked: (reason) =>
|
|
937
|
+
controller.setStatus(
|
|
938
|
+
reason === "observer"
|
|
939
|
+
? "observer - read-only"
|
|
940
|
+
: "review submitted - read-only",
|
|
941
|
+
),
|
|
942
|
+
onCursorChange: setCursor,
|
|
943
|
+
focusedAnnotationId,
|
|
944
|
+
onFocusAnnotation: setFocusedAnnotationId,
|
|
945
|
+
onAnnotate: (span, body) =>
|
|
946
|
+
void controller.annotate(
|
|
947
|
+
"comment",
|
|
948
|
+
span.start.blockIndex,
|
|
949
|
+
span.start.char,
|
|
950
|
+
span.end.char,
|
|
951
|
+
body,
|
|
952
|
+
span.end.blockIndex,
|
|
953
|
+
),
|
|
954
|
+
onReply: (rootAnnotationId, body) =>
|
|
955
|
+
void controller.reply(rootAnnotationId, body),
|
|
956
|
+
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
957
|
+
onExit: () => onExit?.(0),
|
|
958
|
+
}}
|
|
959
|
+
rejectedRows={rejectedRows}
|
|
960
|
+
fold={diffFold}
|
|
961
|
+
fileStats={controller.fileStats()}
|
|
962
|
+
split={diffView === "split" && workbench.zoomed}
|
|
963
|
+
dimmed={walking}
|
|
964
|
+
readFile={(path) => controller.repoReadFile(path)}
|
|
965
|
+
theme={theme}
|
|
966
|
+
/>
|
|
967
|
+
)}
|
|
737
968
|
theme={theme}
|
|
738
969
|
/>
|
|
739
|
-
|
|
970
|
+
}
|
|
971
|
+
projectPanel={
|
|
972
|
+
<ProjectPanelBody
|
|
973
|
+
mode={workbench.projectMode}
|
|
974
|
+
loadChanges={() => controller.repoChanges()}
|
|
975
|
+
loadProjectFiles={() => controller.repoFiles()}
|
|
976
|
+
reloadKey={activeSession.id}
|
|
977
|
+
// a diff review opens a changed file as its captured diff; other threads show live contents
|
|
978
|
+
onOpenChangedFile={(path) => workbench.openFile(path, isDiff ? "diff" : "contents")}
|
|
979
|
+
onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
|
|
980
|
+
commentCounts={diffCommentCounts}
|
|
981
|
+
theme={theme}
|
|
982
|
+
/>
|
|
983
|
+
}
|
|
984
|
+
theme={theme}
|
|
985
|
+
>
|
|
740
986
|
<TrailingOverlays
|
|
741
987
|
walking={walking}
|
|
742
988
|
walk={walk}
|
|
@@ -766,7 +1012,7 @@ export function App({
|
|
|
766
1012
|
</box>
|
|
767
1013
|
) : null}
|
|
768
1014
|
{menuChrome}
|
|
769
|
-
</
|
|
1015
|
+
</AppShell>
|
|
770
1016
|
</ThemeProvider>
|
|
771
1017
|
);
|
|
772
1018
|
}
|