@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.81
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +574 -0
- package/src/app-view-model.ts +162 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +876 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -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 +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- 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 +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +59 -0
- package/src/components/ProjectTreeView.tsx +111 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +56 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -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 +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +517 -33
- package/src/config.ts +467 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +64 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +78 -13
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +1893 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +915 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
package/src/App.tsx
CHANGED
|
@@ -1,676 +1,1406 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
10
|
+
import type { Clock, KeyEvent } from "@opentui/core";
|
|
11
|
+
import { marksByDisplay, type Mark } from "./view-plan";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type
|
|
21
|
-
type
|
|
22
|
-
type
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
13
|
+
DEFAULT_KEYS,
|
|
14
|
+
DEFAULT_QUICK_ACTIONS,
|
|
15
|
+
loadConfig,
|
|
16
|
+
persistAuthorName,
|
|
17
|
+
persistDiffView,
|
|
18
|
+
persistIdentity,
|
|
19
|
+
persistPins,
|
|
20
|
+
type AutoClose,
|
|
21
|
+
type DiffViewMode,
|
|
22
|
+
type IdentityConfig,
|
|
23
|
+
type QuickAction,
|
|
24
|
+
} from "./config";
|
|
25
|
+
import { resolveDisplayName } from "./attribution";
|
|
26
|
+
import { resolveGithubIdentity } from "./github-identity";
|
|
27
|
+
import { loadSkills, PaletteNamesContext, SlashSkillsContext } from "./skills";
|
|
28
|
+
import { mergeSlashItems, slashItemsFrom, type SlashItem } from "./slash-palette";
|
|
29
|
+
import {
|
|
30
|
+
composeTheme,
|
|
31
|
+
DEFAULT_THEME_NAME,
|
|
32
|
+
themeForName,
|
|
33
|
+
type Appearance,
|
|
34
|
+
type ThemeName,
|
|
35
|
+
} from "./theme-presets";
|
|
36
|
+
import type { Theme } from "./theme";
|
|
37
|
+
import { createReviewController, type ShareTransport } from "./thread-controller";
|
|
38
|
+
import type { ThreadClient } from "@cueloop/daemon/client";
|
|
39
|
+
import { createIntentDispatch, type Mode, type RailTab } from "./intent-dispatch";
|
|
40
|
+
import { reduceKey, type KeyState } from "./keymap";
|
|
41
|
+
import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
42
|
+
import { useReadySignal } from "./ready-signal";
|
|
43
|
+
import { ThemeProvider } from "./components/theme-context";
|
|
44
|
+
import { Button } from "./components/primitives/Button";
|
|
45
|
+
import { ShareDialog } from "./components/ShareDialog";
|
|
46
|
+
import { shareDialogStore } from "./components/share-dialog-store";
|
|
47
|
+
import { Toolbar } from "./components/primitives/Toolbar";
|
|
48
|
+
import { groupInbox, projectName, threadTitle } from "./components/thread-tree";
|
|
49
|
+
import { ThreadTree } from "./components/ThreadTree";
|
|
50
|
+
import { ChangesFileTree } from "./components/ChangesColumn";
|
|
51
|
+
import { MenuControlProvider, useMenuControlState } from "./components/menu-control";
|
|
52
|
+
import { ProjectTreeView } from "./components/ProjectTreeView";
|
|
53
|
+
import { AppShell, type FocusPane, type ProjectPanelMode } from "./components/AppShell";
|
|
54
|
+
import { EditorGrid } from "./components/EditorGrid";
|
|
55
|
+
import { GridTabContent } from "./components/GridTabContent";
|
|
56
|
+
import { useChangesWorkbench } from "./use-changes-workbench";
|
|
57
|
+
import { useThreadBodyEditing } from "./use-thread-body-editing";
|
|
58
|
+
import { useRememberLayout } from "./use-remember-layout";
|
|
59
|
+
import type { LaunchLayout } from "./launch-layout";
|
|
60
|
+
import { ThreadFooter, THREAD_FOOTER_HEIGHT } from "./components/ThreadFooter";
|
|
61
|
+
import { ConfirmCard } from "./components/ConfirmCard";
|
|
62
|
+
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
63
|
+
import {
|
|
64
|
+
curationCommandEntries,
|
|
65
|
+
diffCommandEntries,
|
|
66
|
+
resolveNavKey,
|
|
67
|
+
resolveSessionChord,
|
|
68
|
+
sessionCommandEntries,
|
|
69
|
+
treeCommandEntries,
|
|
70
|
+
} from "./thread-chords";
|
|
71
|
+
import { type DiffFoldControls } from "./components/DiffContentView";
|
|
72
|
+
import { commentCountsByFile } from "./view-diff";
|
|
73
|
+
import { annotationTarget, isAddressed, isAgentNote, threadShareLinks } from "@cueloop/schema";
|
|
74
|
+
import type {
|
|
75
|
+
Annotation,
|
|
76
|
+
Artifact,
|
|
77
|
+
DiffFileContents,
|
|
78
|
+
Identity,
|
|
79
|
+
ShareLink,
|
|
80
|
+
Thread,
|
|
81
|
+
MessageOutcome,
|
|
82
|
+
} from "@cueloop/schema";
|
|
83
|
+
import { PrototypePixels } from "./prototype-pixels";
|
|
84
|
+
import type { PrototypeElement } from "./prototype-browser";
|
|
85
|
+
import {
|
|
86
|
+
buildRenderFlags,
|
|
87
|
+
buildSubmitConfirmState,
|
|
88
|
+
computeRoleCapabilities,
|
|
89
|
+
deriveReviewFlags,
|
|
90
|
+
isPixelPrototypeMode,
|
|
91
|
+
isCompletionOverlayPhase,
|
|
92
|
+
isWalking,
|
|
93
|
+
resolveOverlay,
|
|
94
|
+
} from "./app-view-model";
|
|
95
|
+
import { buildKeyState } from "./app-key-state";
|
|
96
|
+
import { useSettingsDialog } from "./use-settings-dialog";
|
|
97
|
+
import {
|
|
98
|
+
CompletionScreen,
|
|
99
|
+
ConnectingScreen,
|
|
100
|
+
ErrorScreen,
|
|
101
|
+
MenuChrome,
|
|
102
|
+
NoThreadShell,
|
|
103
|
+
TrailingOverlays,
|
|
104
|
+
} from "./app-screens";
|
|
105
|
+
/** A toast clears itself after this idle; esc dismisses it sooner. */
|
|
106
|
+
const TOAST_DISMISS_MS = 2000;
|
|
31
107
|
|
|
32
108
|
export interface AppProps {
|
|
33
109
|
home?: string;
|
|
34
110
|
sessionId?: string;
|
|
111
|
+
/** The launch directory whose git repo backs the no-session welcome tree; defaults to process.cwd(). */
|
|
112
|
+
cwd?: string;
|
|
35
113
|
/**
|
|
36
|
-
* Observer mode (SSH-served connections): every mutating
|
|
114
|
+
* Observer mode (SSH-served connections): every mutating primitive is ignored and
|
|
37
115
|
* answers "observer - read-only" in the status line; navigation still works.
|
|
38
116
|
*/
|
|
39
117
|
readOnly?: boolean;
|
|
40
118
|
onExit?: (code: number) => void;
|
|
119
|
+
/** Fired once, after the first frame that paints a usable screen with its keyboard handlers live (ready-signal.ts). */
|
|
120
|
+
onReady?: () => void;
|
|
121
|
+
/** Timer source for the auto-close countdown; tests inject a ManualClock. */
|
|
122
|
+
clock?: Clock;
|
|
123
|
+
/** Session source; the sharing gateway injects a blob-backed client. */
|
|
124
|
+
openClient?: () => Promise<ThreadClient>;
|
|
125
|
+
shareTransport?: ShareTransport;
|
|
126
|
+
/**
|
|
127
|
+
* Who is at the keyboard. `owner` is the local planner (default). `observer`
|
|
128
|
+
* is a passive `cueloop serve` watcher (read-only). `collaborator` is a share
|
|
129
|
+
* viewer: annotates, but cannot edit the plan or submit an agent message.
|
|
130
|
+
*/
|
|
131
|
+
role?: "owner" | "observer" | "collaborator";
|
|
132
|
+
/**
|
|
133
|
+
* A collaborator's own SSH fingerprint. On first open of a share it seeds the
|
|
134
|
+
* name prompt so their notes attribute to a name, not a fingerprint.
|
|
135
|
+
*/
|
|
136
|
+
selfAuthor?: string;
|
|
137
|
+
/**
|
|
138
|
+
* The terminal's background appearance (from an OSC query at startup). The
|
|
139
|
+
* branded transparent theme darkens its text on a light terminal so it is not
|
|
140
|
+
* light-on-light. Defaults to dark - the historical assumption.
|
|
141
|
+
*/
|
|
142
|
+
appearance?: Appearance;
|
|
143
|
+
/** The pane composition to open in; a create-command sets it, a bare launch restores the remembered one. */
|
|
144
|
+
layout?: LaunchLayout;
|
|
145
|
+
/** Serve mode: the frozen working-tree diff an observer reads for the served workbench thread. */
|
|
146
|
+
servedArtifact?: Artifact;
|
|
41
147
|
}
|
|
42
148
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
| { m: "submit"; verdict: VerdictKind; summary: string };
|
|
149
|
+
/** True while the drop-up or one of its dialogs is open and owns the keyboard. */
|
|
150
|
+
function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
151
|
+
return menuDialog !== null;
|
|
152
|
+
}
|
|
48
153
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
request_changes: "Request changes",
|
|
54
|
-
};
|
|
154
|
+
/** Whether a thread already has at least one live share link. */
|
|
155
|
+
function isSharedThread(session: Thread | null): boolean {
|
|
156
|
+
return session !== null && (threadShareLinks(session)?.length ?? 0) > 0;
|
|
157
|
+
}
|
|
55
158
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
[home, sessionId],
|
|
61
|
-
);
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
controller.connect();
|
|
64
|
-
return () => controller.close();
|
|
65
|
-
}, [controller]);
|
|
66
|
-
const { session, inbox, status, error, completion } = useSyncExternalStore(
|
|
67
|
-
controller.subscribe,
|
|
68
|
-
controller.getSnapshot,
|
|
69
|
-
controller.getSnapshot,
|
|
70
|
-
);
|
|
159
|
+
/** A thread's live share links for the share dialog, or an empty list with no session. */
|
|
160
|
+
function shareLinksFor(session: Thread | null): ShareLink[] {
|
|
161
|
+
return session ? (threadShareLinks(session) ?? []) : [];
|
|
162
|
+
}
|
|
71
163
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const [focusedAnn, setFocusedAnn] = useState<string | undefined>(undefined);
|
|
77
|
-
// live mirror of overlay input text: refs commit synchronously, so the
|
|
78
|
-
// RETURN handler never reads a stale value mid-typing
|
|
79
|
-
const liveInput = useRef("");
|
|
80
|
-
// keymap from layered config; theme overrides land on the shared tokens
|
|
81
|
-
const keysRef = useRef(DEFAULT_KEYS);
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
const cfg = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
84
|
-
keysRef.current = cfg.keys;
|
|
85
|
-
Object.assign(T, cfg.theme);
|
|
86
|
-
controller.applyConfig(cfg);
|
|
87
|
-
}, [session?.workspace.repoRoot, controller]);
|
|
164
|
+
/** The title the share dialog shows and prefills a new link with, or empty with no session. */
|
|
165
|
+
function shareThreadName(session: Thread | null): string {
|
|
166
|
+
return session ? threadTitle(session) : "";
|
|
167
|
+
}
|
|
88
168
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const resolved = session?.status === "resolved";
|
|
97
|
-
const isDiff = session?.artifact.type === "diff";
|
|
169
|
+
/** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
|
|
170
|
+
function keyboardHeldByMenu(
|
|
171
|
+
menuDialog: "keybinds" | "settings" | null,
|
|
172
|
+
openMenuId: string | null,
|
|
173
|
+
): boolean {
|
|
174
|
+
return menuChromeOpen(menuDialog) || openMenuId !== null;
|
|
175
|
+
}
|
|
98
176
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (intent.to === "down") setCursor((c) => Math.min(len - 1, c + 1));
|
|
109
|
-
else if (intent.to === "up") setCursor((c) => Math.max(0, c - 1));
|
|
110
|
-
else if (intent.to === "top") setCursor(0);
|
|
111
|
-
else setCursor(len - 1);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
case "inboxMove": {
|
|
115
|
-
const len = inbox?.length ?? 0;
|
|
116
|
-
setInboxCursor((c) => (intent.to === "down" ? Math.min(len - 1, c + 1) : Math.max(0, c - 1)));
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
case "openSession": {
|
|
120
|
-
const s = inbox?.[inboxCursor];
|
|
121
|
-
if (s) controller.open(s.id);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
case "startSpan": {
|
|
125
|
-
const d = display[cursor];
|
|
126
|
-
if (!d?.work) return;
|
|
127
|
-
const span = startSpan(cursor, displayText(d));
|
|
128
|
-
if (span) setMode({ m: "span", span });
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
case "spanKey":
|
|
132
|
-
if (mode.m === "span") {
|
|
133
|
-
setMode({ m: "span", span: spanKey(mode.span, intent.name, displayText(display[mode.span.dispIdx]!)) });
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
case "openCompose": {
|
|
137
|
-
liveInput.current = "";
|
|
138
|
-
if (intent.from === "span" && mode.m === "span") {
|
|
139
|
-
setMode({
|
|
140
|
-
m: "compose",
|
|
141
|
-
kind: intent.kind,
|
|
142
|
-
dispIdx: mode.span.dispIdx,
|
|
143
|
-
start: mode.span.start,
|
|
144
|
-
end: mode.span.end,
|
|
145
|
-
text: "",
|
|
146
|
-
});
|
|
147
|
-
} else if (isDiff) {
|
|
148
|
-
const row = rows[cursor];
|
|
149
|
-
if (row) setMode({ m: "compose", kind: intent.kind, dispIdx: cursor, start: 0, end: row.text.length, text: "" });
|
|
150
|
-
} else {
|
|
151
|
-
const d = display[cursor];
|
|
152
|
-
if (d) setMode({ m: "compose", kind: intent.kind, dispIdx: cursor, start: 0, end: displayText(d).length, text: "" });
|
|
153
|
-
}
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
case "openSubmit":
|
|
157
|
-
if (!session) return;
|
|
158
|
-
liveInput.current = "";
|
|
159
|
-
return void setMode({ m: "submit", verdict: defaultVerdict(session), summary: "" });
|
|
160
|
-
case "cut":
|
|
161
|
-
return controller.cut(cursor);
|
|
162
|
-
case "edit":
|
|
163
|
-
return controller.edit();
|
|
164
|
-
case "nextAnn":
|
|
165
|
-
case "prevAnn": {
|
|
166
|
-
const anns = session?.annotations ?? [];
|
|
167
|
-
if (!anns.length) return;
|
|
168
|
-
const idx = anns.findIndex((a) => a.id === focusedAnn);
|
|
169
|
-
const next = idx === -1 ? 0 : (idx + (intent.t === "nextAnn" ? 1 : -1) + anns.length) % anns.length;
|
|
170
|
-
return void setFocusedAnn(anns[next]!.id);
|
|
171
|
-
}
|
|
172
|
-
case "removeAnnotation":
|
|
173
|
-
if (focusedAnn) {
|
|
174
|
-
controller.removeAnnotation(focusedAnn);
|
|
175
|
-
setFocusedAnn(undefined);
|
|
176
|
-
}
|
|
177
|
-
return;
|
|
178
|
-
case "closeOverlay":
|
|
179
|
-
return void setMode({ m: "normal" });
|
|
180
|
-
case "saveCompose": {
|
|
181
|
-
if (mode.m !== "compose") return;
|
|
182
|
-
const body = liveInput.current.trim();
|
|
183
|
-
if (session && body) controller.annotate(mode.kind, mode.dispIdx, mode.start, mode.end, body);
|
|
184
|
-
return void setMode({ m: "normal" });
|
|
185
|
-
}
|
|
186
|
-
case "submitVerdict":
|
|
187
|
-
if (mode.m === "submit") controller.submit(mode.verdict, liveInput.current);
|
|
188
|
-
return void setMode({ m: "normal" });
|
|
189
|
-
case "cycleVerdict": {
|
|
190
|
-
if (mode.m !== "submit") return;
|
|
191
|
-
const idx = (VERDICTS.indexOf(mode.verdict) + intent.dir + VERDICTS.length) % VERDICTS.length;
|
|
192
|
-
return void setMode({ ...mode, verdict: VERDICTS[idx]! });
|
|
193
|
-
}
|
|
194
|
-
case "finishReview":
|
|
195
|
-
return controller.finishReview();
|
|
196
|
-
case "optInAutoClose":
|
|
197
|
-
return controller.optInAutoClose();
|
|
198
|
-
case "dismissCompletion":
|
|
199
|
-
return controller.dismissCompletion();
|
|
200
|
-
}
|
|
201
|
-
};
|
|
177
|
+
/** A non-modal toast: escape dismisses it only when nothing else owns escape (no overlay, not span mode). */
|
|
178
|
+
export function toastDismissRequested(
|
|
179
|
+
toastOpen: boolean,
|
|
180
|
+
key: { name: string },
|
|
181
|
+
overlay: KeyState["overlay"],
|
|
182
|
+
spanMode: boolean,
|
|
183
|
+
): boolean {
|
|
184
|
+
return toastOpen && key.name === "escape" && overlay === "none" && !spanMode;
|
|
185
|
+
}
|
|
202
186
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
: mode.m === "submit"
|
|
211
|
-
? "submit"
|
|
212
|
-
: completion.phase === "prompt"
|
|
213
|
-
? "completion-prompt"
|
|
214
|
-
: completion.phase === "counting"
|
|
215
|
-
? "completion-counting"
|
|
216
|
-
: "none",
|
|
217
|
-
view: !session ? "inbox" : isDiff ? "diff" : "plan",
|
|
218
|
-
spanMode: mode.m === "span",
|
|
219
|
-
resolved: !!resolved,
|
|
220
|
-
hasInboxItems: !!inbox?.length,
|
|
221
|
-
annotationCount: session?.annotations.length ?? 0,
|
|
222
|
-
hasFocusedAnnotation: focusedAnn !== undefined,
|
|
223
|
-
cursorAnnotatable: isDiff
|
|
224
|
-
? rows[cursor] !== undefined && rows[cursor]!.t !== "file" && rows[cursor]!.t !== "hunk"
|
|
225
|
-
: !!display[cursor]?.work,
|
|
226
|
-
};
|
|
227
|
-
for (const intent of reduceKey(state, { name: key.name, shift: !!key.shift })) dispatch(intent);
|
|
228
|
-
});
|
|
187
|
+
/** ctrl+q quits from anywhere, ahead of every focus/menu/overlay gate, with no confirmation. */
|
|
188
|
+
export function quitKeyHandled(
|
|
189
|
+
key: { name: string; ctrl?: boolean },
|
|
190
|
+
onExit?: (code: number) => void,
|
|
191
|
+
): boolean {
|
|
192
|
+
if (!key.ctrl || key.name !== "q") return false;
|
|
193
|
+
onExit?.(0);
|
|
229
194
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!session && !inbox) return <text fg={T.textDim}>connecting to the daemon…</text>;
|
|
233
|
-
if (!session && inbox) return <Inbox inbox={inbox} cursor={inboxCursor} />;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
234
197
|
|
|
235
|
-
|
|
236
|
-
|
|
198
|
+
/** A popover menu is open, so the shell keyboard is modal: escape closes it, every other key is swallowed. */
|
|
199
|
+
function menuModalHandled(
|
|
200
|
+
menuControl: { openMenuId: string | null; closeMenu: () => void },
|
|
201
|
+
key: { name: string },
|
|
202
|
+
): boolean {
|
|
203
|
+
if (menuControl.openMenuId === null) return false;
|
|
204
|
+
if (key.name === "escape") menuControl.closeMenu();
|
|
237
205
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** True while a menu, an overlay, or the share dialog owns the keyboard instead of the thread view. */
|
|
210
|
+
function keyboardOwnedElsewhere(
|
|
211
|
+
menuOwnsKeyboard: boolean,
|
|
212
|
+
overlay: KeyState["overlay"],
|
|
213
|
+
shareDialogOpen: boolean,
|
|
214
|
+
): boolean {
|
|
215
|
+
return menuOwnsKeyboard || overlay !== "none" || shareDialogOpen;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
219
|
+
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
220
|
+
return isOwner && !resolved && !observer;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The thread body edits inline only for its owner, on an unresolved markdown thread (never a diff). */
|
|
224
|
+
function canEditThreadBody(
|
|
225
|
+
threadViewActive: boolean,
|
|
226
|
+
isDiff: boolean,
|
|
227
|
+
isOwner: boolean,
|
|
228
|
+
resolved: boolean,
|
|
229
|
+
): boolean {
|
|
230
|
+
return threadViewActive && !isDiff && isOwner && !resolved;
|
|
231
|
+
}
|
|
248
232
|
|
|
233
|
+
/** The owner's header actions: an edit/normal toggle that opens or leaves the inline editor, plus share when not editing. */
|
|
234
|
+
function ownerThreadActions(actions: {
|
|
235
|
+
editing: boolean;
|
|
236
|
+
onEdit: () => void;
|
|
237
|
+
onExitEdit: () => void;
|
|
238
|
+
onShare: () => void;
|
|
239
|
+
theme: Theme;
|
|
240
|
+
}): React.ReactNode {
|
|
249
241
|
return (
|
|
250
|
-
<
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
|
|
258
|
-
</text>
|
|
259
|
-
</box>
|
|
260
|
-
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
261
|
-
{isDiff ? (
|
|
262
|
-
<DiffPane rows={rows} cursor={cursor} annotations={s.annotations} focusedAnn={focusedAnn} />
|
|
263
|
-
) : (
|
|
264
|
-
<PlanPane display={display} marks={marks} cursor={cursor} mode={mode} />
|
|
265
|
-
)}
|
|
266
|
-
<Rail session={s} focusedAnn={focusedAnn} pendingCount={pendingCount} />
|
|
267
|
-
</box>
|
|
268
|
-
{mode.m === "compose" ? (
|
|
269
|
-
<ComposeBar mode={mode} quote={isDiff ? (rows[mode.dispIdx]?.text ?? "") : displayText(display[mode.dispIdx]!).slice(mode.start, mode.end)} onChange={(text) => { liveInput.current = text; setMode({ ...mode, text }); }} />
|
|
270
|
-
) : mode.m === "submit" ? (
|
|
271
|
-
<SubmitBar mode={mode} pendingCount={pendingCount} onChange={(summary) => { liveInput.current = summary; setMode({ ...mode, summary }); }} />
|
|
272
|
-
) : (
|
|
273
|
-
<box style={{ height: 1, backgroundColor: T.panel, paddingLeft: 1 }}>
|
|
274
|
-
<text fg={T.textDim}>
|
|
275
|
-
{readOnly
|
|
276
|
-
? "observer - read-only · j/k move · n/p annotations · q quit"
|
|
277
|
-
: mode.m === "span"
|
|
278
|
-
? "span · l/h grow/shrink · w/b slide · $ end · c comment · s suggest · esc"
|
|
279
|
-
: "j/k move · v span · c comment · s suggest · x cut · e edit · n/p annotations · ⏎ submit · q quit"}
|
|
280
|
-
</text>
|
|
281
|
-
</box>
|
|
242
|
+
<Toolbar>
|
|
243
|
+
<Button onPress={actions.editing ? actions.onExitEdit : actions.onEdit} theme={actions.theme}>
|
|
244
|
+
{actions.editing ? " normal " : " edit "}
|
|
245
|
+
</Button>
|
|
246
|
+
{actions.editing ? null : (
|
|
247
|
+
<Button onPress={actions.onShare} theme={actions.theme}>
|
|
248
|
+
{" share "}
|
|
249
|
+
</Button>
|
|
282
250
|
)}
|
|
283
|
-
</
|
|
251
|
+
</Toolbar>
|
|
284
252
|
);
|
|
285
253
|
}
|
|
286
254
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
marks: Map<number, { start: number; end: number; role: StyleRun["role"]; annotationId?: string }[]>;
|
|
297
|
-
cursor: number;
|
|
298
|
-
mode: Mode;
|
|
255
|
+
/** Pick the thread pane's body: the pixel prototype, the diff placeholder, the inline editor, or the read-only view. */
|
|
256
|
+
function chooseThreadBody(choice: {
|
|
257
|
+
isPixelPrototype: boolean;
|
|
258
|
+
isDiff: boolean;
|
|
259
|
+
editingBody: boolean;
|
|
260
|
+
prototype: React.ReactNode;
|
|
261
|
+
diffPlaceholder: React.ReactNode;
|
|
262
|
+
editor: React.ReactNode;
|
|
263
|
+
threadView: React.ReactNode;
|
|
299
264
|
}): React.ReactNode {
|
|
300
|
-
|
|
301
|
-
return
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const blockMarks = [...(marks.get(i) ?? [])];
|
|
318
|
-
if (mode.m === "span" && mode.span.dispIdx === i) {
|
|
319
|
-
blockMarks.push({ start: mode.span.start, end: mode.span.end, role: "kspan" });
|
|
320
|
-
}
|
|
321
|
-
const runs = overlayMarks(blockRuns(d, true), blockMarks);
|
|
322
|
-
const lines = wrapRuns(runs, width - marker(d).length);
|
|
323
|
-
return (
|
|
324
|
-
<box key={i} style={{ flexDirection: "column", marginTop: gap }}>
|
|
325
|
-
{lines.map((line, li) => (
|
|
326
|
-
<text key={li} bg={isCursor ? T.cursorBg : undefined}>
|
|
327
|
-
<span fg={isCursor ? T.accent : T.textDim}>{li === 0 ? cursorGlyph(isCursor) : " "}</span>
|
|
328
|
-
<span fg={T.textDim}>{li === 0 ? marker(d) : " ".repeat(marker(d).length)}</span>
|
|
329
|
-
{line.map((r, ri) => (
|
|
330
|
-
<span key={ri} {...runStyle(r, d)}>
|
|
331
|
-
{r.text}
|
|
332
|
-
</span>
|
|
333
|
-
))}
|
|
334
|
-
{li === 0 && d.type !== "same" ? <span fg={tagColor(d)}> [{tagLabel(d)}]</span> : null}
|
|
335
|
-
</text>
|
|
336
|
-
))}
|
|
337
|
-
</box>
|
|
338
|
-
);
|
|
339
|
-
})}
|
|
340
|
-
</scrollbox>
|
|
341
|
-
);
|
|
265
|
+
if (choice.isPixelPrototype) return choice.prototype;
|
|
266
|
+
if (choice.isDiff) return choice.diffPlaceholder;
|
|
267
|
+
if (choice.editingBody) return choice.editor;
|
|
268
|
+
|
|
269
|
+
return choice.threadView;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The author display name for a comment's hover tooltip: the reviewer's own name for their notes, else the resolved collaborator name. */
|
|
273
|
+
function authorLabelResolver(
|
|
274
|
+
ownName: string | undefined,
|
|
275
|
+
participants: Identity[] | undefined,
|
|
276
|
+
overrides: Record<string, string>,
|
|
277
|
+
): (annotation: Annotation) => string | undefined {
|
|
278
|
+
return (annotation) =>
|
|
279
|
+
annotation.author === undefined
|
|
280
|
+
? (ownName ?? "you")
|
|
281
|
+
: resolveDisplayName(annotation.author, participants, overrides);
|
|
342
282
|
}
|
|
343
283
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
284
|
+
/** The Project pane body: the resolved repo's changed files in changes mode, its full tree otherwise. */
|
|
285
|
+
function ProjectPanelBody(props: {
|
|
286
|
+
mode: ProjectPanelMode;
|
|
287
|
+
loadChanges: () => Promise<readonly DiffFileContents[]>;
|
|
288
|
+
loadProjectFiles: () => Promise<string[]>;
|
|
289
|
+
reloadKey: string;
|
|
290
|
+
onOpenChangedFile: (path: string) => void;
|
|
291
|
+
onOpenProjectFile: (path: string) => void;
|
|
292
|
+
commentCounts?: ReadonlyMap<string, number>;
|
|
293
|
+
focused?: boolean;
|
|
294
|
+
theme: Theme;
|
|
354
295
|
}): React.ReactNode {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
// Shiki tokens arrive async; plain verbatim lines render until then
|
|
358
|
-
const [tokens, setTokens] = useState<CodeToken[][] | null>(null);
|
|
296
|
+
const [changes, setChanges] = useState<readonly DiffFileContents[]>([]);
|
|
297
|
+
const loadRef = useRef(props.loadChanges);
|
|
359
298
|
useEffect(() => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
299
|
+
loadRef.current = props.loadChanges;
|
|
300
|
+
});
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
let alive = true;
|
|
303
|
+
|
|
304
|
+
void loadRef.current().then(
|
|
305
|
+
(files) => {
|
|
306
|
+
if (alive) setChanges(files);
|
|
307
|
+
},
|
|
308
|
+
() => {
|
|
309
|
+
if (alive) setChanges([]);
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
|
|
364
313
|
return () => {
|
|
365
|
-
|
|
314
|
+
alive = false;
|
|
366
315
|
};
|
|
367
|
-
}, [
|
|
368
|
-
|
|
316
|
+
}, [props.reloadKey, props.mode]);
|
|
317
|
+
|
|
318
|
+
if (props.mode === "changes") {
|
|
319
|
+
return (
|
|
320
|
+
<ChangesFileTree
|
|
321
|
+
files={changes}
|
|
322
|
+
onSelectFile={props.onOpenChangedFile}
|
|
323
|
+
commentCounts={props.commentCounts}
|
|
324
|
+
focused={props.focused}
|
|
325
|
+
theme={props.theme}
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
369
329
|
return (
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<box
|
|
378
|
-
style={{
|
|
379
|
-
flexDirection: "column",
|
|
380
|
-
backgroundColor: T.elevated,
|
|
381
|
-
paddingLeft: 2,
|
|
382
|
-
paddingRight: 2,
|
|
383
|
-
paddingTop: 1,
|
|
384
|
-
paddingBottom: 1,
|
|
385
|
-
marginLeft: 2,
|
|
386
|
-
}}
|
|
387
|
-
>
|
|
388
|
-
{lines.map((line, li) => (
|
|
389
|
-
<text key={li}>
|
|
390
|
-
{line.length === 0 || (line.length === 1 && line[0]!.content === "") ? (
|
|
391
|
-
" "
|
|
392
|
-
) : (
|
|
393
|
-
line.map((tok, ti) => (
|
|
394
|
-
<span key={ti} fg={tok.color ?? T.textMuted}>
|
|
395
|
-
{tok.content}
|
|
396
|
-
</span>
|
|
397
|
-
))
|
|
398
|
-
)}
|
|
399
|
-
</text>
|
|
400
|
-
))}
|
|
401
|
-
</box>
|
|
402
|
-
</box>
|
|
330
|
+
<ProjectTreeView
|
|
331
|
+
key={props.reloadKey}
|
|
332
|
+
loadFiles={props.loadProjectFiles}
|
|
333
|
+
onSelectFile={props.onOpenProjectFile}
|
|
334
|
+
focused={props.focused}
|
|
335
|
+
theme={props.theme}
|
|
336
|
+
/>
|
|
403
337
|
);
|
|
404
338
|
}
|
|
405
339
|
|
|
406
|
-
/**
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
340
|
+
/** The render tree has left the connecting screen: an error, a session, or the no-thread shell. */
|
|
341
|
+
function usableScreenReached(
|
|
342
|
+
error: string | null,
|
|
343
|
+
session: Thread | null,
|
|
344
|
+
inbox: Thread[] | null,
|
|
345
|
+
): boolean {
|
|
346
|
+
return Boolean(error) || session !== null || inbox !== null;
|
|
412
347
|
}
|
|
413
348
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
<text fg={T.textDim}>no annotations yet</text>
|
|
430
|
-
) : (
|
|
431
|
-
session.annotations.map((a) => <AnnCard key={a.id} a={a} focused={a.id === focusedAnn} />)
|
|
432
|
-
)}
|
|
433
|
-
<box style={{ flexGrow: 1 }} />
|
|
434
|
-
<text fg={session.status === "resolved" ? T.green : T.accent}>
|
|
435
|
-
{session.status === "resolved" ? `resolved: ${session.verdict!.kind.replace("_", " ")}` : `Submit review (${pendingCount}) ⏎`}
|
|
436
|
-
</text>
|
|
437
|
-
</box>
|
|
438
|
-
);
|
|
349
|
+
/** The keybinds dialog content: the thread grammar while the thread view owns the keys. */
|
|
350
|
+
function cheatsheetFor(keyBindings: KeyBindings, threadViewActive: boolean): CheatsheetSection[] {
|
|
351
|
+
const base = keyBindings.cheatsheet();
|
|
352
|
+
|
|
353
|
+
if (!threadViewActive) {
|
|
354
|
+
return base;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return [
|
|
358
|
+
...THREAD_VIEW_CHEATSHEET,
|
|
359
|
+
{ title: "Nav mode · session", entries: sessionCommandEntries() },
|
|
360
|
+
{ title: "Nav mode · diff", entries: diffCommandEntries() },
|
|
361
|
+
{ title: "Nav mode · discussion", entries: curationCommandEntries() },
|
|
362
|
+
{ title: "Nav mode · history", entries: treeCommandEntries() },
|
|
363
|
+
];
|
|
439
364
|
}
|
|
440
365
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
{a.kind.toUpperCase()}
|
|
448
|
-
</text>
|
|
449
|
-
<text fg={T.textDim}> “{truncate(a.anchor.quote, 26)}”</text>
|
|
450
|
-
<text fg={T.textMuted}> {truncate(a.body, 28)}</text>
|
|
451
|
-
</box>
|
|
452
|
-
);
|
|
366
|
+
/** The Threads sidebar opens from the launch layout; a bare launch opens it only on the inbox. */
|
|
367
|
+
function initialThreadsOpen(
|
|
368
|
+
layout: LaunchLayout | undefined,
|
|
369
|
+
sessionId: string | undefined,
|
|
370
|
+
): boolean {
|
|
371
|
+
return layout ? layout.threads : sessionId === undefined;
|
|
453
372
|
}
|
|
454
373
|
|
|
455
|
-
function
|
|
456
|
-
return
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
<text> </text>
|
|
460
|
-
{inbox.length === 0 ? (
|
|
461
|
-
<text fg={T.textDim}>nothing waiting for review</text>
|
|
462
|
-
) : (
|
|
463
|
-
inbox.map((s, i) => (
|
|
464
|
-
<text key={s.id} fg={i === cursor ? T.text : T.textMuted} bg={i === cursor ? T.cursorBg : undefined}>
|
|
465
|
-
{i === cursor ? "▸ " : " "}
|
|
466
|
-
{s.artifact.meta.title ?? s.id} · {s.workspace.branch} · {s.artifact.type}
|
|
467
|
-
</text>
|
|
468
|
-
))
|
|
469
|
-
)}
|
|
470
|
-
<box style={{ flexGrow: 1 }} />
|
|
471
|
-
<text fg={T.textDim}>j/k move · ⏎ open · q quit</text>
|
|
472
|
-
</box>
|
|
473
|
-
);
|
|
374
|
+
function initialFocusedPane(sessionId: string | undefined, zoomed: boolean): FocusPane {
|
|
375
|
+
if (sessionId === undefined) return "threads";
|
|
376
|
+
|
|
377
|
+
return zoomed ? "changes" : "thread";
|
|
474
378
|
}
|
|
475
379
|
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
quote: string;
|
|
483
|
-
onChange: (text: string) => void;
|
|
484
|
-
}): React.ReactNode {
|
|
485
|
-
return (
|
|
486
|
-
<box style={{ height: 2, backgroundColor: T.elevated, flexDirection: "column", paddingLeft: 1 }}>
|
|
487
|
-
<text fg={mode.kind === "suggestion" ? T.green : T.accent}>
|
|
488
|
-
{mode.kind === "suggestion" ? "SUGGEST REPLACEMENT FOR" : "COMMENT ON"} “{truncate(quote, 60)}” · ⏎ save · esc cancel
|
|
489
|
-
</text>
|
|
490
|
-
<input focused value={mode.text} onInput={onChange} />
|
|
491
|
-
</box>
|
|
492
|
-
);
|
|
380
|
+
function bareShellDefersKeys(
|
|
381
|
+
sessionIsNull: boolean,
|
|
382
|
+
overlayIsNone: boolean,
|
|
383
|
+
focusedPane: FocusPane,
|
|
384
|
+
): boolean {
|
|
385
|
+
return sessionIsNull && overlayIsNone && focusedPane !== "threads";
|
|
493
386
|
}
|
|
494
387
|
|
|
495
|
-
function
|
|
496
|
-
|
|
497
|
-
pendingCount,
|
|
498
|
-
onChange,
|
|
499
|
-
}: {
|
|
500
|
-
mode: Extract<Mode, { m: "submit" }>;
|
|
501
|
-
pendingCount: number;
|
|
502
|
-
onChange: (summary: string) => void;
|
|
503
|
-
}): React.ReactNode {
|
|
504
|
-
return (
|
|
505
|
-
<box style={{ height: 2, backgroundColor: T.elevated, flexDirection: "column", paddingLeft: 1 }}>
|
|
506
|
-
<text>
|
|
507
|
-
<span fg={T.textDim}>verdict ←/→ : </span>
|
|
508
|
-
{VERDICTS.map((v, i) => (
|
|
509
|
-
<span key={v} fg={v === mode.verdict ? verdictColor(v) : T.textDim}>
|
|
510
|
-
{v === mode.verdict ? `[${VERDICT_LABEL[v]}]` : ` ${VERDICT_LABEL[v]} `}
|
|
511
|
-
{i < VERDICTS.length - 1 ? " " : ""}
|
|
512
|
-
</span>
|
|
513
|
-
))}
|
|
514
|
-
<span fg={T.textDim}> · Submit review ({pendingCount}) on ⏎ · esc cancel</span>
|
|
515
|
-
</text>
|
|
516
|
-
<input focused value={mode.summary} onInput={onChange} placeholder="summary for the agent (optional)" />
|
|
517
|
-
</box>
|
|
518
|
-
);
|
|
388
|
+
function surfaceSuspended(base: boolean, focusedPane: FocusPane, pane: FocusPane): boolean {
|
|
389
|
+
return base || focusedPane !== pane;
|
|
519
390
|
}
|
|
520
391
|
|
|
521
|
-
function
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
for (const a of annotations) {
|
|
537
|
-
const idx = rows.findIndex((r) => r.text === a.anchor.quote && (r.t === "ctx" || r.t === "add" || r.t === "del"));
|
|
538
|
-
if (idx !== -1) annotated.set(idx, a);
|
|
392
|
+
export function threadsNavHandled(params: {
|
|
393
|
+
focusedPane: FocusPane;
|
|
394
|
+
quiet: boolean;
|
|
395
|
+
key: { name: string };
|
|
396
|
+
count: number;
|
|
397
|
+
setInboxCursor: (update: (cursor: number) => number) => void;
|
|
398
|
+
openSession: () => void;
|
|
399
|
+
}): boolean {
|
|
400
|
+
const { focusedPane, quiet, key, count } = params;
|
|
401
|
+
|
|
402
|
+
if (focusedPane !== "threads" || !quiet || count === 0) return false;
|
|
403
|
+
if (key.name === "j" || key.name === "down") {
|
|
404
|
+
params.setInboxCursor((cursor) => Math.min(cursor + 1, count - 1));
|
|
405
|
+
|
|
406
|
+
return true;
|
|
539
407
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{visible.map((row, vi) => {
|
|
543
|
-
const i = windowStart + vi;
|
|
544
|
-
const isCursor = i === cursor;
|
|
545
|
-
const ann = annotated.get(i);
|
|
546
|
-
const gutter = row.t === "del" ? (row.oldLine?.toString() ?? "") : (row.newLine?.toString() ?? "");
|
|
547
|
-
const sign = row.t === "add" ? "+" : row.t === "del" ? "-" : " ";
|
|
548
|
-
if (row.t === "file") {
|
|
549
|
-
return (
|
|
550
|
-
<text key={i} fg={T.text} bg={isCursor ? T.cursorBg : T.panel}>
|
|
551
|
-
{isCursor ? "▎" : " "}■ {row.text}
|
|
552
|
-
</text>
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
if (row.t === "hunk") {
|
|
556
|
-
return (
|
|
557
|
-
<text key={i} fg={T.blue} bg={isCursor ? T.cursorBg : undefined}>
|
|
558
|
-
{isCursor ? "▎" : " "}{row.text}
|
|
559
|
-
</text>
|
|
560
|
-
);
|
|
561
|
-
}
|
|
562
|
-
const fg = row.t === "add" ? T.insFg : row.t === "del" ? T.delFg : T.textMuted;
|
|
563
|
-
return (
|
|
564
|
-
<box key={i} style={{ flexDirection: "column" }}>
|
|
565
|
-
<text bg={isCursor ? T.cursorBg : ann ? T.markCommentBg : undefined}>
|
|
566
|
-
<span fg={T.textDim}>{isCursor ? "▎" : " "}{gutter.padStart(4)} </span>
|
|
567
|
-
<span fg={fg}>{sign}{row.text}</span>
|
|
568
|
-
</text>
|
|
569
|
-
{ann ? (
|
|
570
|
-
<text>
|
|
571
|
-
<span fg={T.textDim}>{" "}</span>
|
|
572
|
-
<span fg={ann.id === focusedAnn ? T.text : T.accent}>◆ {truncate(ann.body, 70)}</span>
|
|
573
|
-
</text>
|
|
574
|
-
) : null}
|
|
575
|
-
</box>
|
|
576
|
-
);
|
|
577
|
-
})}
|
|
578
|
-
</box>
|
|
579
|
-
);
|
|
580
|
-
}
|
|
408
|
+
if (key.name === "k" || key.name === "up") {
|
|
409
|
+
params.setInboxCursor((cursor) => Math.max(cursor - 1, 0));
|
|
581
410
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
returnsTo,
|
|
587
|
-
}: {
|
|
588
|
-
verdict: VerdictKind;
|
|
589
|
-
completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
|
|
590
|
-
/** Latest status line (e.g. the vault-export path) stays visible here. */
|
|
591
|
-
status: string;
|
|
592
|
-
/** Where focus goes on close (the agent's herdr pane), when known. */
|
|
593
|
-
returnsTo?: string;
|
|
594
|
-
}): React.ReactNode {
|
|
595
|
-
const approved = verdict === "approve";
|
|
596
|
-
return (
|
|
597
|
-
<box
|
|
598
|
-
style={{
|
|
599
|
-
width: "100%",
|
|
600
|
-
height: "100%",
|
|
601
|
-
backgroundColor: T.bg,
|
|
602
|
-
flexDirection: "column",
|
|
603
|
-
justifyContent: "center",
|
|
604
|
-
alignItems: "center",
|
|
605
|
-
}}
|
|
606
|
-
>
|
|
607
|
-
<text fg={approved ? T.green : T.accent}>{approved ? "✓ review approved" : "✎ feedback sent"}</text>
|
|
608
|
-
<text> </text>
|
|
609
|
-
<text fg={T.text}>The agent has your {approved ? "approval" : "feedback"} and is unblocked.</text>
|
|
610
|
-
{status ? <text fg={T.textDim}>{status}</text> : null}
|
|
611
|
-
<text> </text>
|
|
612
|
-
{completion.phase === "counting" ? (
|
|
613
|
-
<text fg={T.textDim}>
|
|
614
|
-
closing in {completion.remaining}s{returnsTo ? ` - returning to ${returnsTo}` : ""} · ⏎ now · esc stay
|
|
615
|
-
</text>
|
|
616
|
-
) : (
|
|
617
|
-
<text fg={T.textDim}>⏎ close · a always close after submit (3s) · esc stay</text>
|
|
618
|
-
)}
|
|
619
|
-
</box>
|
|
620
|
-
);
|
|
621
|
-
}
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
if (key.name === "return" || key.name === "enter") {
|
|
414
|
+
params.openSession();
|
|
622
415
|
|
|
623
|
-
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
624
418
|
|
|
625
|
-
|
|
626
|
-
if (d.kind === "li") return "- ";
|
|
627
|
-
if (d.kind === "oli") return `${d.oliNum ?? 1}. `;
|
|
628
|
-
if (d.kind === "quote") return "▏ ";
|
|
629
|
-
return "";
|
|
419
|
+
return false;
|
|
630
420
|
}
|
|
631
421
|
|
|
632
|
-
function
|
|
633
|
-
|
|
422
|
+
export function paneCycleRequested(
|
|
423
|
+
key: { name: string },
|
|
424
|
+
overlay: string,
|
|
425
|
+
menuOwnsKeyboard: boolean,
|
|
426
|
+
threadComposing: boolean,
|
|
427
|
+
): boolean {
|
|
428
|
+
return key.name === "tab" && overlay === "none" && !menuOwnsKeyboard && !threadComposing;
|
|
634
429
|
}
|
|
635
430
|
|
|
636
|
-
function
|
|
637
|
-
|
|
638
|
-
|
|
431
|
+
export function visiblePanes(
|
|
432
|
+
sidebarOpen: boolean,
|
|
433
|
+
threadShown: boolean,
|
|
434
|
+
changesOpen: boolean,
|
|
435
|
+
projectOpen: boolean,
|
|
436
|
+
): FocusPane[] {
|
|
437
|
+
const panes: FocusPane[] = [];
|
|
438
|
+
|
|
439
|
+
if (sidebarOpen) panes.push("threads");
|
|
440
|
+
if (threadShown) panes.push("thread");
|
|
441
|
+
if (changesOpen) panes.push("changes");
|
|
442
|
+
if (projectOpen) panes.push("project");
|
|
639
443
|
|
|
640
|
-
|
|
641
|
-
return d.type === "del" ? T.red : d.type === "add" ? T.green : T.accent;
|
|
444
|
+
return panes;
|
|
642
445
|
}
|
|
643
446
|
|
|
644
|
-
function
|
|
645
|
-
|
|
447
|
+
export function nextFocusPane(
|
|
448
|
+
current: FocusPane,
|
|
449
|
+
panes: FocusPane[],
|
|
450
|
+
backward: boolean,
|
|
451
|
+
): FocusPane {
|
|
452
|
+
if (panes.length === 0) return current;
|
|
453
|
+
const index = panes.indexOf(current);
|
|
454
|
+
const step = backward ? -1 : 1;
|
|
455
|
+
|
|
456
|
+
return panes[(index + step + panes.length) % panes.length] ?? current;
|
|
646
457
|
}
|
|
647
458
|
|
|
648
|
-
|
|
649
|
-
|
|
459
|
+
/** The panes that can own the keyboard: a diff's Thread pane is an inert placeholder, so it is out. */
|
|
460
|
+
function navigableFocusPanes(
|
|
461
|
+
sidebarOpen: boolean,
|
|
462
|
+
zoomed: boolean,
|
|
463
|
+
isDiff: boolean,
|
|
464
|
+
changesOpen: boolean,
|
|
465
|
+
projectOpen: boolean,
|
|
466
|
+
): FocusPane[] {
|
|
467
|
+
return visiblePanes(sidebarOpen, !zoomed && !isDiff, changesOpen, projectOpen);
|
|
650
468
|
}
|
|
651
469
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return { fg: T.insFg };
|
|
658
|
-
case "del":
|
|
659
|
-
return { fg: T.delFg };
|
|
660
|
-
case "mark-comment":
|
|
661
|
-
return { fg: T.text, bg: T.markCommentBg };
|
|
662
|
-
case "mark-suggestion":
|
|
663
|
-
return { fg: T.text, bg: T.markSuggestionBg };
|
|
664
|
-
case "mark-focus":
|
|
665
|
-
return { fg: T.accentInk, bg: T.accent };
|
|
666
|
-
case "kspan":
|
|
667
|
-
return { fg: T.accentInk, bg: T.accent };
|
|
668
|
-
default:
|
|
669
|
-
return { fg: struck ? T.red : (headingFg ?? (d.kind === "code" ? T.textMuted : T.text)) };
|
|
670
|
-
}
|
|
470
|
+
/** The layout hid or inert-ed the focused pane: fall back to the first navigable one, else stay put. */
|
|
471
|
+
export function reconciledFocus(navigable: FocusPane[], focusedPane: FocusPane): FocusPane | null {
|
|
472
|
+
if (navigable.length === 0 || navigable.includes(focusedPane)) return null;
|
|
473
|
+
|
|
474
|
+
return navigable[0] ?? null;
|
|
671
475
|
}
|
|
672
476
|
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
|
|
477
|
+
export function App({
|
|
478
|
+
home,
|
|
479
|
+
sessionId,
|
|
480
|
+
cwd,
|
|
481
|
+
readOnly = false,
|
|
482
|
+
onExit,
|
|
483
|
+
onReady,
|
|
484
|
+
clock,
|
|
485
|
+
openClient,
|
|
486
|
+
shareTransport,
|
|
487
|
+
role = "owner",
|
|
488
|
+
selfAuthor,
|
|
489
|
+
appearance = "dark",
|
|
490
|
+
layout,
|
|
491
|
+
servedArtifact,
|
|
492
|
+
}: AppProps): React.ReactNode {
|
|
493
|
+
const { observer, isOwner } = computeRoleCapabilities(readOnly, role);
|
|
494
|
+
const controller = useMemo(
|
|
495
|
+
() =>
|
|
496
|
+
createReviewController({
|
|
497
|
+
home,
|
|
498
|
+
sessionId,
|
|
499
|
+
cwd,
|
|
500
|
+
readOnly: observer,
|
|
501
|
+
onExit,
|
|
502
|
+
clock,
|
|
503
|
+
openClient,
|
|
504
|
+
shareTransport,
|
|
505
|
+
servedArtifact,
|
|
506
|
+
}),
|
|
507
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
508
|
+
[home, sessionId],
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
useEffect(() => {
|
|
512
|
+
controller.connect();
|
|
513
|
+
|
|
514
|
+
return () => controller.close();
|
|
515
|
+
}, [controller]);
|
|
516
|
+
// stable across renders so the memoized PrototypeContentView is not
|
|
517
|
+
// re-rendered by unrelated App state (status ticks, a rail-width drag)
|
|
518
|
+
const onCommentPrototype = useCallback(
|
|
519
|
+
(element: PrototypeElement, body: string) =>
|
|
520
|
+
controller.annotatePrototype(element.selector, element.quote, body),
|
|
521
|
+
[controller],
|
|
522
|
+
);
|
|
523
|
+
const { session, inbox, status, toast, error, completion, editOrphanCount, walk } =
|
|
524
|
+
useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
525
|
+
|
|
526
|
+
// A shared plan you own polls for collaborator notes while it is open; the
|
|
527
|
+
// merge refreshes through the normal event path. Stops on leave.
|
|
528
|
+
const threadShared = isSharedThread(session);
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (!isOwner || !threadShared) return;
|
|
531
|
+
|
|
532
|
+
return controller.startShareSync();
|
|
533
|
+
}, [isOwner, session?.id, threadShared, controller]);
|
|
534
|
+
const renderer = useRenderer();
|
|
535
|
+
const { width: terminalWidth } = useTerminalDimensions();
|
|
536
|
+
|
|
537
|
+
// ── view state ──────────────────────────────
|
|
538
|
+
const [cursor, setCursor] = useState(0);
|
|
539
|
+
const [inboxCursor, setInboxCursor] = useState(0);
|
|
540
|
+
// Projects and Threads grouping; ordered is the flat sequence the inbox cursor walks
|
|
541
|
+
// pinned threads are client-local view state, seeded from the user config and
|
|
542
|
+
// persisted on toggle; a pin lifts the thread into the sidebar's Pinned section
|
|
543
|
+
const [pinnedIds, setPinnedIds] = useState<Set<string>>(new Set());
|
|
544
|
+
const grouped = useMemo(() => groupInbox(inbox ?? [], pinnedIds), [inbox, pinnedIds]);
|
|
545
|
+
const togglePin = (id: string): void =>
|
|
546
|
+
setPinnedIds((current) => {
|
|
547
|
+
const next = new Set(current);
|
|
548
|
+
|
|
549
|
+
if (next.has(id)) next.delete(id);
|
|
550
|
+
else next.add(id);
|
|
551
|
+
persistPins([...next]);
|
|
552
|
+
|
|
553
|
+
return next;
|
|
554
|
+
});
|
|
555
|
+
// the left Projects and Threads column; collapsed by default per the context
|
|
556
|
+
// matrix (the thread owns the width), toggled open to jump between threads
|
|
557
|
+
// one sidebar-open state across the no-thread shell and the thread view, so
|
|
558
|
+
// picking a thread keeps the sidebar as it was. It opens when the app lands with
|
|
559
|
+
// nothing selected (pick a thread) and stays collapsed on a direct thread open,
|
|
560
|
+
// where the thread owns the width.
|
|
561
|
+
const [sidebarOpen, setSidebarOpen] = useState(() => initialThreadsOpen(layout, sessionId));
|
|
562
|
+
// the Changes + Project right region and its editor grid (tabs, splits, zoom)
|
|
563
|
+
const workbench = useChangesWorkbench({ layout });
|
|
564
|
+
const [focusedPane, setFocusedPane] = useState<FocusPane>(() =>
|
|
565
|
+
initialFocusedPane(sessionId, workbench.zoomed),
|
|
566
|
+
);
|
|
567
|
+
// only the session view persists from here; the bare shell owns its own (NoThreadShell), so this
|
|
568
|
+
// workbench stays inactive with no session and never clobbers what the shell saved
|
|
569
|
+
useRememberLayout(
|
|
570
|
+
layout,
|
|
571
|
+
session !== null,
|
|
572
|
+
sidebarOpen,
|
|
573
|
+
workbench.changesOpen,
|
|
574
|
+
workbench.projectOpen,
|
|
575
|
+
workbench.zoomed,
|
|
576
|
+
);
|
|
577
|
+
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
578
|
+
// the top-left settings gear drop-down and the centered dialog it opens
|
|
579
|
+
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
580
|
+
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
|
581
|
+
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
582
|
+
// unified or side-by-side diff; split only lays out when the Changes pane is zoomed
|
|
583
|
+
const [diffView, setDiffView] = useState<DiffViewMode>("split");
|
|
584
|
+
const [defaultMessage, setDefaultMessage] = useState<MessageOutcome>("approved");
|
|
585
|
+
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | undefined>(undefined);
|
|
586
|
+
const [selectedCurationId, setSelectedCurationId] = useState<string | undefined>(undefined);
|
|
587
|
+
const [railTab, setRailTab] = useState<RailTab>("review");
|
|
588
|
+
// the tree row the reviewer stands on in the session tree
|
|
589
|
+
const [selectedEntryId, setSelectedEntryId] = useState<string | undefined>(undefined);
|
|
590
|
+
// ~2s focus pulse on the document highlight when a card is activated
|
|
591
|
+
const [pulsedAnnotationId, setPulsedAnnotationId] = useState<string | null>(null);
|
|
592
|
+
const pulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
593
|
+
// live mirror of overlay input text: refs commit synchronously, so the
|
|
594
|
+
// RETURN handler never reads a stale value mid-typing
|
|
595
|
+
const liveInput = useRef("");
|
|
596
|
+
// keymap from layered config; the loaded theme swaps the provider value
|
|
597
|
+
const keysRef = useRef(DEFAULT_KEYS);
|
|
598
|
+
const keyBindings = useMemo(() => new KeyBindings(DEFAULT_KEYS), []);
|
|
599
|
+
const [theme, setTheme] = useState(() => themeForName(DEFAULT_THEME_NAME, appearance));
|
|
600
|
+
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
|
|
601
|
+
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
602
|
+
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
603
|
+
const [identity, setIdentity] = useState<IdentityConfig>({ provider: "typed" });
|
|
604
|
+
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
605
|
+
const [skills, setSkills] = useState<SlashItem[]>([]);
|
|
606
|
+
const paletteNames = useMemo(
|
|
607
|
+
() => new Set(mergeSlashItems(slashItemsFrom(quickActions), skills).map((item) => item.name)),
|
|
608
|
+
[quickActions, skills],
|
|
609
|
+
);
|
|
610
|
+
// opt-in: render a prototype as a kitty pixel mockup instead of the markdown doc
|
|
611
|
+
const [prototypePixels, setPrototypePixels] = useState(false);
|
|
612
|
+
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const config = loadConfig({ repoRoot: session?.workspace.repoRoot });
|
|
615
|
+
|
|
616
|
+
setPrototypePixels(config.experimental.prototypePixels);
|
|
617
|
+
keysRef.current = config.keys;
|
|
618
|
+
keyBindings.setKeys(config.keys);
|
|
619
|
+
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
620
|
+
setThemeName(config.ui.theme);
|
|
621
|
+
setThemeOverrides(config.themeOverrides);
|
|
622
|
+
setAuthorNames(config.authors);
|
|
623
|
+
setIdentity(config.identity);
|
|
624
|
+
setQuickActions(config.actions);
|
|
625
|
+
setSkills(loadSkills(config.skillsPath));
|
|
626
|
+
setAutoClose(config.ui.autoClose);
|
|
627
|
+
setDiffView(config.ui.diffView);
|
|
628
|
+
setDefaultMessage(config.ui.defaultMessage);
|
|
629
|
+
setPinnedIds(new Set(config.ui.pins));
|
|
630
|
+
controller.applyConfig(config);
|
|
631
|
+
}, [session?.workspace.repoRoot, controller, keyBindings, appearance]);
|
|
632
|
+
useEffect(
|
|
633
|
+
() => () => {
|
|
634
|
+
if (pulseTimer.current) clearTimeout(pulseTimer.current);
|
|
635
|
+
},
|
|
636
|
+
[],
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (!toast) return;
|
|
641
|
+
const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
|
|
642
|
+
|
|
643
|
+
return () => clearTimeout(timer);
|
|
644
|
+
}, [toast, controller]);
|
|
645
|
+
|
|
646
|
+
// First open of a share: ask the collaborator for a display name once, unless
|
|
647
|
+
// a past visit already recorded one. esc skips and their notes read anonymous.
|
|
648
|
+
const promptedSelfRef = useRef(false);
|
|
649
|
+
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
if (promptedSelfRef.current || role !== "collaborator" || !selfAuthor || !session) return;
|
|
652
|
+
promptedSelfRef.current = true;
|
|
653
|
+
const known = session.participants?.find((participant) => participant.id === selfAuthor)?.name;
|
|
654
|
+
|
|
655
|
+
if (!known) setMode({ type: "nameSelf", text: "" });
|
|
656
|
+
}, [role, selfAuthor, session]);
|
|
657
|
+
|
|
658
|
+
// Bumped on every identity change so a slow GitHub sync that lands after a newer rename is discarded.
|
|
659
|
+
const identityGenerationRef = useRef(0);
|
|
660
|
+
const applyIdentity = (next: IdentityConfig): void => {
|
|
661
|
+
identityGenerationRef.current += 1;
|
|
662
|
+
setIdentity(next);
|
|
663
|
+
try {
|
|
664
|
+
persistIdentity(next);
|
|
665
|
+
} catch {
|
|
666
|
+
controller.setStatus("could not save identity to config");
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const syncGithubIdentity = (): void => {
|
|
670
|
+
const generation = (identityGenerationRef.current += 1);
|
|
671
|
+
void resolveGithubIdentity().then((github) => {
|
|
672
|
+
if (identityGenerationRef.current !== generation) return;
|
|
673
|
+
if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
|
|
674
|
+
const name = github.name?.trim() || github.login;
|
|
675
|
+
|
|
676
|
+
applyIdentity({ name, provider: "github" });
|
|
677
|
+
controller.setStatus(`identity synced from GitHub - ${name}`);
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
682
|
+
const {
|
|
683
|
+
settingsNav,
|
|
684
|
+
settingsCategories,
|
|
685
|
+
settingsValues,
|
|
686
|
+
cycleSetting,
|
|
687
|
+
handleSettingsKey,
|
|
688
|
+
onCategorySelect,
|
|
689
|
+
openSettings,
|
|
690
|
+
} = useSettingsDialog({
|
|
691
|
+
theme,
|
|
692
|
+
appearance,
|
|
693
|
+
autoClose,
|
|
694
|
+
setAutoClose,
|
|
695
|
+
diffView,
|
|
696
|
+
setDiffView,
|
|
697
|
+
themeName,
|
|
698
|
+
setThemeName,
|
|
699
|
+
themeOverrides,
|
|
700
|
+
setTheme,
|
|
701
|
+
quickActions,
|
|
702
|
+
setQuickActions,
|
|
703
|
+
setMenuDialog,
|
|
704
|
+
identityName: identity.name,
|
|
705
|
+
identityProvider: identity.provider,
|
|
706
|
+
onSyncGithubIdentity: syncGithubIdentity,
|
|
707
|
+
onRenameDisplayName: () => {
|
|
708
|
+
setMenuDialog(null);
|
|
709
|
+
setMode({ type: "renameSelf", text: identity.name ?? "" });
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// ── derived view model ──────────────────────
|
|
714
|
+
const display = controller.display();
|
|
715
|
+
const rows = controller.rows();
|
|
716
|
+
const rejectedRows = controller.rejectedRows();
|
|
717
|
+
const marks = useMemo(
|
|
718
|
+
() =>
|
|
719
|
+
session
|
|
720
|
+
? marksByDisplay(
|
|
721
|
+
// the plan thread shows only artifact notes; Changes-diff notes render on that surface
|
|
722
|
+
session.annotations.filter(
|
|
723
|
+
(annotation) => annotationTarget(annotation).kind === "artifact",
|
|
724
|
+
),
|
|
725
|
+
display,
|
|
726
|
+
pulsedAnnotationId ?? undefined,
|
|
727
|
+
)
|
|
728
|
+
: new Map<number, Mark[]>(),
|
|
729
|
+
[session, display, pulsedAnnotationId],
|
|
730
|
+
);
|
|
731
|
+
/** Annotation ids whose anchor resolved against the working copy. */
|
|
732
|
+
const resolvedIds = useMemo(() => {
|
|
733
|
+
const ids = new Set<string>();
|
|
734
|
+
|
|
735
|
+
for (const blockMarks of marks.values()) {
|
|
736
|
+
for (const mark of blockMarks) if (mark.annotationId) ids.add(mark.annotationId);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return ids;
|
|
740
|
+
}, [marks]);
|
|
741
|
+
const { isDiff, isPrototype, resolved } = deriveReviewFlags(session);
|
|
742
|
+
// a prototype is a markdown design doc by default; only the opt-in experimental
|
|
743
|
+
// pixel mode (flag on + an HTML entry) renders it as a kitty mockup instead
|
|
744
|
+
const isPixelPrototype = isPixelPrototypeMode(
|
|
745
|
+
isPrototype,
|
|
746
|
+
prototypePixels,
|
|
747
|
+
session?.artifact.meta.prototypePath,
|
|
748
|
+
);
|
|
749
|
+
// comments per changed-file path, for the changed-files tree and tab badges
|
|
750
|
+
const diffCommentCounts = useMemo(
|
|
751
|
+
() => (session && isDiff ? commentCountsByFile(session, rows) : undefined),
|
|
752
|
+
[session, isDiff, rows],
|
|
753
|
+
);
|
|
754
|
+
// entering a diff opens the right region in changed-files mode; a plan or reply opens it closed
|
|
755
|
+
workbench.syncSession(session?.id, isDiff);
|
|
756
|
+
// plans, replies, and the default (markdown) prototype open in the thread view, diffs in the
|
|
757
|
+
// diff sheet: both drive the shared annotation surface; only the pixel prototype keeps the keymap
|
|
758
|
+
const threadViewActive = session !== null && !isPixelPrototype;
|
|
759
|
+
const [threadComposing, setThreadComposing] = useState(false);
|
|
760
|
+
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
761
|
+
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
762
|
+
// inline body edit: the markdown editor owns the thread pane and all keys while open
|
|
763
|
+
const canEditBody = canEditThreadBody(threadViewActive, isDiff, isOwner, resolved);
|
|
764
|
+
const bodyEditing = useThreadBodyEditing({
|
|
765
|
+
controller,
|
|
766
|
+
sessionId: session?.id,
|
|
767
|
+
canEdit: canEditBody,
|
|
768
|
+
});
|
|
769
|
+
// sort position per annotation so the rail interleaves annotation and removal
|
|
770
|
+
// cards in one line-ordered stack: a diff row carries its blockIndex; a plan
|
|
771
|
+
// annotation resolves to the display index it marked
|
|
772
|
+
// ── the guided walk's view model ────────────
|
|
773
|
+
const walkFileList = controller.files();
|
|
774
|
+
const walking = isWalking(isDiff, walk);
|
|
775
|
+
const viewedPaths = useMemo(() => new Set(session?.viewedPaths ?? []), [session]);
|
|
776
|
+
|
|
777
|
+
// ── selection symmetry: one selected id, both sides ──
|
|
778
|
+
const selectCardFromDocument = (annotationId: string): void => {
|
|
779
|
+
setFocusedAnnotationId(annotationId);
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const openCardEdit = (annotationId: string): void => {
|
|
783
|
+
if (observer) return controller.setStatus("observer - read-only");
|
|
784
|
+
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
785
|
+
|
|
786
|
+
if (!annotation) return;
|
|
787
|
+
// a collaborator's note is theirs to word: activating it (click or e) renames
|
|
788
|
+
// the author rather than editing the body the planner does not own
|
|
789
|
+
if (annotation.author) {
|
|
790
|
+
setFocusedAnnotationId(annotationId);
|
|
791
|
+
|
|
792
|
+
return void setMode({
|
|
793
|
+
type: "rename",
|
|
794
|
+
authorId: annotation.author,
|
|
795
|
+
text: authorNames[annotation.author] ?? "",
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
799
|
+
liveInput.current = annotation.body;
|
|
800
|
+
setMode({ type: "railEdit", id: annotation.id, text: annotation.body });
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// a markdown thread edits inline; a diff is not markdown, so it keeps the external-editor hand-off
|
|
804
|
+
const openBodyEditor = (): void => {
|
|
805
|
+
if (canEditBody) {
|
|
806
|
+
bodyEditing.openEditor();
|
|
807
|
+
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
renderer?.suspend();
|
|
811
|
+
try {
|
|
812
|
+
controller.edit();
|
|
813
|
+
} finally {
|
|
814
|
+
renderer?.resume();
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// ── keyboard grammar: build state, reduce, dispatch ──
|
|
819
|
+
const dispatch = createIntentDispatch({
|
|
820
|
+
controller,
|
|
821
|
+
onExit,
|
|
822
|
+
isDiff,
|
|
823
|
+
display,
|
|
824
|
+
rows,
|
|
825
|
+
cursor,
|
|
826
|
+
inbox: inbox === null ? null : grouped.ordered,
|
|
827
|
+
inboxCursor,
|
|
828
|
+
mode,
|
|
829
|
+
session,
|
|
830
|
+
defaultMessage,
|
|
831
|
+
focusedAnnotationId,
|
|
832
|
+
selectedCurationId,
|
|
833
|
+
railTab,
|
|
834
|
+
selectedEntryId,
|
|
835
|
+
authorNames,
|
|
836
|
+
quickActions,
|
|
837
|
+
renameAuthor: (id: string, name: string) => {
|
|
838
|
+
persistAuthorName(id, name);
|
|
839
|
+
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
840
|
+
},
|
|
841
|
+
renameThread: (id: string, title: string) => controller.renameSession(id, title),
|
|
842
|
+
setLocalIdentityName: (name: string) => {
|
|
843
|
+
applyIdentity(name ? { name, provider: "typed" } : { provider: "typed" });
|
|
844
|
+
},
|
|
845
|
+
liveInput,
|
|
846
|
+
setCursor,
|
|
847
|
+
setInboxCursor,
|
|
848
|
+
setMode,
|
|
849
|
+
setRailTab,
|
|
850
|
+
setSelectedEntryId,
|
|
851
|
+
setFocusedAnnotationId,
|
|
852
|
+
setSelectedCurationId,
|
|
853
|
+
setPulsedAnnotationId,
|
|
854
|
+
selectCardFromDocument,
|
|
855
|
+
openBodyEditor,
|
|
856
|
+
openCardEdit,
|
|
857
|
+
toggleDiffView: () => {
|
|
858
|
+
const next: DiffViewMode = diffView === "stacked" ? "split" : "stacked";
|
|
859
|
+
|
|
860
|
+
setDiffView(next);
|
|
861
|
+
persistDiffView(next);
|
|
862
|
+
// every toggle names the new mode; picking split on a narrow pane also says it needs the wide layout
|
|
863
|
+
if (next === "stacked") controller.setStatus("stacked diff");
|
|
864
|
+
else if (workbench.zoomed) controller.setStatus("split diff");
|
|
865
|
+
else controller.setStatus("split diff shows when zoomed");
|
|
866
|
+
},
|
|
867
|
+
openShareDialog: () => {
|
|
868
|
+
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
869
|
+
shareDialogStore.getState().reset();
|
|
870
|
+
setShareDialogOpen(true);
|
|
871
|
+
},
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
const overlay = resolveOverlay(mode, completion.phase, walking);
|
|
875
|
+
|
|
876
|
+
const menuControl = useMenuControlState();
|
|
877
|
+
const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
|
|
878
|
+
// an overlay (submit, walk, prompt, confirm, share choice), the manage-access
|
|
879
|
+
// dialog, or the menu takes the keyboard from the thread view; the view suspends
|
|
880
|
+
// its own grammar meanwhile so typing lands in the dialog, not the thread
|
|
881
|
+
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay, shareDialogOpen);
|
|
882
|
+
|
|
883
|
+
const navigablePanes = navigableFocusPanes(
|
|
884
|
+
sidebarOpen,
|
|
885
|
+
workbench.zoomed,
|
|
886
|
+
isDiff,
|
|
887
|
+
workbench.changesOpen,
|
|
888
|
+
workbench.projectOpen,
|
|
889
|
+
);
|
|
890
|
+
useEffect(() => {
|
|
891
|
+
// the bare shell drives its panes from its own workbench, so only reconcile against this one
|
|
892
|
+
if (!session) return;
|
|
893
|
+
const next = reconciledFocus(navigablePanes, focusedPane);
|
|
894
|
+
|
|
895
|
+
if (next) setFocusedPane(next);
|
|
896
|
+
}, [session, navigablePanes, focusedPane]);
|
|
897
|
+
const cyclePanes = (backward: boolean): void =>
|
|
898
|
+
setFocusedPane((current) => nextFocusPane(current, navigablePanes, backward));
|
|
899
|
+
const runNavCommand = (key: { name: string; shift?: boolean }): boolean => {
|
|
900
|
+
const intent = resolveNavKey(key, {
|
|
901
|
+
isOwner,
|
|
902
|
+
resolved,
|
|
903
|
+
treeActive: railTab === "tree",
|
|
904
|
+
isDiff,
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (!intent) return false;
|
|
908
|
+
dispatch(intent);
|
|
909
|
+
|
|
910
|
+
return true;
|
|
911
|
+
};
|
|
912
|
+
// clicking a sidebar thread moves the cursor onto it too, so the row shows its selected backdrop at once
|
|
913
|
+
const openThread = (id: string): void => {
|
|
914
|
+
// save the open editor into the leaving thread's working copy before the switch, so a click away never drops edits
|
|
915
|
+
if (bodyEditing.editing) bodyEditing.requestExit();
|
|
916
|
+
const index = grouped.ordered.findIndex((thread) => thread.id === id);
|
|
917
|
+
|
|
918
|
+
if (index >= 0) setInboxCursor(() => index);
|
|
919
|
+
controller.open(id);
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const threadSurfaceHandledKey = (key: KeyEvent): boolean => {
|
|
923
|
+
if (!threadViewActive || threadViewSuspended) return false;
|
|
924
|
+
if (!threadComposing) {
|
|
925
|
+
const chord = resolveSessionChord(key, { isOwner, resolved });
|
|
926
|
+
|
|
927
|
+
if (chord) dispatch(chord);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return true;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
useKeyboard((key) => {
|
|
934
|
+
if (quitKeyHandled(key, onExit)) return;
|
|
935
|
+
// the inline body editor owns the pane and every key while open
|
|
936
|
+
if (bodyEditing.editing) return;
|
|
937
|
+
// the share dialog owns its own keys while open; the shell grammar stands down
|
|
938
|
+
if (shareDialogOpen) return;
|
|
939
|
+
if (menuModalHandled(menuControl, key)) return;
|
|
940
|
+
if (paneCycleRequested(key, overlay, menuOwnsKeyboard, threadComposing)) {
|
|
941
|
+
return cyclePanes(Boolean(key.shift));
|
|
942
|
+
}
|
|
943
|
+
if (
|
|
944
|
+
threadsNavHandled({
|
|
945
|
+
focusedPane,
|
|
946
|
+
quiet: overlay === "none" && !menuOwnsKeyboard && !threadComposing,
|
|
947
|
+
key,
|
|
948
|
+
count: grouped.ordered.length,
|
|
949
|
+
setInboxCursor,
|
|
950
|
+
openSession: () => dispatch({ type: "openSession" }),
|
|
951
|
+
})
|
|
952
|
+
)
|
|
953
|
+
return;
|
|
954
|
+
if (threadSurfaceHandledKey(key)) return;
|
|
955
|
+
// A compose textarea owns the keyboard while open: let it receive the typed note instead of the
|
|
956
|
+
// global keymap acting on each letter (the prototype, and the bare-shell welcome playground).
|
|
957
|
+
if (prototypeComposing || welcomeComposing) return;
|
|
958
|
+
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
959
|
+
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
960
|
+
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
961
|
+
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
962
|
+
if (toastDismissRequested(toast !== null, key, overlay, mode.type === "span"))
|
|
963
|
+
return controller.dismissToast();
|
|
964
|
+
if (bareShellDefersKeys(session === null, overlay === "none", focusedPane)) return;
|
|
965
|
+
const state = buildKeyState({
|
|
966
|
+
keys: keysRef.current,
|
|
967
|
+
observer,
|
|
968
|
+
isOwner,
|
|
969
|
+
overlay,
|
|
970
|
+
session,
|
|
971
|
+
isDiff,
|
|
972
|
+
mode,
|
|
973
|
+
resolved,
|
|
974
|
+
inbox,
|
|
975
|
+
focusedAnnotationId,
|
|
976
|
+
walk,
|
|
977
|
+
walkFileList,
|
|
978
|
+
rows,
|
|
979
|
+
cursor,
|
|
980
|
+
display,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
keyBindings.setContext({
|
|
984
|
+
overlay: state.overlay,
|
|
985
|
+
spanMode: state.spanMode,
|
|
986
|
+
});
|
|
987
|
+
const action = keyBindings.resolveAction({
|
|
988
|
+
name: key.name,
|
|
989
|
+
shift: !!key.shift,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
for (const intent of reduceKey(
|
|
993
|
+
state,
|
|
994
|
+
{ name: key.name, shift: !!key.shift, meta: !!key.meta },
|
|
995
|
+
action,
|
|
996
|
+
))
|
|
997
|
+
dispatch(intent);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// after every keyboard hook above, so their subscriptions precede the signal
|
|
1001
|
+
useReadySignal(usableScreenReached(error, session, inbox), onReady);
|
|
1002
|
+
|
|
1003
|
+
// ── shared bottom chrome: the menu bar and its drop-up dialogs, one render
|
|
1004
|
+
// reused by the inbox and by plan/diff review so the two never drift ──
|
|
1005
|
+
const menuChrome = (
|
|
1006
|
+
<MenuChrome
|
|
1007
|
+
menuDialog={menuDialog}
|
|
1008
|
+
theme={theme}
|
|
1009
|
+
keybindsSections={cheatsheetFor(keyBindings, threadViewActive)}
|
|
1010
|
+
settingsCategories={settingsCategories}
|
|
1011
|
+
settingsValues={settingsValues}
|
|
1012
|
+
settingsNav={settingsNav}
|
|
1013
|
+
onCategorySelect={onCategorySelect}
|
|
1014
|
+
cycleSetting={cycleSetting}
|
|
1015
|
+
onClose={() => setMenuDialog(null)}
|
|
1016
|
+
/>
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
// ── render ──────────────────────────────────
|
|
1020
|
+
if (error) return <ErrorScreen error={error} theme={theme} />;
|
|
1021
|
+
if (!session)
|
|
1022
|
+
return inbox ? (
|
|
1023
|
+
<SlashSkillsContext.Provider value={skills}>
|
|
1024
|
+
<PaletteNamesContext.Provider value={paletteNames}>
|
|
1025
|
+
<NoThreadShell
|
|
1026
|
+
rows={grouped.rows}
|
|
1027
|
+
inboxCursor={inboxCursor}
|
|
1028
|
+
onOpenThread={openThread}
|
|
1029
|
+
mode={mode}
|
|
1030
|
+
theme={theme}
|
|
1031
|
+
controller={controller}
|
|
1032
|
+
setMode={setMode}
|
|
1033
|
+
menuChrome={menuChrome}
|
|
1034
|
+
onOpenMenu={openSettings}
|
|
1035
|
+
sidebarOpen={sidebarOpen}
|
|
1036
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1037
|
+
focusedPane={focusedPane}
|
|
1038
|
+
onFocusPane={setFocusedPane}
|
|
1039
|
+
menuControl={menuControl}
|
|
1040
|
+
pinnedIds={pinnedIds}
|
|
1041
|
+
onPin={togglePin}
|
|
1042
|
+
onRename={(id, title) => setMode({ type: "renameThread", sessionId: id, text: title })}
|
|
1043
|
+
quickActions={quickActions}
|
|
1044
|
+
onWelcomeComposingChange={setWelcomeComposing}
|
|
1045
|
+
layout={layout}
|
|
1046
|
+
/>
|
|
1047
|
+
</PaletteNamesContext.Provider>
|
|
1048
|
+
</SlashSkillsContext.Provider>
|
|
1049
|
+
) : (
|
|
1050
|
+
<ConnectingScreen theme={theme} />
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
const activeSession = session;
|
|
1054
|
+
const resolveAuthorLabel = authorLabelResolver(
|
|
1055
|
+
identity.name,
|
|
1056
|
+
activeSession.participants,
|
|
1057
|
+
authorNames,
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
if (isCompletionOverlayPhase(completion) && activeSession.message)
|
|
1061
|
+
return (
|
|
1062
|
+
<CompletionScreen
|
|
1063
|
+
theme={theme}
|
|
1064
|
+
session={activeSession}
|
|
1065
|
+
message={activeSession.message.outcome}
|
|
1066
|
+
completion={completion}
|
|
1067
|
+
status={status}
|
|
1068
|
+
onClose={() => dispatch({ type: "finishReview" })}
|
|
1069
|
+
onBackToPlan={() => dispatch({ type: "dismissCompletion" })}
|
|
1070
|
+
onAlways={() => dispatch({ type: "optInAutoClose" })}
|
|
1071
|
+
/>
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
const diffFold: DiffFoldControls = {
|
|
1075
|
+
isCollapsed: (file) => controller.isFileCollapsed(file),
|
|
1076
|
+
isExpanded: (file) => controller.isFileExpanded(file),
|
|
1077
|
+
canExpand: (file) => controller.canExpandFile(file),
|
|
1078
|
+
onToggleCollapse: (file) =>
|
|
1079
|
+
controller.setFileCollapsed(file, !controller.isFileCollapsed(file)),
|
|
1080
|
+
onToggleExpand: (file) => controller.setFileExpanded(file, !controller.isFileExpanded(file)),
|
|
1081
|
+
onCopyPath: (file) => controller.copyFilePath(file),
|
|
1082
|
+
};
|
|
1083
|
+
const submitConfirmState = buildSubmitConfirmState({
|
|
1084
|
+
mode,
|
|
1085
|
+
isDiff,
|
|
1086
|
+
session: activeSession,
|
|
1087
|
+
walkFileList,
|
|
1088
|
+
viewedPaths,
|
|
1089
|
+
quickActions,
|
|
1090
|
+
liveInput,
|
|
1091
|
+
setMode,
|
|
1092
|
+
dispatch,
|
|
1093
|
+
});
|
|
1094
|
+
const { showOwnerActions, prototypeCanComment, chromeHidden, prototypePath } = buildRenderFlags({
|
|
1095
|
+
session: activeSession,
|
|
1096
|
+
isOwner,
|
|
1097
|
+
isDiff,
|
|
1098
|
+
isPixelPrototype,
|
|
1099
|
+
resolved,
|
|
1100
|
+
menuDialog,
|
|
1101
|
+
resolvedIds,
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
const onEditRequest = (): void => {
|
|
1105
|
+
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
1106
|
+
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
1107
|
+
if (!isOwner) return;
|
|
1108
|
+
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
1109
|
+
openBodyEditor();
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// clicking the rail Submit button: same read-only answer as the submit key
|
|
1113
|
+
const onSubmitRequest = (): void => {
|
|
1114
|
+
if (observer) return controller.setStatus("observer - read-only");
|
|
1115
|
+
if (!isOwner) return controller.setStatus("shared view - your notes save as you go");
|
|
1116
|
+
if (resolved) return;
|
|
1117
|
+
dispatch({ type: "openSubmit" });
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
const threadFooter = (
|
|
1121
|
+
<ThreadFooter
|
|
1122
|
+
repo={projectName(activeSession.workspace)}
|
|
1123
|
+
branch={activeSession.workspace.branch}
|
|
1124
|
+
onSubmit={onSubmitRequest}
|
|
1125
|
+
canSubmit={canSubmitReview(isOwner, resolved, observer)}
|
|
1126
|
+
pendingAnnotations={
|
|
1127
|
+
activeSession.annotations.filter(
|
|
1128
|
+
(annotation) => !isAddressed(annotation) && !isAgentNote(annotation),
|
|
1129
|
+
).length
|
|
1130
|
+
}
|
|
1131
|
+
theme={theme}
|
|
1132
|
+
/>
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
return (
|
|
1136
|
+
<SlashSkillsContext.Provider value={skills}>
|
|
1137
|
+
<PaletteNamesContext.Provider value={paletteNames}>
|
|
1138
|
+
<ThemeProvider theme={theme}>
|
|
1139
|
+
<MenuControlProvider value={menuControl}>
|
|
1140
|
+
<AppShell
|
|
1141
|
+
sidebarOpen={sidebarOpen}
|
|
1142
|
+
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
|
1143
|
+
onOpenMenu={openSettings}
|
|
1144
|
+
onFocusPane={setFocusedPane}
|
|
1145
|
+
threadsPanel={
|
|
1146
|
+
<scrollbox style={{ flexGrow: 1 }} focused={false}>
|
|
1147
|
+
<ThreadTree
|
|
1148
|
+
rows={grouped.rows}
|
|
1149
|
+
cursor={inboxCursor}
|
|
1150
|
+
activeId={activeSession.id}
|
|
1151
|
+
focused={focusedPane === "threads"}
|
|
1152
|
+
pinnedIds={pinnedIds}
|
|
1153
|
+
width={30}
|
|
1154
|
+
onSelect={openThread}
|
|
1155
|
+
onPin={togglePin}
|
|
1156
|
+
onRename={(id, title) =>
|
|
1157
|
+
setMode({ type: "renameThread", sessionId: id, text: title })
|
|
1158
|
+
}
|
|
1159
|
+
onRequestDelete={(id, title) =>
|
|
1160
|
+
setMode({ type: "confirmDelete", sessionId: id, title })
|
|
1161
|
+
}
|
|
1162
|
+
theme={theme}
|
|
1163
|
+
/>
|
|
1164
|
+
</scrollbox>
|
|
1165
|
+
}
|
|
1166
|
+
threadTitle={threadTitle(activeSession)}
|
|
1167
|
+
threadActions={
|
|
1168
|
+
showOwnerActions
|
|
1169
|
+
? ownerThreadActions({
|
|
1170
|
+
editing: bodyEditing.editing,
|
|
1171
|
+
onEdit: onEditRequest,
|
|
1172
|
+
onExitEdit: bodyEditing.requestExit,
|
|
1173
|
+
onShare: () => dispatch({ type: "share" }),
|
|
1174
|
+
theme,
|
|
1175
|
+
})
|
|
1176
|
+
: undefined
|
|
1177
|
+
}
|
|
1178
|
+
threadPanel={
|
|
1179
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
1180
|
+
<box style={{ flexGrow: 1, flexDirection: "row" }}>
|
|
1181
|
+
{chooseThreadBody({
|
|
1182
|
+
isPixelPrototype,
|
|
1183
|
+
isDiff,
|
|
1184
|
+
editingBody: bodyEditing.editing,
|
|
1185
|
+
prototype: (
|
|
1186
|
+
<PrototypePixels
|
|
1187
|
+
prototypePath={prototypePath}
|
|
1188
|
+
canComment={prototypeCanComment}
|
|
1189
|
+
onCommentElement={onCommentPrototype}
|
|
1190
|
+
onComposingChange={setPrototypeComposing}
|
|
1191
|
+
hidden={chromeHidden}
|
|
1192
|
+
/>
|
|
1193
|
+
),
|
|
1194
|
+
diffPlaceholder: (
|
|
1195
|
+
<box
|
|
1196
|
+
style={{
|
|
1197
|
+
flexGrow: 1,
|
|
1198
|
+
flexDirection: "column",
|
|
1199
|
+
alignItems: "center",
|
|
1200
|
+
justifyContent: "center",
|
|
1201
|
+
}}
|
|
1202
|
+
>
|
|
1203
|
+
<text fg={theme.textDim}>Select a thread</text>
|
|
1204
|
+
</box>
|
|
1205
|
+
),
|
|
1206
|
+
editor: bodyEditing.renderEditor(theme),
|
|
1207
|
+
threadView: (
|
|
1208
|
+
<ThreadView
|
|
1209
|
+
session={activeSession}
|
|
1210
|
+
suspended={surfaceSuspended(threadViewSuspended, focusedPane, "thread")}
|
|
1211
|
+
editOrphanCount={editOrphanCount}
|
|
1212
|
+
onComposingChange={setThreadComposing}
|
|
1213
|
+
onNavCommand={runNavCommand}
|
|
1214
|
+
resolved={resolved}
|
|
1215
|
+
onObserverBlocked={(reason) =>
|
|
1216
|
+
controller.setStatus(
|
|
1217
|
+
reason === "observer"
|
|
1218
|
+
? "observer - read-only"
|
|
1219
|
+
: "review submitted - read-only",
|
|
1220
|
+
)
|
|
1221
|
+
}
|
|
1222
|
+
onCursorChange={setCursor}
|
|
1223
|
+
focusedAnnotationId={focusedAnnotationId}
|
|
1224
|
+
onFocusAnnotation={setFocusedAnnotationId}
|
|
1225
|
+
display={display}
|
|
1226
|
+
marks={marks}
|
|
1227
|
+
quickActions={quickActions}
|
|
1228
|
+
observer={observer}
|
|
1229
|
+
onAnnotate={(span, body) =>
|
|
1230
|
+
void controller.annotate(
|
|
1231
|
+
"comment",
|
|
1232
|
+
span.start.blockIndex,
|
|
1233
|
+
span.start.char,
|
|
1234
|
+
span.end.char,
|
|
1235
|
+
body,
|
|
1236
|
+
span.end.blockIndex,
|
|
1237
|
+
)
|
|
1238
|
+
}
|
|
1239
|
+
onReply={(rootAnnotationId, body) =>
|
|
1240
|
+
void controller.reply(rootAnnotationId, body)
|
|
1241
|
+
}
|
|
1242
|
+
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1243
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1244
|
+
onExit={() => onExit?.(0)}
|
|
1245
|
+
/>
|
|
1246
|
+
),
|
|
1247
|
+
})}
|
|
1248
|
+
</box>
|
|
1249
|
+
{threadFooter}
|
|
1250
|
+
</box>
|
|
1251
|
+
}
|
|
1252
|
+
changesOpen={workbench.changesOpen}
|
|
1253
|
+
projectOpen={workbench.projectOpen}
|
|
1254
|
+
onToggleChanges={workbench.toggleChanges}
|
|
1255
|
+
onToggleProject={workbench.toggleProject}
|
|
1256
|
+
onToggleRight={workbench.toggleRight}
|
|
1257
|
+
projectMode={workbench.projectMode}
|
|
1258
|
+
zoomHideThread={workbench.zoomed}
|
|
1259
|
+
changesFooter={threadFooter}
|
|
1260
|
+
changesPanel={
|
|
1261
|
+
<EditorGrid
|
|
1262
|
+
tree={workbench.grid}
|
|
1263
|
+
focusedGroupId={workbench.activeGroup}
|
|
1264
|
+
commentCounts={diffCommentCounts}
|
|
1265
|
+
onFocusGroup={workbench.focusGroup}
|
|
1266
|
+
onActivateTab={workbench.activate}
|
|
1267
|
+
onCloseTab={workbench.close}
|
|
1268
|
+
onSplit={workbench.split}
|
|
1269
|
+
onZoom={workbench.toggleZoom}
|
|
1270
|
+
zoomed={workbench.zoomed}
|
|
1271
|
+
renderTab={(tab, groupFocused) => (
|
|
1272
|
+
<GridTabContent
|
|
1273
|
+
tab={tab}
|
|
1274
|
+
rows={rows}
|
|
1275
|
+
surface={{
|
|
1276
|
+
session: activeSession,
|
|
1277
|
+
quickActions,
|
|
1278
|
+
observer,
|
|
1279
|
+
commentsEnabled: true,
|
|
1280
|
+
resolved,
|
|
1281
|
+
suspended: surfaceSuspended(
|
|
1282
|
+
threadViewSuspended || !groupFocused,
|
|
1283
|
+
focusedPane,
|
|
1284
|
+
"changes",
|
|
1285
|
+
),
|
|
1286
|
+
onComposingChange: setThreadComposing,
|
|
1287
|
+
onObserverBlocked: (reason) =>
|
|
1288
|
+
controller.setStatus(
|
|
1289
|
+
reason === "observer"
|
|
1290
|
+
? "observer - read-only"
|
|
1291
|
+
: "review submitted - read-only",
|
|
1292
|
+
),
|
|
1293
|
+
onCursorChange: setCursor,
|
|
1294
|
+
focusedAnnotationId,
|
|
1295
|
+
onFocusAnnotation: setFocusedAnnotationId,
|
|
1296
|
+
// a diff thread's Changes view is the artifact itself; any other thread's is the
|
|
1297
|
+
// live working-tree diff, so its notes carry a file target and anchor there
|
|
1298
|
+
onAnnotate: (span, body) =>
|
|
1299
|
+
void controller.annotate(
|
|
1300
|
+
"comment",
|
|
1301
|
+
span.start.blockIndex,
|
|
1302
|
+
span.start.char,
|
|
1303
|
+
span.end.char,
|
|
1304
|
+
body,
|
|
1305
|
+
span.end.blockIndex,
|
|
1306
|
+
isDiff
|
|
1307
|
+
? { kind: "artifact" }
|
|
1308
|
+
: { kind: "file", path: "", rev: "worktree" },
|
|
1309
|
+
),
|
|
1310
|
+
onReply: (rootAnnotationId, body) =>
|
|
1311
|
+
void controller.reply(rootAnnotationId, body),
|
|
1312
|
+
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1313
|
+
resolveAuthorLabel,
|
|
1314
|
+
onNavCommand: runNavCommand,
|
|
1315
|
+
onExit: () => onExit?.(0),
|
|
1316
|
+
}}
|
|
1317
|
+
rejectedRows={rejectedRows}
|
|
1318
|
+
fold={diffFold}
|
|
1319
|
+
fileStats={controller.fileStats()}
|
|
1320
|
+
split={diffView === "split" && workbench.zoomed}
|
|
1321
|
+
dimmed={walking}
|
|
1322
|
+
readFile={(path) => controller.repoReadFile(path)}
|
|
1323
|
+
onAddFileComment={(path, anchor, body) =>
|
|
1324
|
+
void controller.addComment(
|
|
1325
|
+
anchor,
|
|
1326
|
+
{ kind: "file", path, rev: "worktree" },
|
|
1327
|
+
body,
|
|
1328
|
+
)
|
|
1329
|
+
}
|
|
1330
|
+
// zoomed, the footer already rides the Changes pane, so no extra offset is needed
|
|
1331
|
+
emptyBottomPadding={workbench.zoomed ? 0 : THREAD_FOOTER_HEIGHT}
|
|
1332
|
+
theme={theme}
|
|
1333
|
+
/>
|
|
1334
|
+
)}
|
|
1335
|
+
theme={theme}
|
|
1336
|
+
/>
|
|
1337
|
+
}
|
|
1338
|
+
projectPanel={
|
|
1339
|
+
<ProjectPanelBody
|
|
1340
|
+
mode={workbench.projectMode}
|
|
1341
|
+
loadChanges={() => controller.repoChanges()}
|
|
1342
|
+
loadProjectFiles={() => controller.repoFiles()}
|
|
1343
|
+
reloadKey={activeSession.id}
|
|
1344
|
+
// the Changes navigator always opens a changed file as a diff - a diff review shows its
|
|
1345
|
+
// captured snapshot, every other thread the live working-tree diff
|
|
1346
|
+
onOpenChangedFile={(path) => workbench.openFile(path, "diff")}
|
|
1347
|
+
onOpenProjectFile={(path) => workbench.openFile(path, "contents")}
|
|
1348
|
+
commentCounts={diffCommentCounts}
|
|
1349
|
+
focused={focusedPane === "project"}
|
|
1350
|
+
theme={theme}
|
|
1351
|
+
/>
|
|
1352
|
+
}
|
|
1353
|
+
theme={theme}
|
|
1354
|
+
>
|
|
1355
|
+
<TrailingOverlays
|
|
1356
|
+
walking={walking}
|
|
1357
|
+
walk={walk}
|
|
1358
|
+
walkFileList={walkFileList}
|
|
1359
|
+
viewedPaths={viewedPaths}
|
|
1360
|
+
session={activeSession}
|
|
1361
|
+
terminalWidth={terminalWidth}
|
|
1362
|
+
theme={theme}
|
|
1363
|
+
mode={mode}
|
|
1364
|
+
toast={toast}
|
|
1365
|
+
setMode={setMode}
|
|
1366
|
+
dispatch={dispatch}
|
|
1367
|
+
/>
|
|
1368
|
+
{submitConfirmState !== null ? (
|
|
1369
|
+
<box
|
|
1370
|
+
onMouseUp={submitConfirmState.onCancel}
|
|
1371
|
+
style={{
|
|
1372
|
+
position: "absolute",
|
|
1373
|
+
left: 0,
|
|
1374
|
+
top: 0,
|
|
1375
|
+
width: "100%",
|
|
1376
|
+
height: "100%",
|
|
1377
|
+
justifyContent: "center",
|
|
1378
|
+
alignItems: "center",
|
|
1379
|
+
}}
|
|
1380
|
+
>
|
|
1381
|
+
<box onMouseUp={(event) => event.stopPropagation()}>
|
|
1382
|
+
<ConfirmCard {...submitConfirmState} theme={theme} />
|
|
1383
|
+
</box>
|
|
1384
|
+
</box>
|
|
1385
|
+
) : null}
|
|
1386
|
+
<ShareDialog
|
|
1387
|
+
key={String(shareDialogOpen)}
|
|
1388
|
+
isOpen={shareDialogOpen}
|
|
1389
|
+
threadName={shareThreadName(session)}
|
|
1390
|
+
links={shareLinksFor(session)}
|
|
1391
|
+
isOwner={isOwner}
|
|
1392
|
+
onCreateLink={(input) => controller.createShareLink(input)}
|
|
1393
|
+
onUpdateLink={(id, input) => controller.updateShareLink(id, input)}
|
|
1394
|
+
onDeleteLink={(id) => controller.deleteShareLink(id)}
|
|
1395
|
+
onCopyLink={(id) => controller.copyShareLink(id)}
|
|
1396
|
+
onClose={() => setShareDialogOpen(false)}
|
|
1397
|
+
theme={theme}
|
|
1398
|
+
/>
|
|
1399
|
+
{menuChrome}
|
|
1400
|
+
</AppShell>
|
|
1401
|
+
</MenuControlProvider>
|
|
1402
|
+
</ThemeProvider>
|
|
1403
|
+
</PaletteNamesContext.Provider>
|
|
1404
|
+
</SlashSkillsContext.Provider>
|
|
1405
|
+
);
|
|
676
1406
|
}
|