@cueloop/client 0.1.0-alpha.76 → 0.1.0-alpha.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/App.autoclose.test.tsx +3 -3
- package/src/App.diff.test.tsx +14 -2
- package/src/App.export.test.tsx +1 -2
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-review.test.tsx +16 -0
- package/src/App.share-choice.test.tsx +105 -0
- package/src/App.submit-confirm.test.tsx +34 -5
- package/src/App.test.tsx +2 -2
- package/src/App.toast.test.tsx +3 -1
- package/src/App.tsx +180 -5
- package/src/App.walk.test.tsx +2 -2
- package/src/__snapshots__/view-plan.regression.test.ts.snap +1 -1
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-screens.tsx +20 -3
- package/src/app-view-model.ts +4 -0
- package/src/components/AnnotatableFileView.tsx +10 -2
- package/src/components/AnnotationCards.tsx +40 -6
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/CompletionOverlay.stories.tsx +15 -1
- package/src/components/CompletionOverlay.tsx +22 -5
- package/src/components/ConfirmCard.stories.tsx +2 -0
- package/src/components/ConfirmCard.tsx +45 -18
- package/src/components/ConfirmDialog.tsx +11 -12
- package/src/components/DiffContentView.tsx +9 -5
- package/src/components/FileContentsView.tsx +1 -1
- package/src/components/GridTabContent.tsx +2 -0
- package/src/components/ManageAccessDialog.stories.tsx +36 -0
- package/src/components/ManageAccessDialog.test.tsx +94 -0
- package/src/components/ManageAccessDialog.tsx +122 -0
- package/src/components/PromptDialog.test.tsx +2 -2
- package/src/components/PromptDialog.tsx +9 -10
- package/src/components/ShareChoiceDialog.stories.tsx +34 -0
- package/src/components/ShareChoiceDialog.test.tsx +103 -0
- package/src/components/ShareChoiceDialog.tsx +80 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadTree.tsx +11 -6
- package/src/components/ThreadView.selection.test.tsx +88 -1
- package/src/components/ThreadView.slash.test.tsx +0 -1
- package/src/components/ThreadView.tsx +71 -8
- package/src/components/Toast.tsx +0 -5
- package/src/components/WalkWizard.stories.tsx +2 -2
- package/src/components/WalkWizard.tsx +10 -12
- package/src/components/__snapshots__/stories.test.tsx.snap +277 -61
- package/src/components/primitives/Card.tsx +3 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/session-tree.ts +7 -6
- package/src/components/stories-app.tsx +13 -4
- package/src/components/stories.test.tsx +8 -1
- package/src/components/story-tree.test.ts +32 -24
- package/src/components/story.ts +32 -15
- package/src/config-identity.test.ts +78 -0
- package/src/config.ts +44 -1
- package/src/diff-intraline.test.ts +75 -2
- package/src/diff-intraline.ts +95 -26
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/intent-dispatch.test.ts +17 -2
- package/src/intent-dispatch.ts +7 -1
- package/src/keymap.test.ts +2 -2
- package/src/keymap.ts +1 -1
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/run.ts +31 -19
- package/src/serve.ts +1 -1
- package/src/share.ts +11 -3
- package/src/slash-palette.test.ts +17 -0
- package/src/slash-palette.ts +11 -0
- package/src/thread-controller.curate.test.ts +1 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +1 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +36 -2
- package/src/thread-controller.switch-cache.test.ts +146 -0
- package/src/thread-controller.tree.test.ts +1 -0
- package/src/thread-controller.ts +171 -24
- package/src/use-annotation-surface.tsx +53 -29
- package/src/use-settings-dialog.tsx +24 -0
package/src/App.tsx
CHANGED
|
@@ -15,11 +15,15 @@ import {
|
|
|
15
15
|
loadConfig,
|
|
16
16
|
persistAuthorName,
|
|
17
17
|
persistDiffView,
|
|
18
|
+
persistIdentity,
|
|
18
19
|
persistPins,
|
|
19
20
|
type AutoClose,
|
|
20
21
|
type DiffViewMode,
|
|
22
|
+
type IdentityConfig,
|
|
21
23
|
type QuickAction,
|
|
22
24
|
} from "./config";
|
|
25
|
+
import { resolveDisplayName } from "./attribution";
|
|
26
|
+
import { resolveGithubIdentity } from "./github-identity";
|
|
23
27
|
import { loadSkills, PaletteNamesContext, SlashSkillsContext } from "./skills";
|
|
24
28
|
import { mergeSlashItems, slashItemsFrom, type SlashItem } from "./slash-palette";
|
|
25
29
|
import {
|
|
@@ -38,6 +42,7 @@ import { KeyBindings, type CheatsheetSection } from "./key-bindings";
|
|
|
38
42
|
import { useReadySignal } from "./ready-signal";
|
|
39
43
|
import { ThemeProvider } from "./components/theme-context";
|
|
40
44
|
import { Button } from "./components/primitives/Button";
|
|
45
|
+
import { ShareChoiceDialog, SHARE_CHOICES } from "./components/ShareChoiceDialog";
|
|
41
46
|
import { Toolbar } from "./components/primitives/Toolbar";
|
|
42
47
|
import { groupInbox, projectName, threadTitle } from "./components/session-tree";
|
|
43
48
|
import { ThreadTree } from "./components/ThreadTree";
|
|
@@ -52,6 +57,7 @@ import { useRememberLayout } from "./use-remember-layout";
|
|
|
52
57
|
import type { LaunchLayout } from "./launch-layout";
|
|
53
58
|
import { ThreadFooter } from "./components/ThreadFooter";
|
|
54
59
|
import { ConfirmCard } from "./components/ConfirmCard";
|
|
60
|
+
import { ManageAccessDialog } from "./components/ManageAccessDialog";
|
|
55
61
|
import { THREAD_VIEW_CHEATSHEET, ThreadView } from "./components/ThreadView";
|
|
56
62
|
import {
|
|
57
63
|
diffChordEntries,
|
|
@@ -67,7 +73,7 @@ import {
|
|
|
67
73
|
import { type DiffFoldControls } from "./components/DiffContentView";
|
|
68
74
|
import { commentCountsByFile } from "./view-diff";
|
|
69
75
|
import { annotationTarget } from "@cueloop/schema";
|
|
70
|
-
import type { Artifact, DiffFileContents, Thread } from "@cueloop/schema";
|
|
76
|
+
import type { Annotation, Artifact, DiffFileContents, Identity, Thread } from "@cueloop/schema";
|
|
71
77
|
import { PrototypePixels } from "./prototype-pixels";
|
|
72
78
|
import type { PrototypeElement } from "./prototype-browser";
|
|
73
79
|
import {
|
|
@@ -139,6 +145,11 @@ function menuChromeOpen(menuDialog: "keybinds" | "settings" | null): boolean {
|
|
|
139
145
|
return menuDialog !== null;
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
/** The private-share allowlist for a session, or an empty list for a public one. */
|
|
149
|
+
function allowedGithubLogins(session: Thread | null): string[] {
|
|
150
|
+
return session?.access?.githubLogins ?? [];
|
|
151
|
+
}
|
|
152
|
+
|
|
142
153
|
/** The drop-up chrome or a floating popover menu (thread actions, editor split) holds the keyboard. */
|
|
143
154
|
function keyboardHeldByMenu(
|
|
144
155
|
menuDialog: "keybinds" | "settings" | null,
|
|
@@ -184,11 +195,89 @@ function keyboardOwnedElsewhere(menuOwnsKeyboard: boolean, overlay: KeyState["ov
|
|
|
184
195
|
return menuOwnsKeyboard || overlay !== "none";
|
|
185
196
|
}
|
|
186
197
|
|
|
198
|
+
/** The highlighted share option, or the public default when the choice is closed. */
|
|
199
|
+
function shareChoiceIndex(mode: Mode): number {
|
|
200
|
+
return mode.type === "shareChoice" ? mode.index : 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface ShareChoiceActions {
|
|
204
|
+
setMode: (mode: Mode) => void;
|
|
205
|
+
isOwner: boolean;
|
|
206
|
+
publish: () => void;
|
|
207
|
+
openManageAccess: () => void;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Move the share-choice highlight by one row, clamped to the list. */
|
|
211
|
+
function movedShareChoice(index: number, delta: number): Mode {
|
|
212
|
+
const next = Math.min(SHARE_CHOICES.length - 1, Math.max(0, index + delta));
|
|
213
|
+
|
|
214
|
+
return { type: "shareChoice", index: next };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Commit the highlighted share option: public publishes a link, private opens manage-access. */
|
|
218
|
+
function commitShareChoice(index: number, actions: ShareChoiceActions): void {
|
|
219
|
+
const picked = SHARE_CHOICES[index]?.choice;
|
|
220
|
+
|
|
221
|
+
actions.setMode({ type: "normal" });
|
|
222
|
+
|
|
223
|
+
if (!actions.isOwner) return;
|
|
224
|
+
|
|
225
|
+
if (picked === "private") actions.openManageAccess();
|
|
226
|
+
else actions.publish();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Route one key to the open share choice: escape cancels, arrows move, enter commits. */
|
|
230
|
+
function handleShareChoiceKey(
|
|
231
|
+
index: number,
|
|
232
|
+
key: { name: string },
|
|
233
|
+
actions: ShareChoiceActions,
|
|
234
|
+
): void {
|
|
235
|
+
if (key.name === "escape") return actions.setMode({ type: "normal" });
|
|
236
|
+
|
|
237
|
+
if (key.name === "down" || key.name === "j") return actions.setMode(movedShareChoice(index, 1));
|
|
238
|
+
|
|
239
|
+
if (key.name === "up" || key.name === "k") return actions.setMode(movedShareChoice(index, -1));
|
|
240
|
+
|
|
241
|
+
if (key.name === "return" || key.name === "enter") commitShareChoice(index, actions);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* An app-level dialog owns the keyboard while open: the manage-access dialog takes its own keys,
|
|
246
|
+
* and the share choice moves the highlight, commits on enter, and cancels on escape. Returns true
|
|
247
|
+
* when a dialog consumed the key so the caller stops the grammar.
|
|
248
|
+
*/
|
|
249
|
+
function appDialogOwnsKey(
|
|
250
|
+
accessDialogOpen: boolean,
|
|
251
|
+
mode: Mode,
|
|
252
|
+
key: { name: string },
|
|
253
|
+
actions: ShareChoiceActions,
|
|
254
|
+
): boolean {
|
|
255
|
+
if (accessDialogOpen) return true;
|
|
256
|
+
|
|
257
|
+
if (mode.type !== "shareChoice") return false;
|
|
258
|
+
|
|
259
|
+
handleShareChoiceKey(mode.index, key, actions);
|
|
260
|
+
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
187
264
|
/** The footer submit fires only for the owner of an unresolved review, never an observer. */
|
|
188
265
|
function canSubmitReview(isOwner: boolean, resolved: boolean, observer: boolean): boolean {
|
|
189
266
|
return isOwner && !resolved && !observer;
|
|
190
267
|
}
|
|
191
268
|
|
|
269
|
+
/** The author display name for a comment's hover tooltip: the reviewer's own name for their notes, else the resolved collaborator name. */
|
|
270
|
+
function authorLabelResolver(
|
|
271
|
+
ownName: string | undefined,
|
|
272
|
+
participants: Identity[] | undefined,
|
|
273
|
+
overrides: Record<string, string>,
|
|
274
|
+
): (annotation: Annotation) => string | undefined {
|
|
275
|
+
return (annotation) =>
|
|
276
|
+
annotation.author === undefined
|
|
277
|
+
? (ownName ?? "you")
|
|
278
|
+
: resolveDisplayName(annotation.author, participants, overrides);
|
|
279
|
+
}
|
|
280
|
+
|
|
192
281
|
/** The Project pane body: the resolved repo's changed files in changes mode, its full tree otherwise. */
|
|
193
282
|
function ProjectPanelBody(props: {
|
|
194
283
|
mode: ProjectPanelMode;
|
|
@@ -505,6 +594,7 @@ export function App({
|
|
|
505
594
|
const [mode, setMode] = useState<Mode>({ type: "normal" });
|
|
506
595
|
// the top-left settings gear drop-down and the centered dialog it opens
|
|
507
596
|
const [menuDialog, setMenuDialog] = useState<"keybinds" | "settings" | null>(null);
|
|
597
|
+
const [accessDialogOpen, setAccessDialogOpen] = useState(false);
|
|
508
598
|
const [autoClose, setAutoClose] = useState<AutoClose>("off");
|
|
509
599
|
// unified or side-by-side diff; split only lays out when the Changes pane is zoomed
|
|
510
600
|
const [diffView, setDiffView] = useState<DiffViewMode>("split");
|
|
@@ -526,6 +616,7 @@ export function App({
|
|
|
526
616
|
const [themeName, setThemeName] = useState<ThemeName>(DEFAULT_THEME_NAME);
|
|
527
617
|
const [themeOverrides, setThemeOverrides] = useState<Partial<Theme>>({});
|
|
528
618
|
const [authorNames, setAuthorNames] = useState<Record<string, string>>({});
|
|
619
|
+
const [identity, setIdentity] = useState<IdentityConfig>({ provider: "typed" });
|
|
529
620
|
const [quickActions, setQuickActions] = useState<QuickAction[]>(DEFAULT_QUICK_ACTIONS);
|
|
530
621
|
const [skills, setSkills] = useState<SlashItem[]>([]);
|
|
531
622
|
const paletteNames = useMemo(
|
|
@@ -545,6 +636,7 @@ export function App({
|
|
|
545
636
|
setThemeName(config.ui.theme);
|
|
546
637
|
setThemeOverrides(config.themeOverrides);
|
|
547
638
|
setAuthorNames(config.authors);
|
|
639
|
+
setIdentity(config.identity);
|
|
548
640
|
setQuickActions(config.actions);
|
|
549
641
|
setSkills(loadSkills(config.skillsPath));
|
|
550
642
|
setAutoClose(config.ui.autoClose);
|
|
@@ -578,6 +670,29 @@ export function App({
|
|
|
578
670
|
if (!known) setMode({ type: "nameSelf", text: "" });
|
|
579
671
|
}, [role, selfAuthor, session]);
|
|
580
672
|
|
|
673
|
+
// Bumped on every identity change so a slow GitHub sync that lands after a newer rename is discarded.
|
|
674
|
+
const identityGenerationRef = useRef(0);
|
|
675
|
+
const applyIdentity = (next: IdentityConfig): void => {
|
|
676
|
+
identityGenerationRef.current += 1;
|
|
677
|
+
setIdentity(next);
|
|
678
|
+
try {
|
|
679
|
+
persistIdentity(next);
|
|
680
|
+
} catch {
|
|
681
|
+
controller.setStatus("could not save identity to config");
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const syncGithubIdentity = (): void => {
|
|
685
|
+
const generation = (identityGenerationRef.current += 1);
|
|
686
|
+
void resolveGithubIdentity().then((github) => {
|
|
687
|
+
if (identityGenerationRef.current !== generation) return;
|
|
688
|
+
if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
|
|
689
|
+
const name = github.name?.trim() || github.login;
|
|
690
|
+
|
|
691
|
+
applyIdentity({ name, provider: "github" });
|
|
692
|
+
controller.setStatus(`identity synced from GitHub - ${name}`);
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
581
696
|
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
582
697
|
const {
|
|
583
698
|
settingsNav,
|
|
@@ -600,6 +715,13 @@ export function App({
|
|
|
600
715
|
quickActions,
|
|
601
716
|
setQuickActions,
|
|
602
717
|
setMenuDialog,
|
|
718
|
+
identityName: identity.name,
|
|
719
|
+
identityProvider: identity.provider,
|
|
720
|
+
onSyncGithubIdentity: syncGithubIdentity,
|
|
721
|
+
onRenameDisplayName: () => {
|
|
722
|
+
setMenuDialog(null);
|
|
723
|
+
setMode({ type: "renameSelf", text: identity.name ?? "" });
|
|
724
|
+
},
|
|
603
725
|
});
|
|
604
726
|
|
|
605
727
|
// ── derived view model ──────────────────────
|
|
@@ -718,6 +840,9 @@ export function App({
|
|
|
718
840
|
setAuthorNames((prev) => ({ ...prev, [id]: name }));
|
|
719
841
|
},
|
|
720
842
|
renameThread: (id: string, title: string) => controller.renameSession(id, title),
|
|
843
|
+
setLocalIdentityName: (name: string) => {
|
|
844
|
+
applyIdentity(name ? { name, provider: "typed" } : { provider: "typed" });
|
|
845
|
+
},
|
|
721
846
|
liveInput,
|
|
722
847
|
setCursor,
|
|
723
848
|
setInboxCursor,
|
|
@@ -746,8 +871,8 @@ export function App({
|
|
|
746
871
|
|
|
747
872
|
const menuControl = useMenuControlState();
|
|
748
873
|
const menuOwnsKeyboard = keyboardHeldByMenu(menuDialog, menuControl.openMenuId);
|
|
749
|
-
// an overlay (submit, walk, prompt, confirm) or the menu takes the
|
|
750
|
-
// from the thread view; the view suspends its own grammar meanwhile
|
|
874
|
+
// an overlay (submit, walk, prompt, confirm, share choice) or the menu takes the
|
|
875
|
+
// keyboard from the thread view; the view suspends its own grammar meanwhile
|
|
751
876
|
const threadViewSuspended = keyboardOwnedElsewhere(menuOwnsKeyboard, overlay);
|
|
752
877
|
|
|
753
878
|
const leaderCombos = leaderCombosFor(keysRef.current.leader);
|
|
@@ -787,6 +912,16 @@ export function App({
|
|
|
787
912
|
|
|
788
913
|
useKeyboard((key) => {
|
|
789
914
|
if (quitKeyHandled(key, onExit)) return;
|
|
915
|
+
// an app-level dialog (manage-access, share choice) owns the keyboard while open
|
|
916
|
+
if (
|
|
917
|
+
appDialogOwnsKey(accessDialogOpen, mode, key, {
|
|
918
|
+
setMode,
|
|
919
|
+
isOwner,
|
|
920
|
+
publish: () => controller.share(),
|
|
921
|
+
openManageAccess: () => setAccessDialogOpen(true),
|
|
922
|
+
})
|
|
923
|
+
)
|
|
924
|
+
return;
|
|
790
925
|
if (menuModalHandled(menuControl, key)) return;
|
|
791
926
|
if (
|
|
792
927
|
appLeaderHandled({ focusedPane, key, leaderCombos, pending: leaderPending, runLeaderCommand })
|
|
@@ -919,6 +1054,11 @@ export function App({
|
|
|
919
1054
|
);
|
|
920
1055
|
|
|
921
1056
|
const activeSession = session;
|
|
1057
|
+
const resolveAuthorLabel = authorLabelResolver(
|
|
1058
|
+
identity.name,
|
|
1059
|
+
activeSession.participants,
|
|
1060
|
+
authorNames,
|
|
1061
|
+
);
|
|
922
1062
|
|
|
923
1063
|
if (isCompletionOverlayPhase(completion) && activeSession.verdict)
|
|
924
1064
|
return (
|
|
@@ -928,6 +1068,9 @@ export function App({
|
|
|
928
1068
|
verdict={activeSession.verdict.kind}
|
|
929
1069
|
completion={completion}
|
|
930
1070
|
status={status}
|
|
1071
|
+
onClose={() => dispatch({ type: "finishReview" })}
|
|
1072
|
+
onBackToPlan={() => dispatch({ type: "dismissCompletion" })}
|
|
1073
|
+
onAlways={() => dispatch({ type: "optInAutoClose" })}
|
|
931
1074
|
/>
|
|
932
1075
|
);
|
|
933
1076
|
|
|
@@ -946,6 +1089,7 @@ export function App({
|
|
|
946
1089
|
session: activeSession,
|
|
947
1090
|
walkFileList,
|
|
948
1091
|
viewedPaths,
|
|
1092
|
+
quickActions,
|
|
949
1093
|
liveInput,
|
|
950
1094
|
setMode,
|
|
951
1095
|
dispatch,
|
|
@@ -968,12 +1112,19 @@ export function App({
|
|
|
968
1112
|
runEditorHandOff();
|
|
969
1113
|
};
|
|
970
1114
|
|
|
971
|
-
//
|
|
1115
|
+
// the share choice's public option: publish the plan, copy the ssh line
|
|
972
1116
|
const onShareRequest = (): void => {
|
|
973
1117
|
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
974
1118
|
controller.share();
|
|
975
1119
|
};
|
|
976
1120
|
|
|
1121
|
+
// the share choice's private option opens the manage-access allowlist surface
|
|
1122
|
+
const onPrivateShareRequest = (): void => {
|
|
1123
|
+
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
1124
|
+
setAccessDialogOpen(true);
|
|
1125
|
+
};
|
|
1126
|
+
const allowedLogins = allowedGithubLogins(session);
|
|
1127
|
+
|
|
977
1128
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
978
1129
|
const onSubmitRequest = (): void => {
|
|
979
1130
|
if (observer) return controller.setStatus("observer - read-only");
|
|
@@ -1027,7 +1178,7 @@ export function App({
|
|
|
1027
1178
|
<Button onPress={onEditRequest} theme={theme}>
|
|
1028
1179
|
{" edit "}
|
|
1029
1180
|
</Button>
|
|
1030
|
-
<Button onPress={
|
|
1181
|
+
<Button onPress={() => dispatch({ type: "share" })} theme={theme}>
|
|
1031
1182
|
{" share "}
|
|
1032
1183
|
</Button>
|
|
1033
1184
|
</Toolbar>
|
|
@@ -1092,6 +1243,7 @@ export function App({
|
|
|
1092
1243
|
void controller.reply(rootAnnotationId, body)
|
|
1093
1244
|
}
|
|
1094
1245
|
onUpdateAnnotation={(id, body) => controller.updateAnnotation(id, body)}
|
|
1246
|
+
resolveAuthorLabel={resolveAuthorLabel}
|
|
1095
1247
|
onExit={() => onExit?.(0)}
|
|
1096
1248
|
/>
|
|
1097
1249
|
)}
|
|
@@ -1160,6 +1312,7 @@ export function App({
|
|
|
1160
1312
|
onReply: (rootAnnotationId, body) =>
|
|
1161
1313
|
void controller.reply(rootAnnotationId, body),
|
|
1162
1314
|
onUpdateAnnotation: (id, body) => controller.updateAnnotation(id, body),
|
|
1315
|
+
resolveAuthorLabel,
|
|
1163
1316
|
leaderCombos,
|
|
1164
1317
|
onLeaderCommand: runLeaderCommand,
|
|
1165
1318
|
onExit: () => onExit?.(0),
|
|
@@ -1231,6 +1384,28 @@ export function App({
|
|
|
1231
1384
|
</box>
|
|
1232
1385
|
</box>
|
|
1233
1386
|
) : null}
|
|
1387
|
+
<ManageAccessDialog
|
|
1388
|
+
isOpen={accessDialogOpen}
|
|
1389
|
+
logins={allowedLogins}
|
|
1390
|
+
onAdd={(login) => controller.setShareAccess([...allowedLogins, login])}
|
|
1391
|
+
onRemove={(login) =>
|
|
1392
|
+
controller.setShareAccess(allowedLogins.filter((entry) => entry !== login))
|
|
1393
|
+
}
|
|
1394
|
+
onCreateLink={() => {
|
|
1395
|
+
setAccessDialogOpen(false);
|
|
1396
|
+
controller.share();
|
|
1397
|
+
}}
|
|
1398
|
+
onClose={() => setAccessDialogOpen(false)}
|
|
1399
|
+
theme={theme}
|
|
1400
|
+
/>
|
|
1401
|
+
<ShareChoiceDialog
|
|
1402
|
+
isOpen={mode.type === "shareChoice"}
|
|
1403
|
+
selectedIndex={shareChoiceIndex(mode)}
|
|
1404
|
+
onPublicShare={onShareRequest}
|
|
1405
|
+
onPrivateShare={onPrivateShareRequest}
|
|
1406
|
+
onClose={() => setMode({ type: "normal" })}
|
|
1407
|
+
theme={theme}
|
|
1408
|
+
/>
|
|
1234
1409
|
{menuChrome}
|
|
1235
1410
|
</AppShell>
|
|
1236
1411
|
</MenuControlProvider>
|
package/src/App.walk.test.tsx
CHANGED
|
@@ -162,7 +162,7 @@ describe("the guided walk", () => {
|
|
|
162
162
|
expect(setup.captureCharFrame()).toContain("3/3 files viewed");
|
|
163
163
|
|
|
164
164
|
// Act
|
|
165
|
-
await
|
|
165
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
166
166
|
|
|
167
167
|
// Assert
|
|
168
168
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -253,7 +253,7 @@ describe("the guided walk", () => {
|
|
|
253
253
|
await pressKey(setup, "RETURN", { meta: true });
|
|
254
254
|
// nothing pending despite the note, so the confirm card defaults to approve
|
|
255
255
|
await waitForText(setup, "[Approve]");
|
|
256
|
-
await
|
|
256
|
+
await pressKey(setup, "RETURN", { meta: true });
|
|
257
257
|
|
|
258
258
|
// Assert
|
|
259
259
|
await waitForState(setup, () => server.core.sessionGet(session.id).status === "resolved");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { ansiScreenToLines } from "./ansi-screen";
|
|
3
|
+
|
|
4
|
+
describe("ansiScreenToLines", () => {
|
|
5
|
+
test("places text at the positioned cursor", () => {
|
|
6
|
+
const lines = ansiScreenToLines("\x1b[2;3Hhi", 10, 4);
|
|
7
|
+
|
|
8
|
+
expect(lines[0]).toBe("");
|
|
9
|
+
expect(lines[1]).toBe(" hi");
|
|
10
|
+
expect(lines[2]).toBe("");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("clears the screen before repositioning", () => {
|
|
14
|
+
const lines = ansiScreenToLines("\x1b[1;1Hold\x1b[2J\x1b[1;1Hnew", 10, 2);
|
|
15
|
+
|
|
16
|
+
expect(lines[0]).toBe("new");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("ignores colour, cursor-visibility, and clipboard sequences", () => {
|
|
20
|
+
const lines = ansiScreenToLines("\x1b[?25l\x1b[1;1H\x1b[32mgo\x1b]52;c;AA\x07", 10, 2);
|
|
21
|
+
|
|
22
|
+
expect(lines[0]).toBe("go");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("clamps writes outside the grid instead of throwing", () => {
|
|
26
|
+
expect(() => ansiScreenToLines("\x1b[99;99Hx", 5, 3)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Interpret an absolute-positioned ANSI byte stream (cursor moves plus printed text) into a grid of rows. */
|
|
2
|
+
|
|
3
|
+
const CSI = "\x1b[";
|
|
4
|
+
|
|
5
|
+
export function ansiScreenToLines(ansi: string, cols: number, rows: number): string[] {
|
|
6
|
+
const grid: string[][] = Array.from({ length: rows }, () =>
|
|
7
|
+
Array.from({ length: cols }, () => " "),
|
|
8
|
+
);
|
|
9
|
+
let row = 0;
|
|
10
|
+
let column = 0;
|
|
11
|
+
let index = 0;
|
|
12
|
+
|
|
13
|
+
const put = (character: string): void => {
|
|
14
|
+
if (row >= 0 && row < rows && column >= 0 && column < cols) grid[row]![column] = character;
|
|
15
|
+
column += 1;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
while (index < ansi.length) {
|
|
19
|
+
const character = ansi[index]!;
|
|
20
|
+
|
|
21
|
+
if (character === "\x1b") {
|
|
22
|
+
index = consumeEscape(ansi, index, (nextRow, nextColumn) => {
|
|
23
|
+
if (nextRow === "clear") {
|
|
24
|
+
for (const line of grid) line.fill(" ");
|
|
25
|
+
row = 0;
|
|
26
|
+
column = 0;
|
|
27
|
+
} else {
|
|
28
|
+
row = nextRow;
|
|
29
|
+
column = nextColumn;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (character === "\n") {
|
|
35
|
+
row += 1;
|
|
36
|
+
column = 0;
|
|
37
|
+
} else if (character === "\r") {
|
|
38
|
+
column = 0;
|
|
39
|
+
} else {
|
|
40
|
+
put(character);
|
|
41
|
+
}
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return grid.map((line) => line.join("").replace(/\s+$/, ""));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type EscapeEffect = (row: number | "clear", column: number) => void;
|
|
49
|
+
|
|
50
|
+
function consumeEscape(ansi: string, start: number, apply: EscapeEffect): number {
|
|
51
|
+
if (ansi[start + 1] === "]") {
|
|
52
|
+
const bell = ansi.indexOf("\x07", start);
|
|
53
|
+
|
|
54
|
+
return bell === -1 ? ansi.length : bell + 1;
|
|
55
|
+
}
|
|
56
|
+
if (ansi.slice(start, start + CSI.length) !== CSI) return start + 1;
|
|
57
|
+
let index = start + CSI.length;
|
|
58
|
+
|
|
59
|
+
while (index < ansi.length && !/[A-Za-z]/.test(ansi[index]!)) index += 1;
|
|
60
|
+
const final = ansi[index];
|
|
61
|
+
const parameters = ansi.slice(start + CSI.length, index);
|
|
62
|
+
|
|
63
|
+
if (final === "H" || final === "f") {
|
|
64
|
+
const [rowParameter = "1", columnParameter = "1"] = parameters.split(";");
|
|
65
|
+
|
|
66
|
+
apply(Math.max(0, Number(rowParameter) - 1), Math.max(0, Number(columnParameter) - 1));
|
|
67
|
+
} else if (final === "J" && parameters === "2") {
|
|
68
|
+
apply("clear", 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return index + 1;
|
|
72
|
+
}
|
package/src/app-screens.tsx
CHANGED
|
@@ -388,7 +388,6 @@ export function NoThreadShell(props: {
|
|
|
388
388
|
<PromptDialog
|
|
389
389
|
isOpen
|
|
390
390
|
title=" rename thread "
|
|
391
|
-
label="new title for this thread:"
|
|
392
391
|
value={mode.text}
|
|
393
392
|
placeholder="a short title"
|
|
394
393
|
onInput={(text) => setMode({ ...mode, text })}
|
|
@@ -412,8 +411,11 @@ export function CompletionScreen(props: {
|
|
|
412
411
|
verdict: VerdictKind;
|
|
413
412
|
completion: { phase: "prompt" } | { phase: "counting"; remaining: number };
|
|
414
413
|
status: string;
|
|
414
|
+
onClose: () => void;
|
|
415
|
+
onBackToPlan: () => void;
|
|
416
|
+
onAlways: () => void;
|
|
415
417
|
}): React.ReactNode {
|
|
416
|
-
const { theme, session, verdict, completion, status } = props;
|
|
418
|
+
const { theme, session, verdict, completion, status, onClose, onBackToPlan, onAlways } = props;
|
|
417
419
|
|
|
418
420
|
return (
|
|
419
421
|
<ThemeProvider theme={theme}>
|
|
@@ -421,6 +423,9 @@ export function CompletionScreen(props: {
|
|
|
421
423
|
verdict={verdict}
|
|
422
424
|
completion={completion}
|
|
423
425
|
status={status}
|
|
426
|
+
onClose={onClose}
|
|
427
|
+
onBackToPlan={onBackToPlan}
|
|
428
|
+
onAlways={onAlways}
|
|
424
429
|
returnsTo={
|
|
425
430
|
returnPaneFor(session.artifact.meta.herdrPane)
|
|
426
431
|
? (session.artifact.meta.agent ?? "the agent")
|
|
@@ -510,7 +515,6 @@ export function TrailingOverlays(props: {
|
|
|
510
515
|
<PromptDialog
|
|
511
516
|
isOpen
|
|
512
517
|
title=" rename thread "
|
|
513
|
-
label="new title for this thread:"
|
|
514
518
|
value={mode.text}
|
|
515
519
|
placeholder="a short title"
|
|
516
520
|
onInput={(text) => setMode({ ...mode, text })}
|
|
@@ -532,6 +536,19 @@ export function TrailingOverlays(props: {
|
|
|
532
536
|
theme={theme}
|
|
533
537
|
/>
|
|
534
538
|
) : null}
|
|
539
|
+
{mode.type === "renameSelf" ? (
|
|
540
|
+
<PromptDialog
|
|
541
|
+
isOpen
|
|
542
|
+
title=" Display name "
|
|
543
|
+
label="Your display name - it attributes the notes you leave:"
|
|
544
|
+
value={mode.text}
|
|
545
|
+
placeholder="your name"
|
|
546
|
+
onInput={(text) => setMode({ ...mode, text })}
|
|
547
|
+
onSave={() => dispatch({ type: "confirmDialog" })}
|
|
548
|
+
onCancel={() => setMode({ type: "normal" })}
|
|
549
|
+
theme={theme}
|
|
550
|
+
/>
|
|
551
|
+
) : null}
|
|
535
552
|
{mode.type === "treePrompt" ? (
|
|
536
553
|
<PromptDialog
|
|
537
554
|
isOpen
|
package/src/app-view-model.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Mode } from "./intent-dispatch";
|
|
|
4
4
|
import type { Intent, KeyState } from "./keymap";
|
|
5
5
|
import type { Completion } from "./thread-controller";
|
|
6
6
|
import type { WalkFile } from "./walk";
|
|
7
|
+
import type { QuickAction } from "./config";
|
|
7
8
|
import { viewedCount } from "./walk";
|
|
8
9
|
import type { ConfirmCardProps } from "./components/ConfirmCard";
|
|
9
10
|
import type { BreadcrumbItem } from "./components/Breadcrumb";
|
|
@@ -57,6 +58,7 @@ export function resolveOverlay(
|
|
|
57
58
|
mode.type === "treePrompt"
|
|
58
59
|
)
|
|
59
60
|
return "prompt";
|
|
61
|
+
if (mode.type === "shareChoice") return "shareChoice";
|
|
60
62
|
if (mode.type === "spanActions") return "spanActions";
|
|
61
63
|
if (completionPhase === "prompt") return "completion-prompt";
|
|
62
64
|
if (completionPhase === "counting") return "completion-counting";
|
|
@@ -135,6 +137,7 @@ export function buildSubmitConfirmState(
|
|
|
135
137
|
session: Thread;
|
|
136
138
|
walkFileList: WalkFile[];
|
|
137
139
|
viewedPaths: Set<string>;
|
|
140
|
+
quickActions: QuickAction[];
|
|
138
141
|
},
|
|
139
142
|
): Omit<ConfirmCardProps, "theme"> | null {
|
|
140
143
|
const { mode, isDiff, session, walkFileList, viewedPaths, liveInput, setMode, dispatch } = deps;
|
|
@@ -144,6 +147,7 @@ export function buildSubmitConfirmState(
|
|
|
144
147
|
return {
|
|
145
148
|
verdict: mode.verdict,
|
|
146
149
|
summary: mode.summary,
|
|
150
|
+
quickActions: deps.quickActions,
|
|
147
151
|
viewedSummary:
|
|
148
152
|
isDiff && session.viewedPaths !== undefined
|
|
149
153
|
? `${viewedCount(walkFileList, viewedPaths)}/${walkFileList.length} files viewed`
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
// A project file shown as its own annotatable surface: the file's lines render through the diff
|
|
2
2
|
// sheet as context rows (one line-number gutter, no +/- sign), so selecting text leaves a comment
|
|
3
|
-
// anchored to the file exactly as a plan or diff comment is. Used where a
|
|
3
|
+
// anchored to the file exactly as a plan or diff comment is. Used where a thread exists to
|
|
4
4
|
// hold the notes; the bare-launch welcome shell uses the read-only FileContentsView instead.
|
|
5
5
|
|
|
6
6
|
import React, { useEffect, useRef, useState } from "react";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
makeAnchor,
|
|
9
|
+
annotationTarget,
|
|
10
|
+
type Anchor,
|
|
11
|
+
type Annotation,
|
|
12
|
+
type Thread,
|
|
13
|
+
} from "@cueloop/schema";
|
|
8
14
|
import type { Theme } from "../theme";
|
|
9
15
|
import type { QuickAction } from "../config";
|
|
10
16
|
import { useComponentTheme } from "./theme-context";
|
|
@@ -28,6 +34,7 @@ export interface AnnotatableFileViewProps {
|
|
|
28
34
|
onAddComment: (anchor: Anchor, body: string) => void;
|
|
29
35
|
onReply: (rootAnnotationId: string, body: string) => void;
|
|
30
36
|
onUpdateAnnotation: (id: string, body: string) => void;
|
|
37
|
+
resolveAuthorLabel?: (annotation: Annotation) => string | undefined;
|
|
31
38
|
onExit: () => void;
|
|
32
39
|
theme?: Theme;
|
|
33
40
|
}
|
|
@@ -119,6 +126,7 @@ export function AnnotatableFileView(props: AnnotatableFileViewProps): React.Reac
|
|
|
119
126
|
}
|
|
120
127
|
onReply={props.onReply}
|
|
121
128
|
onUpdateAnnotation={props.onUpdateAnnotation}
|
|
129
|
+
resolveAuthorLabel={props.resolveAuthorLabel}
|
|
122
130
|
onExit={props.onExit}
|
|
123
131
|
theme={props.theme}
|
|
124
132
|
/>
|