@cueloop/client 0.1.0-alpha.65 → 0.1.0-alpha.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -5
- package/src/App.autoclose.test.tsx +2 -2
- package/src/App.diff.test.tsx +79 -112
- package/src/App.plan-review.test.tsx +23 -205
- package/src/App.prototype.test.tsx +1 -8
- package/src/App.readonly.test.tsx +9 -27
- package/src/App.submit-confirm.test.tsx +19 -72
- package/src/App.test.tsx +136 -31
- package/src/App.tsx +491 -322
- package/src/App.walk.test.tsx +15 -16
- package/src/App.workbench.test.tsx +390 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/annotation-palette.ts +61 -0
- package/src/app-screens.tsx +197 -77
- package/src/app-view-model.ts +9 -69
- package/src/components/AnnotationCard.stories.tsx +1 -1
- package/src/components/AnnotationCard.tsx +1 -1
- package/src/components/AnnotationCards.stories.tsx +87 -0
- package/src/components/AnnotationCards.tsx +303 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +239 -0
- package/src/components/AppShell.zoom.test.tsx +94 -0
- package/src/components/Breadcrumb.stories.tsx +1 -1
- package/src/components/ChangesColumn.stories.tsx +39 -0
- package/src/components/ChangesColumn.tsx +170 -0
- package/src/components/CodeBlock.stories.tsx +1 -1
- package/src/components/CompletionOverlay.stories.tsx +1 -1
- package/src/components/ConfirmCard.stories.tsx +1 -1
- package/src/components/ConfirmCard.tsx +3 -4
- package/src/components/ConfirmDialog.stories.tsx +1 -1
- package/src/components/DiffSheet.scroll.test.tsx +38 -55
- package/src/components/DiffSheet.split.test.tsx +140 -0
- package/src/components/DiffSheet.stories.tsx +56 -54
- package/src/components/DiffSheet.syntax.test.tsx +24 -7
- package/src/components/DiffSheet.tsx +607 -276
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +181 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +105 -0
- package/src/components/EditorGrid.tsx +338 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +78 -0
- package/src/components/InboxList.stories.tsx +23 -8
- package/src/components/InboxList.tsx +188 -33
- package/src/components/KeybindsDialog.stories.tsx +1 -1
- package/src/components/MarkerPopover.stories.tsx +1 -1
- package/src/components/MenuBar.stories.tsx +3 -12
- package/src/components/MenuBar.tsx +25 -56
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +95 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +32 -0
- package/src/components/ProjectTreeView.tsx +82 -0
- package/src/components/PromptDialog.stories.tsx +1 -1
- package/src/components/PromptDialog.tsx +8 -1
- package/src/components/PrototypeSheet.stories.tsx +1 -1
- package/src/components/RootOverlay.stories.tsx +44 -0
- package/src/components/RootOverlay.tsx +35 -0
- package/src/components/SettingsDialog.stories.tsx +7 -1
- package/src/components/SettingsDialog.tsx +114 -67
- package/src/components/SettingsRows.stories.tsx +1 -1
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SurfaceScrollbar.stories.tsx +34 -0
- package/src/components/SurfaceScrollbar.test.tsx +111 -0
- package/src/components/SurfaceScrollbar.tsx +93 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +31 -0
- package/src/components/ThreadFooter.tsx +54 -0
- package/src/components/ThreadView.slash.test.tsx +113 -0
- package/src/components/ThreadView.stories.tsx +1 -1
- package/src/components/ThreadView.tsx +62 -1352
- package/src/components/ThreadsSidebar.stories.tsx +36 -0
- package/src/components/ThreadsSidebar.tsx +70 -0
- package/src/components/Toast.stories.tsx +1 -1
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +1 -1
- package/src/components/WalkWizard.stories.tsx +1 -1
- package/src/components/WelcomeSurface.stories.tsx +12 -0
- package/src/components/WelcomeSurface.tsx +52 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +732 -572
- package/src/components/agent-launcher.stories.tsx +1 -1
- package/src/components/diff-sheet-layout.test.ts +125 -55
- package/src/components/diff-sheet-layout.ts +3 -103
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/fade-title.test.ts +49 -0
- package/src/components/fade-title.ts +61 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +1 -1
- package/src/components/primitives/Card.stories.tsx +1 -1
- package/src/components/primitives/Dialog.stories.tsx +1 -1
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +1 -1
- package/src/components/primitives/Tabs.stories.tsx +1 -1
- package/src/components/primitives/Toolbar.stories.tsx +1 -1
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +96 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +1 -1
- package/src/components/session-tree.test.ts +134 -0
- package/src/components/session-tree.ts +108 -0
- package/src/components/stories-app.tsx +142 -82
- package/src/components/story-tree.test.ts +100 -0
- package/src/components/story.ts +55 -0
- package/src/components/theme-context.stories.tsx +1 -1
- package/src/config.test.ts +20 -49
- package/src/config.ts +26 -28
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-syntax.test.ts +16 -0
- package/src/diff-syntax.ts +6 -3
- package/src/discussions.ts +79 -0
- package/src/intent-dispatch.test.ts +31 -11
- package/src/intent-dispatch.ts +93 -48
- package/src/key-bindings.test.ts +1 -1
- package/src/keymap.test.ts +3 -25
- package/src/keymap.ts +17 -18
- package/src/mark-runs.ts +120 -0
- package/src/pty.ts +10 -2
- package/src/run.ts +17 -7
- package/src/serve.test.ts +4 -8
- package/src/session-controller.curate.test.ts +75 -0
- package/src/session-controller.share.test.ts +1 -0
- package/src/session-controller.tree.test.ts +26 -0
- package/src/session-controller.ts +160 -31
- package/src/slash-palette.ts +83 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +3 -3
- package/src/test-support.ts +39 -8
- package/src/theme.ts +4 -4
- package/src/thread-chords.test.ts +25 -9
- package/src/thread-chords.ts +33 -8
- package/src/use-annotation-surface.tsx +837 -0
- package/src/use-changes-workbench.ts +151 -0
- package/src/use-frame-measure.ts +38 -0
- package/src/use-settings-dialog.tsx +23 -24
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/view-diff.ts +114 -0
- package/src/App.review-panel.test.tsx +0 -140
- package/src/App.tree.test.tsx +0 -224
- package/src/components/ReviewPanel.stories.tsx +0 -100
- package/src/components/ReviewPanel.tsx +0 -103
- package/src/components/ReviewRail.stories.tsx +0 -232
- package/src/components/ReviewRail.tsx +0 -327
- package/src/review-panel.test.ts +0 -113
- package/src/review-panel.ts +0 -83
package/src/App.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import React, {
|
|
|
18
18
|
useSyncExternalStore,
|
|
19
19
|
} from "react";
|
|
20
20
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
21
|
-
import type { Clock
|
|
21
|
+
import type { Clock } from "@opentui/core";
|
|
22
22
|
import { marksByDisplay, type Mark } from "./view-plan";
|
|
23
23
|
import { dimmedTheme } from "./theme";
|
|
24
24
|
import {
|
|
@@ -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,42 +42,39 @@ 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 {
|
|
43
|
-
import { createIntentDispatch, type Mode } from "./intent-dispatch";
|
|
45
|
+
import { createIntentDispatch, type Mode, type RailTab } from "./intent-dispatch";
|
|
44
46
|
import { reduceKey, type KeyState } from "./keymap";
|
|
45
47
|
import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
46
48
|
import { ThemeProvider } from "./components/theme-context";
|
|
47
49
|
import { Button } from "./components/primitives/Button";
|
|
48
50
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
49
|
-
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";
|
|
60
|
+
import { ThreadFooter } from "./components/ThreadFooter";
|
|
61
|
+
import { ConfirmCard } from "./components/ConfirmCard";
|
|
50
62
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
51
63
|
import {
|
|
64
|
+
DIFF_CHORD_ENTRIES,
|
|
52
65
|
RAIL_CHORD_ENTRIES,
|
|
53
66
|
resolveThreadChord,
|
|
54
67
|
THREAD_CHORD_ENTRIES,
|
|
55
68
|
TREE_CHORD_ENTRIES,
|
|
56
69
|
} from "./thread-chords";
|
|
57
|
-
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";
|
|
58
73
|
import { PrototypeSheet } from "./components/PrototypeSheet";
|
|
59
74
|
import type { PrototypeElement } from "./prototype-browser";
|
|
60
|
-
import { type RailTab, type ReviewRailHandle } from "./components/ReviewRail";
|
|
61
|
-
import type { AgentTerminalHandle } from "./components/agent-launcher";
|
|
62
|
-
import { ReviewPanel } from "./components/ReviewPanel";
|
|
63
75
|
import {
|
|
64
|
-
REVIEW_DEFAULT_WIDTH,
|
|
65
|
-
resolveReviewWidth,
|
|
66
|
-
toggleReviewPanelMode,
|
|
67
|
-
widthFromMouseColumn,
|
|
68
|
-
type ReviewPanelMode,
|
|
69
|
-
} from "./review-panel";
|
|
70
|
-
import {
|
|
71
|
-
buildCardEditState,
|
|
72
|
-
buildDiffComposeState,
|
|
73
|
-
buildHeaderItems,
|
|
74
76
|
buildRenderFlags,
|
|
75
77
|
buildSubmitConfirmState,
|
|
76
|
-
computePendingCount,
|
|
77
|
-
computeRailFootprint,
|
|
78
78
|
computeRoleCapabilities,
|
|
79
79
|
deriveReviewFlags,
|
|
80
80
|
isCompletionOverlayPhase,
|
|
@@ -87,17 +87,18 @@ import {
|
|
|
87
87
|
CompletionScreen,
|
|
88
88
|
ConnectingScreen,
|
|
89
89
|
ErrorScreen,
|
|
90
|
-
InboxScreen,
|
|
91
90
|
MenuChrome,
|
|
91
|
+
NoThreadShell,
|
|
92
92
|
TrailingOverlays,
|
|
93
93
|
} from "./app-screens";
|
|
94
|
-
|
|
95
94
|
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
96
95
|
const TOAST_DISMISS_MS = 4000;
|
|
97
96
|
|
|
98
97
|
export interface AppProps {
|
|
99
98
|
home?: string;
|
|
100
99
|
sessionId?: string;
|
|
100
|
+
/** The launch directory whose git repo backs the no-session welcome tree; defaults to process.cwd(). */
|
|
101
|
+
cwd?: string;
|
|
101
102
|
/**
|
|
102
103
|
* Observer mode (SSH-served connections): every mutating primitive is ignored and
|
|
103
104
|
* answers "observer - read-only" in the status line; navigation still works.
|
|
@@ -129,17 +130,193 @@ export interface AppProps {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/** True while the drop-up or one of its dialogs is open and owns the keyboard. */
|
|
132
|
-
function menuChromeOpen(
|
|
133
|
-
return
|
|
133
|
+
function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
134
|
+
return menuDialog !== null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** True while a menu or an overlay owns the keyboard instead of the thread view. */
|
|
138
|
+
function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["overlay"]): boolean {
|
|
139
|
+
return menuOwnsKeyboard || overlay !== "none";
|
|
134
140
|
}
|
|
135
141
|
|
|
136
|
-
/**
|
|
137
|
-
function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
143
|
+
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
144
|
+
return isOwner && !resolved && !observer;
|
|
145
|
+
}
|
|
146
|
+
|
|
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
|
+
);
|
|
143
320
|
}
|
|
144
321
|
|
|
145
322
|
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
@@ -153,6 +330,7 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
153
330
|
return [
|
|
154
331
|
...THREAD_VIEW_CHEATSHEET,
|
|
155
332
|
{ title: "Session", entries: [...THREAD_CHORD_ENTRIES] },
|
|
333
|
+
{ title: "Diff", entries: [...DIFF_CHORD_ENTRIES] },
|
|
156
334
|
{ title: "Rail", entries: [...RAIL_CHORD_ENTRIES] },
|
|
157
335
|
{ title: "Tree", entries: [...TREE_CHORD_ENTRIES] },
|
|
158
336
|
...base.filter((section) => section.title === "Agent terminal"),
|
|
@@ -162,6 +340,7 @@ function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): Che
|
|
|
162
340
|
export function App({
|
|
163
341
|
home,
|
|
164
342
|
sessionId,
|
|
343
|
+
cwd,
|
|
165
344
|
readOnly = false,
|
|
166
345
|
onExit,
|
|
167
346
|
clock,
|
|
@@ -177,6 +356,7 @@ export function App({
|
|
|
177
356
|
createReviewController({
|
|
178
357
|
home,
|
|
179
358
|
sessionId,
|
|
359
|
+
cwd,
|
|
180
360
|
readOnly: observer,
|
|
181
361
|
onExit,
|
|
182
362
|
clock,
|
|
@@ -215,32 +395,47 @@ export function App({
|
|
|
215
395
|
// ── view state ──────────────────────────────
|
|
216
396
|
const [cursor, setCursor] = useState(0);
|
|
217
397
|
const [inboxCursor, setInboxCursor] = useState(0);
|
|
398
|
+
// Projects and Threads grouping; ordered is the flat sequence the inbox cursor walks
|
|
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
|
+
});
|
|
413
|
+
// the left Projects and Threads column; collapsed by default per the context
|
|
414
|
+
// matrix (the thread owns the width), toggled open to jump between threads
|
|
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();
|
|
218
422
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
219
|
-
// the
|
|
220
|
-
const [menuOpen, setMenuOpen] = useState(false);
|
|
423
|
+
// the top-left settings gear drop-down and the centered dialog it opens
|
|
221
424
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
222
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");
|
|
223
428
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
224
429
|
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
225
430
|
const [railTab, setRailTab] = useState<RailTab>("review");
|
|
226
|
-
// the tree row the reviewer stands on in the
|
|
431
|
+
// the tree row the reviewer stands on in the session tree
|
|
227
432
|
const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>(undefined);
|
|
228
|
-
//
|
|
229
|
-
const [agentTerminal, setAgentTerminal] = useState<AgentTerminalHandle | null>(null);
|
|
230
|
-
// review panel layout: mode + expanded width are client view state, loaded
|
|
231
|
-
// from and persisted to the user config so they survive a restart. The ref
|
|
232
|
-
// mirrors the width so the drag-end persist reads the latest value.
|
|
233
|
-
const [reviewMode, setReviewMode] = useState<ReviewPanelMode>("expanded");
|
|
234
|
-
const [reviewWidth, setReviewWidth] = useState(REVIEW_DEFAULT_WIDTH);
|
|
235
|
-
const [dividerDragging, setDividerDragging] = useState(false);
|
|
236
|
-
const reviewWidthRef = useRef(REVIEW_DEFAULT_WIDTH);
|
|
237
|
-
// ~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
|
|
238
434
|
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
239
435
|
const pulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
240
436
|
// live mirror of overlay input text: refs commit synchronously, so the
|
|
241
437
|
// RETURN handler never reads a stale value mid-typing
|
|
242
438
|
const liveInput = useRef("");
|
|
243
|
-
const railRef = useRef<ReviewRailHandle | null>(null);
|
|
244
439
|
// keymap from layered config; the loaded theme swaps the provider value
|
|
245
440
|
const keysRef = useRef(DEFAULT_KEYS);
|
|
246
441
|
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
@@ -258,12 +453,11 @@ export function App({
|
|
|
258
453
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
259
454
|
setThemeName(config.ui.theme);
|
|
260
455
|
setThemeOverrides(config.themeOverrides);
|
|
261
|
-
setReviewMode(config.ui.reviewState);
|
|
262
|
-
setReviewWidth(config.ui.reviewWidth);
|
|
263
|
-
reviewWidthRef.current = config.ui.reviewWidth;
|
|
264
456
|
setAuthorNames(config.authors);
|
|
265
457
|
setQuickActions(config.actions);
|
|
266
458
|
setAutoClose(config.ui.autoClose);
|
|
459
|
+
setDiffView(config.ui.diffView);
|
|
460
|
+
setPinnedIds(new Set(config.ui.pins));
|
|
267
461
|
controller.applyConfig(config);
|
|
268
462
|
}, [session?.workspace.repoRoot, controller, keyBindings, appearance]);
|
|
269
463
|
useEffect(
|
|
@@ -305,15 +499,14 @@ export function App({
|
|
|
305
499
|
appearance,
|
|
306
500
|
autoClose,
|
|
307
501
|
setAutoClose,
|
|
308
|
-
|
|
309
|
-
|
|
502
|
+
diffView,
|
|
503
|
+
setDiffView,
|
|
310
504
|
themeName,
|
|
311
505
|
setThemeName,
|
|
312
506
|
themeOverrides,
|
|
313
507
|
setTheme,
|
|
314
508
|
quickActions,
|
|
315
509
|
setQuickActions,
|
|
316
|
-
controller,
|
|
317
510
|
setMenuDialog,
|
|
318
511
|
});
|
|
319
512
|
|
|
@@ -321,7 +514,6 @@ export function App({
|
|
|
321
514
|
const display = controller.display();
|
|
322
515
|
const rows = controller.rows();
|
|
323
516
|
const rejectedRows = controller.rejectedRows();
|
|
324
|
-
const curationItems = controller.curationItems();
|
|
325
517
|
const marks = useMemo(
|
|
326
518
|
() =>
|
|
327
519
|
session
|
|
@@ -340,84 +532,29 @@ export function App({
|
|
|
340
532
|
return ids;
|
|
341
533
|
}, [marks]);
|
|
342
534
|
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
343
|
-
//
|
|
344
|
-
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;
|
|
345
545
|
const [threadComposing, setThreadComposing] = useState(false);
|
|
346
546
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
347
547
|
// sort position per annotation so the rail interleaves annotation and removal
|
|
348
548
|
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
349
549
|
// annotation resolves to the display index it marked
|
|
350
|
-
const annotationPositions = useMemo(() => {
|
|
351
|
-
const positions = new Map<string, number>();
|
|
352
|
-
|
|
353
|
-
if (!session) return positions;
|
|
354
|
-
if (isDiff) {
|
|
355
|
-
for (const annotation of session.annotations) {
|
|
356
|
-
const blockIndex =
|
|
357
|
-
"blockIndex" in annotation.anchor ? annotation.anchor.blockIndex : undefined;
|
|
358
|
-
|
|
359
|
-
if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
for (const [displayIndex, blockMarks] of marks) {
|
|
363
|
-
for (const mark of blockMarks)
|
|
364
|
-
if (mark.annotationId) positions.set(mark.annotationId, displayIndex);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return positions;
|
|
369
|
-
}, [session, isDiff, marks]);
|
|
370
|
-
|
|
371
550
|
// ── the guided walk's view model ────────────
|
|
372
551
|
const walkFileList = controller.files();
|
|
373
552
|
const walking = isWalking(isDiff, walk);
|
|
374
553
|
const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
|
|
375
554
|
|
|
376
555
|
// ── selection symmetry: one selected id, both sides ──
|
|
377
|
-
const pulse = (id: string): void => {
|
|
378
|
-
setPulsedAnnotationId(id);
|
|
379
|
-
if (pulseTimer.current) clearTimeout(pulseTimer.current);
|
|
380
|
-
pulseTimer.current = setTimeout(() => setPulsedAnnotationId(null), 2000);
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
/** Card activation scrolls the document to the anchor and pulses it. */
|
|
384
|
-
const revealAnchor = (annotationId: string): void => {
|
|
385
|
-
for (const [displayIndex, blockMarks] of marks) {
|
|
386
|
-
if (!blockMarks.some((mark) => mark.annotationId === annotationId)) continue;
|
|
387
|
-
setCursor(displayIndex);
|
|
388
|
-
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
|
|
393
556
|
const selectCardFromDocument = (annotationId: string): void => {
|
|
394
557
|
setFocusedAnnotationId(annotationId);
|
|
395
|
-
railRef.current?.revealCard(annotationId);
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const selectCardFromRail = (annotationId: string): void => {
|
|
399
|
-
setFocusedAnnotationId(annotationId);
|
|
400
|
-
pulse(annotationId);
|
|
401
|
-
revealAnchor(annotationId);
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
// selecting a removal reveals its source line, so the reader sees what the
|
|
405
|
-
// pending undo would bring back: the diff sheet follows the cursor; the plan
|
|
406
|
-
// sheet also needs an explicit scroll to the cut block
|
|
407
|
-
const selectCurationFromRail = (curationId: string): void => {
|
|
408
|
-
setSelectedCurationId(curationId);
|
|
409
|
-
const item = curationItems.find((candidate) => candidate.id === curationId);
|
|
410
|
-
|
|
411
|
-
if (!item) return;
|
|
412
|
-
setCursor(item.revealIndex);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// the selected removal card's undo button: same restore path as the u key,
|
|
416
|
-
// including the observer read-only guard the keyboard path gets via reduceKey
|
|
417
|
-
const undoCurationFromRail = (curationId: string): void => {
|
|
418
|
-
if (observer) return controller.setStatus("observer - read-only");
|
|
419
|
-
controller.restoreCuration(curationId);
|
|
420
|
-
setSelectedCurationId(undefined);
|
|
421
558
|
};
|
|
422
559
|
|
|
423
560
|
const openCardEdit = (annotationId: string): void => {
|
|
@@ -459,13 +596,10 @@ export function App({
|
|
|
459
596
|
display,
|
|
460
597
|
rows,
|
|
461
598
|
cursor,
|
|
462
|
-
inbox,
|
|
599
|
+
inbox: inbox === null ? null : grouped.ordered,
|
|
463
600
|
inboxCursor,
|
|
464
601
|
mode,
|
|
465
602
|
session,
|
|
466
|
-
reviewMode,
|
|
467
|
-
reviewWidth,
|
|
468
|
-
terminalWidth,
|
|
469
603
|
focusedAnnotationId,
|
|
470
604
|
selectedCurationId,
|
|
471
605
|
railTab,
|
|
@@ -476,13 +610,11 @@ export function App({
|
|
|
476
610
|
persistAuthorName(id, name);
|
|
477
611
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
478
612
|
},
|
|
613
|
+
renameThread: (id: string, title: string) => controller.renameSession(id, title),
|
|
479
614
|
liveInput,
|
|
480
|
-
reviewWidthRef,
|
|
481
615
|
setCursor,
|
|
482
616
|
setInboxCursor,
|
|
483
617
|
setMode,
|
|
484
|
-
setReviewMode,
|
|
485
|
-
setReviewWidth,
|
|
486
618
|
setRailTab,
|
|
487
619
|
setSelectedEntryId,
|
|
488
620
|
setFocusedAnnotationId,
|
|
@@ -491,14 +623,22 @@ export function App({
|
|
|
491
623
|
selectCardFromDocument,
|
|
492
624
|
runEditorHandOff,
|
|
493
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
|
+
},
|
|
494
634
|
});
|
|
495
635
|
|
|
496
636
|
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
497
637
|
|
|
498
|
-
const menuOwnsKeyboard = menuChromeOpen(
|
|
638
|
+
const menuOwnsKeyboard = menuChromeOpen(menuDialog);
|
|
499
639
|
// an overlay (submit, walk, prompt, confirm) or the menu takes the keyboard
|
|
500
640
|
// from the thread view; the view suspends its own grammar meanwhile
|
|
501
|
-
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay
|
|
641
|
+
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
|
|
502
642
|
|
|
503
643
|
useKeyboard((key) => {
|
|
504
644
|
// The thread view owns the document grammar while active (its own
|
|
@@ -511,6 +651,7 @@ export function App({
|
|
|
511
651
|
isOwner,
|
|
512
652
|
resolved,
|
|
513
653
|
treeActive: railTab === "tree",
|
|
654
|
+
isDiff,
|
|
514
655
|
});
|
|
515
656
|
|
|
516
657
|
if (chord) dispatch(chord);
|
|
@@ -520,16 +661,8 @@ export function App({
|
|
|
520
661
|
// The prototype compose textarea owns the keyboard while open: let it receive
|
|
521
662
|
// the typed note instead of the global keymap acting on each letter.
|
|
522
663
|
if (prototypeComposing) return;
|
|
523
|
-
// A running in-tab agent terminal owns the keyboard: forward every key to it,
|
|
524
|
-
// with ctrl+] as the detach chord back to the review.
|
|
525
|
-
if (agentTerminal) {
|
|
526
|
-
if (key.ctrl && key.name === "]") return void agentTerminal.detach();
|
|
527
|
-
|
|
528
|
-
return void agentTerminal.write(key.sequence);
|
|
529
|
-
}
|
|
530
664
|
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
531
665
|
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
532
|
-
if (menuOpen) return void (key.name === "escape" && setMenuOpen(false));
|
|
533
666
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
534
667
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
535
668
|
if (toast && key.name === "escape" && overlay === "none" && mode.type !== "span")
|
|
@@ -552,8 +685,14 @@ export function App({
|
|
|
552
685
|
display,
|
|
553
686
|
});
|
|
554
687
|
|
|
555
|
-
keyBindings.setContext({
|
|
556
|
-
|
|
688
|
+
keyBindings.setContext({
|
|
689
|
+
overlay: state.overlay,
|
|
690
|
+
spanMode: state.spanMode,
|
|
691
|
+
});
|
|
692
|
+
const action = keyBindings.resolveAction({
|
|
693
|
+
name: key.name,
|
|
694
|
+
shift: !!key.shift,
|
|
695
|
+
});
|
|
557
696
|
|
|
558
697
|
for (const intent of reduceKey(
|
|
559
698
|
state,
|
|
@@ -567,12 +706,8 @@ export function App({
|
|
|
567
706
|
// reused by the inbox and by plan/diff review so the two never drift ──
|
|
568
707
|
const menuChrome = (
|
|
569
708
|
<MenuChrome
|
|
570
|
-
menuOpen={menuOpen}
|
|
571
709
|
menuDialog={menuDialog}
|
|
572
|
-
status={status}
|
|
573
710
|
theme={theme}
|
|
574
|
-
setMenuOpen={setMenuOpen}
|
|
575
|
-
setMenuDialog={setMenuDialog}
|
|
576
711
|
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
577
712
|
settingsCategories={settingsCategories}
|
|
578
713
|
settingsValues={settingsValues}
|
|
@@ -586,14 +721,20 @@ export function App({
|
|
|
586
721
|
if (error) return <ErrorScreen error={error} theme={theme} />;
|
|
587
722
|
if (!session)
|
|
588
723
|
return inbox ? (
|
|
589
|
-
<
|
|
590
|
-
|
|
724
|
+
<NoThreadShell
|
|
725
|
+
rows={grouped.rows}
|
|
591
726
|
inboxCursor={inboxCursor}
|
|
592
727
|
mode={mode}
|
|
593
728
|
theme={theme}
|
|
594
729
|
controller={controller}
|
|
595
730
|
setMode={setMode}
|
|
596
731
|
menuChrome={menuChrome}
|
|
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 })}
|
|
597
738
|
/>
|
|
598
739
|
) : (
|
|
599
740
|
<ConnectingScreen theme={theme} />
|
|
@@ -612,14 +753,15 @@ export function App({
|
|
|
612
753
|
/>
|
|
613
754
|
);
|
|
614
755
|
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
+
};
|
|
623
765
|
const submitConfirmState = buildSubmitConfirmState({
|
|
624
766
|
mode,
|
|
625
767
|
isDiff,
|
|
@@ -630,21 +772,15 @@ export function App({
|
|
|
630
772
|
setMode,
|
|
631
773
|
dispatch,
|
|
632
774
|
});
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
isPrototype,
|
|
643
|
-
resolved,
|
|
644
|
-
menuOpen,
|
|
645
|
-
menuDialog,
|
|
646
|
-
resolvedIds,
|
|
647
|
-
});
|
|
775
|
+
const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath } = buildRenderFlags({
|
|
776
|
+
session: activeSession,
|
|
777
|
+
isOwner,
|
|
778
|
+
isDiff,
|
|
779
|
+
isPrototype,
|
|
780
|
+
resolved,
|
|
781
|
+
menuDialog,
|
|
782
|
+
resolvedIds,
|
|
783
|
+
});
|
|
648
784
|
|
|
649
785
|
const onEditRequest = (): void => {
|
|
650
786
|
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
@@ -668,167 +804,185 @@ export function App({
|
|
|
668
804
|
dispatch({ type: "openSubmit" });
|
|
669
805
|
};
|
|
670
806
|
|
|
671
|
-
// the clickable chevron toggles expanded <-> compact; the keybinding (b)
|
|
672
|
-
// cycles all three including hidden
|
|
673
|
-
const onToggleReviewPanel = (): void => {
|
|
674
|
-
const next = toggleReviewPanelMode(reviewMode);
|
|
675
|
-
|
|
676
|
-
setReviewMode(next);
|
|
677
|
-
controller.saveReviewPanel({ mode: next });
|
|
678
|
-
};
|
|
679
|
-
// grabbing the divider only arms a drag when there is a width to drag
|
|
680
|
-
const onDividerGrab = (): void => {
|
|
681
|
-
if (reviewMode === "expanded") setDividerDragging(true);
|
|
682
|
-
};
|
|
683
|
-
|
|
684
807
|
return (
|
|
685
808
|
<ThemeProvider theme={theme}>
|
|
686
|
-
<
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}}
|
|
703
|
-
onMouseUp={() => {
|
|
704
|
-
if (!dividerDragging) return;
|
|
705
|
-
setDividerDragging(false);
|
|
706
|
-
controller.saveReviewPanel({ width: reviewWidthRef.current });
|
|
707
|
-
}}
|
|
708
|
-
>
|
|
709
|
-
<box
|
|
710
|
-
style={{ flexDirection: "row", height: 2, paddingTop: 1, backgroundColor: theme.panel }}
|
|
711
|
-
>
|
|
712
|
-
<box style={{ flexGrow: 1, flexDirection: "row", paddingRight: 1 }}>
|
|
713
|
-
<Breadcrumb items={headerItems} />
|
|
714
|
-
<box style={{ flexGrow: 1 }} />
|
|
715
|
-
{showOwnerActions ? (
|
|
716
|
-
<Toolbar>
|
|
717
|
-
<Button onPress={onEditRequest} theme={theme}>
|
|
718
|
-
{" Edit "}
|
|
719
|
-
</Button>
|
|
720
|
-
<Button onPress={onShareRequest} theme={theme}>
|
|
721
|
-
{" Share "}
|
|
722
|
-
</Button>
|
|
723
|
-
</Toolbar>
|
|
724
|
-
) : null}
|
|
725
|
-
</box>
|
|
726
|
-
<box style={{ width: railFootprint }} />
|
|
727
|
-
</box>
|
|
728
|
-
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
729
|
-
{isPrototype ? (
|
|
730
|
-
<PrototypeSheet
|
|
731
|
-
prototypePath={prototypePath}
|
|
732
|
-
quickActions={quickActions}
|
|
733
|
-
canComment={prototypeCanComment}
|
|
734
|
-
onCommentElement={onCommentPrototype}
|
|
735
|
-
onComposingChange={setPrototypeComposing}
|
|
736
|
-
hidden={chromeHidden}
|
|
737
|
-
/>
|
|
738
|
-
) : isDiff ? (
|
|
739
|
-
// the sheet dims to reading-quiet colors while the wizard has focus
|
|
740
|
-
<DiffSheet
|
|
741
|
-
rows={rows}
|
|
742
|
-
cursor={cursor}
|
|
743
|
-
annotations={activeSession.annotations}
|
|
744
|
-
focusedAnnotationId={focusedAnnotationId}
|
|
745
|
-
rejectedRows={rejectedRows}
|
|
746
|
-
compose={diffComposeState}
|
|
747
|
-
theme={walking ? dimmedTheme(theme) : undefined}
|
|
748
|
-
/>
|
|
749
|
-
) : (
|
|
750
|
-
<ThreadView
|
|
751
|
-
session={activeSession}
|
|
752
|
-
suspended={threadViewSuspended}
|
|
753
|
-
editOrphanCount={editOrphanCount}
|
|
754
|
-
onComposingChange={setThreadComposing}
|
|
755
|
-
resolved={resolved}
|
|
756
|
-
onObserverBlocked={(reason) =>
|
|
757
|
-
controller.setStatus(
|
|
758
|
-
reason === "observer" ? "observer - read-only" : "review submitted - read-only",
|
|
759
|
-
)
|
|
760
|
-
}
|
|
761
|
-
onCursorChange={setCursor}
|
|
762
|
-
focusedAnnotationId={focusedAnnotationId}
|
|
763
|
-
onFocusAnnotation={setFocusedAnnotationId}
|
|
764
|
-
display={display}
|
|
765
|
-
marks={marks}
|
|
766
|
-
quickActions={quickActions}
|
|
767
|
-
observer={observer}
|
|
768
|
-
onAnnotate={(span, body) =>
|
|
769
|
-
void controller.annotate(
|
|
770
|
-
"comment",
|
|
771
|
-
span.start.blockIndex,
|
|
772
|
-
span.start.char,
|
|
773
|
-
span.end.char,
|
|
774
|
-
body,
|
|
775
|
-
span.end.blockIndex,
|
|
776
|
-
)
|
|
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 })
|
|
777
825
|
}
|
|
778
|
-
|
|
779
|
-
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
780
|
-
onExit={() => onExit?.(0)}
|
|
826
|
+
theme={theme}
|
|
781
827
|
/>
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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={
|
|
844
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
845
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
846
|
+
{isPrototype ? (
|
|
847
|
+
<PrototypeSheet
|
|
848
|
+
prototypePath={prototypePath}
|
|
849
|
+
quickActions={quickActions}
|
|
850
|
+
canComment={prototypeCanComment}
|
|
851
|
+
onCommentElement={onCommentPrototype}
|
|
852
|
+
onComposingChange={setPrototypeComposing}
|
|
853
|
+
hidden={chromeHidden}
|
|
854
|
+
/>
|
|
855
|
+
) : isDiff ? (
|
|
856
|
+
<box style={{ flexGrow: 1, paddingLeft: 2, paddingTop: 1 }}>
|
|
857
|
+
<text fg={theme.textDim}>review the changes on the right</text>
|
|
858
|
+
</box>
|
|
859
|
+
) : (
|
|
860
|
+
<ThreadView
|
|
861
|
+
session={activeSession}
|
|
862
|
+
suspended={threadViewSuspended}
|
|
863
|
+
editOrphanCount={editOrphanCount}
|
|
864
|
+
onComposingChange={setThreadComposing}
|
|
865
|
+
resolved={resolved}
|
|
866
|
+
onObserverBlocked={(reason) =>
|
|
867
|
+
controller.setStatus(
|
|
868
|
+
reason === "observer"
|
|
869
|
+
? "observer - read-only"
|
|
870
|
+
: "review submitted - read-only",
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
onCursorChange={setCursor}
|
|
874
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
875
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
876
|
+
display={display}
|
|
877
|
+
marks={marks}
|
|
878
|
+
quickActions={quickActions}
|
|
879
|
+
observer={observer}
|
|
880
|
+
onAnnotate={(span, body) =>
|
|
881
|
+
void controller.annotate(
|
|
882
|
+
"comment",
|
|
883
|
+
span.start.blockIndex,
|
|
884
|
+
span.start.char,
|
|
885
|
+
span.end.char,
|
|
886
|
+
body,
|
|
887
|
+
span.end.blockIndex,
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
onReply={(rootAnnotationId, body) =>
|
|
891
|
+
void controller.reply(rootAnnotationId, body)
|
|
892
|
+
}
|
|
893
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
894
|
+
onExit={() => onExit?.(0)}
|
|
895
|
+
/>
|
|
896
|
+
)}
|
|
897
|
+
</box>
|
|
898
|
+
<ThreadFooter
|
|
899
|
+
repo={projectName(activeSession.workspace)}
|
|
900
|
+
branch={activeSession.workspace.branch}
|
|
901
|
+
onSubmit={onSubmitRequest}
|
|
902
|
+
canSubmit={canSubmitReview(isOwner, resolved, observer)}
|
|
903
|
+
theme={theme}
|
|
904
|
+
/>
|
|
905
|
+
</box>
|
|
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
|
+
)}
|
|
968
|
+
theme={theme}
|
|
830
969
|
/>
|
|
831
|
-
|
|
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
|
+
>
|
|
832
986
|
<TrailingOverlays
|
|
833
987
|
walking={walking}
|
|
834
988
|
walk={walk}
|
|
@@ -842,8 +996,23 @@ export function App({
|
|
|
842
996
|
setMode={setMode}
|
|
843
997
|
dispatch={dispatch}
|
|
844
998
|
/>
|
|
999
|
+
{submitConfirmState !== null ? (
|
|
1000
|
+
<box
|
|
1001
|
+
style={{
|
|
1002
|
+
position: "absolute",
|
|
1003
|
+
left: 0,
|
|
1004
|
+
top: 0,
|
|
1005
|
+
width: "100%",
|
|
1006
|
+
height: "100%",
|
|
1007
|
+
justifyContent: "center",
|
|
1008
|
+
alignItems: "center",
|
|
1009
|
+
}}
|
|
1010
|
+
>
|
|
1011
|
+
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
1012
|
+
</box>
|
|
1013
|
+
) : null}
|
|
845
1014
|
{menuChrome}
|
|
846
|
-
</
|
|
1015
|
+
</AppShell>
|
|
847
1016
|
</ThemeProvider>
|
|
848
1017
|
);
|
|
849
1018
|
}
|