@cueloop/client 0.1.0-alpha.102 → 0.1.0-alpha.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/annotations/annotation-palette.ts +1 -0
- package/src/annotations/components/AnnotationCards.tsx +4 -0
- package/src/annotations/components/DiscussionMarkerRail.tsx +3 -0
- package/src/annotations/discussions.ts +1 -0
- package/src/annotations/thread-selection.ts +6 -0
- package/src/annotations/use-annotation-surface.tsx +97 -0
- package/src/app/App.deleted-scroll.test.tsx +3 -0
- package/src/app/App.harness-settings.test.tsx +77 -0
- package/src/app/App.plan-render.test.tsx +2 -0
- package/src/app/App.plan-review.test.tsx +1 -0
- package/src/app/App.test.tsx +1 -0
- package/src/app/App.tsx +67 -1
- package/src/app/App.visual.test.tsx +1 -0
- package/src/app/App.walk.test.tsx +1 -0
- package/src/app/App.workbench.test.tsx +1 -0
- package/src/app/app-screens.tsx +1 -0
- package/src/app/app-view-model.ts +7 -0
- package/src/app/components/WorkspacePanels.tsx +1 -0
- package/src/app/intent-dispatch.test.ts +1 -0
- package/src/app/intent-dispatch.ts +26 -0
- package/src/app/ready-signal.ts +2 -0
- package/src/app/run.ts +2 -0
- package/src/app/serve.test.ts +4 -0
- package/src/diff/components/AnnotatableFileView.tsx +1 -0
- package/src/diff/components/DiffContentView.scroll.test.tsx +9 -0
- package/src/diff/components/DiffContentView.syntax.test.tsx +1 -0
- package/src/diff/components/DiffContentView.tsx +25 -0
- package/src/diff/components/diff-content-view-layout.ts +3 -0
- package/src/diff/diff-fold.ts +4 -0
- package/src/diff/diff-hunk-curate.ts +3 -0
- package/src/diff/diff-intraline.ts +6 -0
- package/src/diff/diff-syntax.ts +8 -0
- package/src/diff/split-diff.ts +6 -0
- package/src/diff/view-diff.ts +18 -0
- package/src/diff/walk.ts +3 -0
- package/src/integrations/attribution.ts +2 -0
- package/src/integrations/client-extension-registry.ts +4 -0
- package/src/integrations/clipboard.ts +1 -0
- package/src/integrations/components/ShareDialog.agent.test.tsx +2 -0
- package/src/integrations/components/ShareDialog.tsx +28 -0
- package/src/integrations/editor.ts +3 -0
- package/src/integrations/github-identity.test.ts +1 -0
- package/src/integrations/github-identity.ts +1 -0
- package/src/integrations/herdr.ts +2 -0
- package/src/integrations/pasted-image.ts +2 -0
- package/src/integrations/share.ts +4 -0
- package/src/keyboard/components/HandleChipsInput.tsx +4 -0
- package/src/keyboard/components/SlashComposer.tsx +3 -0
- package/src/keyboard/key-bindings.ts +3 -0
- package/src/keyboard/keymap.ts +51 -0
- package/src/keyboard/skills.ts +2 -0
- package/src/keyboard/slash-palette.ts +8 -0
- package/src/keyboard/thread-chords.ts +5 -0
- package/src/keyboard/use-shared-keyboard.ts +1 -0
- package/src/markdown/components/MarkdownThreadEditor.tsx +3 -0
- package/src/markdown/components/ThreadView.selection.test.tsx +3 -0
- package/src/markdown/components/ThreadView.tsx +13 -0
- package/src/markdown/mark-runs.ts +4 -0
- package/src/markdown/markdown-editor-syntax-style.ts +1 -0
- package/src/markdown/markdown-grid.ts +6 -0
- package/src/markdown/markdown-highlight.ts +1 -0
- package/src/markdown/thread-code-syntax.ts +3 -0
- package/src/markdown/view-plan.ts +52 -0
- package/src/performance/perf-report.ts +1 -0
- package/src/performance/perf-timings.ts +1 -0
- package/src/prototype/components/PrototypeContentView.tsx +17 -0
- package/src/prototype/prototype-browser.ts +11 -0
- package/src/settings/components/SettingsDialog.tsx +3 -0
- package/src/settings/config.test.ts +41 -0
- package/src/settings/config.ts +44 -0
- package/src/settings/use-settings-dialog.test.tsx +98 -0
- package/src/settings/use-settings-dialog.tsx +38 -1
- package/src/settings/use-thread-harness-setting.test.tsx +87 -0
- package/src/settings/use-thread-harness-setting.ts +86 -0
- package/src/stories/stories.test.tsx +4 -0
- package/src/stories/story.ts +6 -0
- package/src/terminal/ansi-screen.ts +5 -0
- package/src/terminal/components/terminal-pane.ts +12 -0
- package/src/terminal/ghostty-terminal.ts +10 -0
- package/src/terminal/kitty-image.ts +3 -0
- package/src/terminal/pty.ts +9 -0
- package/src/testing/test-support.test.ts +2 -0
- package/src/testing/test-support.ts +16 -0
- package/src/thread/agent-continuation.test.ts +2 -0
- package/src/thread/agent-transcript.ts +7 -0
- package/src/thread/components/AgentConfigControls.tsx +1 -0
- package/src/thread/components/AgentThreadPane.test.tsx +31 -0
- package/src/thread/components/AgentThreadPane.tsx +12 -0
- package/src/thread/components/ThreadTree.tsx +1 -0
- package/src/thread/components/thread-tree.test.ts +1 -0
- package/src/thread/thread-controller.curate.test.ts +2 -0
- package/src/thread/thread-controller.ts +123 -0
- package/src/thread/tree-view.ts +4 -0
- package/src/thread/use-thread-agent.test.tsx +1 -0
- package/src/thread/use-thread-agent.ts +5 -0
- package/src/ui/components/OverlayScrollbar.test.tsx +3 -0
- package/src/ui/components/OverlayScrollbar.tsx +5 -0
- package/src/ui/components/PromptDialog.tsx +2 -0
- package/src/ui/components/RootOverlay.tsx +1 -0
- package/src/ui/components/ScrollArea.tsx +2 -0
- package/src/ui/components/primitives/Button.tsx +1 -0
- package/src/ui/components/primitives/Tabs.tsx +1 -0
- package/src/ui/components/primitives/Tree.tsx +4 -0
- package/src/ui/components/primitives/tree-model.ts +7 -0
- package/src/ui/components/syntax-highlight.ts +5 -0
- package/src/ui/components/truncate-title.ts +1 -0
- package/src/ui/use-frame-measure.ts +3 -0
- package/src/ui/use-terminal-virtualizer.ts +6 -0
- package/src/workbench/components/ChangesColumn.tsx +7 -0
- package/src/workbench/components/ChangesFileTree.test.tsx +3 -0
- package/src/workbench/components/EditorGrid.tsx +8 -0
- package/src/workbench/components/FileContentsView.tsx +2 -0
- package/src/workbench/components/GridTabContent.tsx +2 -0
- package/src/workbench/components/ProjectTreeView.test.tsx +3 -0
- package/src/workbench/components/ProjectTreeView.tsx +8 -0
- package/src/workbench/components/TreePane.tsx +2 -0
- package/src/workbench/components/editor-grid.ts +3 -0
- package/src/workbench/components/file-tree.ts +2 -0
- package/src/workbench/use-changes-workbench.ts +5 -0
- package/src/workbench/use-remember-layout.ts +2 -0
- package/src/workbench/use-repo-changes.ts +2 -0
|
@@ -73,9 +73,11 @@ test("deleted file caret scrolls one row and stays at the file boundaries", asyn
|
|
|
73
73
|
const box = findScrollbox(setup.renderer.root);
|
|
74
74
|
|
|
75
75
|
if (!box) throw new Error("file diff scrollbox missing");
|
|
76
|
+
|
|
76
77
|
for (let offset = 0; offset < 1000; offset += 20) {
|
|
77
78
|
box.scrollTo({ x: 0, y: offset });
|
|
78
79
|
await settle(setup);
|
|
80
|
+
|
|
79
81
|
if (setup.captureCharFrame().includes("row 130")) break;
|
|
80
82
|
}
|
|
81
83
|
const target = locateText(setup, "row 130");
|
|
@@ -127,6 +129,7 @@ test("deleted file caret scrolls one row and stays at the file boundaries", asyn
|
|
|
127
129
|
.findIndex((line) => line.includes("▎"));
|
|
128
130
|
|
|
129
131
|
if (marker === box.viewport.screenY) anchored = true;
|
|
132
|
+
|
|
130
133
|
if (anchored && box.scrollTop > 0) expect(marker).toBe(box.viewport.screenY);
|
|
131
134
|
}
|
|
132
135
|
expect(anchored).toBe(true);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { DaemonServer } from "@cueloop/daemon";
|
|
7
|
+
import type { ThreadAgentState } from "@cueloop/schema";
|
|
8
|
+
import { App } from "./App";
|
|
9
|
+
import {
|
|
10
|
+
clickText,
|
|
11
|
+
isolateUserConfig,
|
|
12
|
+
isolatedUserConfigPath,
|
|
13
|
+
renderReadyApp,
|
|
14
|
+
waitForText,
|
|
15
|
+
waitForTextGone,
|
|
16
|
+
} from "../testing/test-support";
|
|
17
|
+
|
|
18
|
+
test("repeated harness toggle failures show feedback without losing the controller binding", async () => {
|
|
19
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-harness-settings-"));
|
|
20
|
+
const restore = isolateUserConfig(home);
|
|
21
|
+
const server = new DaemonServer({ home, idleExitMs: 0 });
|
|
22
|
+
|
|
23
|
+
writeFileSync(isolatedUserConfigPath(home), "[experimental]\nthread_agent = true\n");
|
|
24
|
+
server.start();
|
|
25
|
+
const thread = server.core.sessionCreate({
|
|
26
|
+
workspace: { repoRoot: home, branch: "main" },
|
|
27
|
+
artifact: { type: "plan", content: "# Switching review", meta: { title: "Switching review" } },
|
|
28
|
+
});
|
|
29
|
+
const state: ThreadAgentState = {
|
|
30
|
+
threadId: thread.id,
|
|
31
|
+
phase: { kind: "idle" },
|
|
32
|
+
messages: [],
|
|
33
|
+
comments: [],
|
|
34
|
+
tools: [],
|
|
35
|
+
};
|
|
36
|
+
let switches = 0;
|
|
37
|
+
const setup = await renderReadyApp(
|
|
38
|
+
<App
|
|
39
|
+
home={home}
|
|
40
|
+
sessionId={thread.id}
|
|
41
|
+
agentClient={{
|
|
42
|
+
agentGet: async () => state,
|
|
43
|
+
agentPrompt: async () => state,
|
|
44
|
+
agentCancel: async () => state,
|
|
45
|
+
agentComment: async () => state,
|
|
46
|
+
agentPermission: async () => state,
|
|
47
|
+
agentConfigure: async (params) => {
|
|
48
|
+
if (!params.configId) return state;
|
|
49
|
+
|
|
50
|
+
switches++;
|
|
51
|
+
throw new Error(`Switch failure ${switches}`);
|
|
52
|
+
},
|
|
53
|
+
}}
|
|
54
|
+
/>,
|
|
55
|
+
{ width: 120, height: 32 },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await setup.mockMouse.click(1, 0);
|
|
60
|
+
await waitForText(setup, "settings");
|
|
61
|
+
await clickText(setup, "settings");
|
|
62
|
+
await waitForText(setup, "General");
|
|
63
|
+
await clickText(setup, "Thread ");
|
|
64
|
+
await waitForText(setup, "Harness");
|
|
65
|
+
await clickText(setup, "Harness");
|
|
66
|
+
await waitForText(setup, "Switch failure 1");
|
|
67
|
+
await waitForTextGone(setup, "Switch failure 1");
|
|
68
|
+
await clickText(setup, "Harness");
|
|
69
|
+
await waitForText(setup, "Switch failure 2");
|
|
70
|
+
expect(switches).toBe(2);
|
|
71
|
+
} finally {
|
|
72
|
+
setup.renderer.destroy();
|
|
73
|
+
server.stop();
|
|
74
|
+
restore();
|
|
75
|
+
rmSync(home, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
@@ -91,6 +91,7 @@ function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
|
91
91
|
for (const line of setup.captureSpans().lines) {
|
|
92
92
|
for (const span of line.spans) {
|
|
93
93
|
if (span.fg === undefined || !span.text.includes(needle)) continue;
|
|
94
|
+
|
|
94
95
|
const [red, green, blue] = span.fg.toInts();
|
|
95
96
|
|
|
96
97
|
foregrounds.push(
|
|
@@ -109,6 +110,7 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
|
109
110
|
for (const line of setup.captureSpans().lines) {
|
|
110
111
|
for (const span of line.spans) {
|
|
111
112
|
if (!span.text.includes(needle)) continue;
|
|
113
|
+
|
|
112
114
|
const [red, green, blue] = span.bg.toInts();
|
|
113
115
|
|
|
114
116
|
backgrounds.push(
|
|
@@ -84,6 +84,7 @@ function backgroundsOf(setup: Setup, needle: string): string[] {
|
|
|
84
84
|
for (const line of setup.captureSpans().lines) {
|
|
85
85
|
for (const span of line.spans) {
|
|
86
86
|
if (!span.text.includes(needle)) continue;
|
|
87
|
+
|
|
87
88
|
const [red, green, blue] = span.bg.toInts();
|
|
88
89
|
|
|
89
90
|
backgrounds.push(
|
package/src/app/App.test.tsx
CHANGED
|
@@ -78,6 +78,7 @@ function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
|
78
78
|
for (const line of setup.captureSpans().lines) {
|
|
79
79
|
for (const span of line.spans) {
|
|
80
80
|
if (!span.text.includes(needle)) continue;
|
|
81
|
+
|
|
81
82
|
const [red, green, blue] = span.fg.toInts();
|
|
82
83
|
|
|
83
84
|
foregrounds.push(
|
package/src/app/App.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useThreadHarnessSetting } from "../settings/use-thread-harness-setting";
|
|
1
2
|
import React, {
|
|
2
3
|
useCallback,
|
|
3
4
|
useEffect,
|
|
@@ -203,6 +204,7 @@ export function quitKeyHandled(
|
|
|
203
204
|
onExit?: (code: number) => void,
|
|
204
205
|
): boolean {
|
|
205
206
|
if (!key.ctrl || key.name !== "q") return false;
|
|
207
|
+
|
|
206
208
|
onExit?.(0);
|
|
207
209
|
|
|
208
210
|
return true;
|
|
@@ -214,6 +216,7 @@ function menuModalHandled(
|
|
|
214
216
|
key: { name: string },
|
|
215
217
|
): boolean {
|
|
216
218
|
if (menuControl.openMenuId === null) return false;
|
|
219
|
+
|
|
217
220
|
if (key.name === "escape") menuControl.closeMenu();
|
|
218
221
|
|
|
219
222
|
return true;
|
|
@@ -300,7 +303,9 @@ function chooseThreadBody(choice: {
|
|
|
300
303
|
threadView: React.ReactNode;
|
|
301
304
|
}): React.ReactNode {
|
|
302
305
|
if (choice.isPixelPrototype) return choice.prototype;
|
|
306
|
+
|
|
303
307
|
if (choice.isDiff) return choice.diffPlaceholder;
|
|
308
|
+
|
|
304
309
|
if (choice.editingBody) return choice.editor;
|
|
305
310
|
|
|
306
311
|
return choice.threadView;
|
|
@@ -453,16 +458,19 @@ export function threadsNavHandled(params: {
|
|
|
453
458
|
const { focusedPane, quiet, key, count } = params;
|
|
454
459
|
|
|
455
460
|
if (focusedPane !== "threads" || !quiet || count === 0) return false;
|
|
461
|
+
|
|
456
462
|
if (key.name === "j" || key.name === "down") {
|
|
457
463
|
params.setInboxCursor((cursor) => Math.min(cursor + 1, count - 1));
|
|
458
464
|
|
|
459
465
|
return true;
|
|
460
466
|
}
|
|
467
|
+
|
|
461
468
|
if (key.name === "k" || key.name === "up") {
|
|
462
469
|
params.setInboxCursor((cursor) => Math.max(cursor - 1, 0));
|
|
463
470
|
|
|
464
471
|
return true;
|
|
465
472
|
}
|
|
473
|
+
|
|
466
474
|
if (key.name === "return" || key.name === "enter") {
|
|
467
475
|
params.openSession();
|
|
468
476
|
|
|
@@ -490,8 +498,11 @@ export function visiblePanes(
|
|
|
490
498
|
const panes: FocusPane[] = [];
|
|
491
499
|
|
|
492
500
|
if (sidebarOpen) panes.push("threads");
|
|
501
|
+
|
|
493
502
|
if (threadShown) panes.push("thread");
|
|
503
|
+
|
|
494
504
|
if (changesOpen) panes.push("changes");
|
|
505
|
+
|
|
495
506
|
if (projectOpen) panes.push("project");
|
|
496
507
|
|
|
497
508
|
return panes;
|
|
@@ -503,6 +514,7 @@ export function nextFocusPane(
|
|
|
503
514
|
backward: boolean,
|
|
504
515
|
): FocusPane {
|
|
505
516
|
if (panes.length === 0) return current;
|
|
517
|
+
|
|
506
518
|
const index = panes.indexOf(current);
|
|
507
519
|
const step = backward ? -1 : 1;
|
|
508
520
|
|
|
@@ -617,6 +629,7 @@ export function App({
|
|
|
617
629
|
|
|
618
630
|
if (next.has(id)) next.delete(id);
|
|
619
631
|
else next.add(id);
|
|
632
|
+
|
|
620
633
|
persistPins([...next]);
|
|
621
634
|
|
|
622
635
|
return next;
|
|
@@ -688,6 +701,7 @@ export function App({
|
|
|
688
701
|
|
|
689
702
|
setPrototypePixels(config.experimental.prototypePixels);
|
|
690
703
|
setThreadAgentEnabled(config.experimental.threadAgent);
|
|
704
|
+
setThreadHarness(config.thread.harness);
|
|
691
705
|
keysRef.current = config.keys;
|
|
692
706
|
keyBindings.setKeys(config.keys);
|
|
693
707
|
setTheme(composeTheme(config.ui.theme, config.themeOverrides, appearance));
|
|
@@ -714,6 +728,7 @@ export function App({
|
|
|
714
728
|
|
|
715
729
|
useEffect(() => {
|
|
716
730
|
if (!toast) return;
|
|
731
|
+
|
|
717
732
|
const timer = setTimeout(() => controller.dismissToast(), TOAST_DISMISS_MS);
|
|
718
733
|
|
|
719
734
|
return () => clearTimeout(timer);
|
|
@@ -725,6 +740,7 @@ export function App({
|
|
|
725
740
|
|
|
726
741
|
useEffect(() => {
|
|
727
742
|
if (promptedSelfRef.current || role !== "collaborator" || !selfAuthor || !session) return;
|
|
743
|
+
|
|
728
744
|
promptedSelfRef.current = true;
|
|
729
745
|
const known = session.participants?.find((participant) => participant.id === selfAuthor)?.name;
|
|
730
746
|
|
|
@@ -747,13 +763,31 @@ export function App({
|
|
|
747
763
|
|
|
748
764
|
void resolveGithubIdentity().then((github) => {
|
|
749
765
|
if (identityGenerationRef.current !== generation) return;
|
|
766
|
+
|
|
750
767
|
if (!github) return controller.setStatus("GitHub not connected - run gh auth login");
|
|
768
|
+
|
|
751
769
|
const name = github.name?.trim() || github.login;
|
|
752
770
|
|
|
753
771
|
applyIdentity({ name, provider: "github" });
|
|
754
772
|
});
|
|
755
773
|
};
|
|
756
774
|
|
|
775
|
+
const [agentState, setAgentState] = useState<import("@cueloop/schema").ThreadAgentState>();
|
|
776
|
+
const [threadHarness, setThreadHarness] = useState<"pi" | "fx">("pi");
|
|
777
|
+
|
|
778
|
+
const harnessSetting = useThreadHarnessSetting({
|
|
779
|
+
home,
|
|
780
|
+
thread: session,
|
|
781
|
+
enabled: threadAgentEnabled,
|
|
782
|
+
owner: isOwner,
|
|
783
|
+
state: agentState,
|
|
784
|
+
preferred: threadHarness,
|
|
785
|
+
client: agentClient,
|
|
786
|
+
onState: setAgentState,
|
|
787
|
+
onPreferred: setThreadHarness,
|
|
788
|
+
onError: (message) => controller.setStatus(message),
|
|
789
|
+
});
|
|
790
|
+
|
|
757
791
|
// ── settings dialog: config-backed model, navigation, persistence ──
|
|
758
792
|
const {
|
|
759
793
|
settingsNav,
|
|
@@ -787,6 +821,9 @@ export function App({
|
|
|
787
821
|
reviewSkill,
|
|
788
822
|
reviewWorkspace,
|
|
789
823
|
setReviewWorkspace,
|
|
824
|
+
canSwitchHarness: harnessSetting.enabled,
|
|
825
|
+
threadHarness: harnessSetting.harness,
|
|
826
|
+
onSwitchHarness: harnessSetting.switchHarness,
|
|
790
827
|
});
|
|
791
828
|
|
|
792
829
|
// ── derived view model ──────────────────────
|
|
@@ -843,7 +880,6 @@ export function App({
|
|
|
843
880
|
() => undefined,
|
|
844
881
|
);
|
|
845
882
|
const agentInvoke = useRef<((commentId?: string) => void) | undefined>(undefined);
|
|
846
|
-
const [agentState, setAgentState] = useState<import("@cueloop/schema").ThreadAgentState>();
|
|
847
883
|
const [prototypeComposing, setPrototypeComposing] = useState(false);
|
|
848
884
|
const [welcomeComposing, setWelcomeComposing] = useState(false);
|
|
849
885
|
// inline body edit: the markdown editor owns the thread pane and all keys while open
|
|
@@ -868,9 +904,11 @@ export function App({
|
|
|
868
904
|
|
|
869
905
|
const openCardEdit = (annotationId: string): void => {
|
|
870
906
|
if (observer) return controller.setStatus("observer - read-only");
|
|
907
|
+
|
|
871
908
|
const annotation = session?.annotations.find((candidate) => candidate.id === annotationId);
|
|
872
909
|
|
|
873
910
|
if (!annotation) return;
|
|
911
|
+
|
|
874
912
|
// a collaborator's note is theirs to word: activating it (click or e) renames
|
|
875
913
|
// the author rather than editing the body the planner does not own
|
|
876
914
|
if (annotation.author) {
|
|
@@ -882,7 +920,9 @@ export function App({
|
|
|
882
920
|
text: authorNames[annotation.author] ?? "",
|
|
883
921
|
});
|
|
884
922
|
}
|
|
923
|
+
|
|
885
924
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
925
|
+
|
|
886
926
|
liveInput.current = annotation.body;
|
|
887
927
|
setMode({ type: "railEdit", id: annotation.id, text: annotation.body });
|
|
888
928
|
};
|
|
@@ -894,6 +934,7 @@ export function App({
|
|
|
894
934
|
|
|
895
935
|
return;
|
|
896
936
|
}
|
|
937
|
+
|
|
897
938
|
renderer?.suspend();
|
|
898
939
|
try {
|
|
899
940
|
controller.edit();
|
|
@@ -949,6 +990,7 @@ export function App({
|
|
|
949
990
|
},
|
|
950
991
|
openShareDialog: () => {
|
|
951
992
|
if (!isOwner) return controller.setStatus("only the plan owner can share");
|
|
993
|
+
|
|
952
994
|
shareDialogStore.getState().reset();
|
|
953
995
|
setShareDialogOpen(true);
|
|
954
996
|
},
|
|
@@ -974,6 +1016,7 @@ export function App({
|
|
|
974
1016
|
useEffect(() => {
|
|
975
1017
|
// the bare shell drives its panes from its own workbench, so only reconcile against this one
|
|
976
1018
|
if (!session) return;
|
|
1019
|
+
|
|
977
1020
|
const next = reconciledFocus(navigablePanes, focusedPane);
|
|
978
1021
|
|
|
979
1022
|
if (next) setFocusedPane(next);
|
|
@@ -995,6 +1038,7 @@ export function App({
|
|
|
995
1038
|
});
|
|
996
1039
|
|
|
997
1040
|
if (!intent) return false;
|
|
1041
|
+
|
|
998
1042
|
if (intent.type === "cut" && selection) {
|
|
999
1043
|
controller.cut(
|
|
1000
1044
|
selection.start.blockIndex,
|
|
@@ -1010,9 +1054,11 @@ export function App({
|
|
|
1010
1054
|
const openThread = (id: string): void => {
|
|
1011
1055
|
// save the open editor into the leaving thread's working copy before the switch, so a click away never drops edits
|
|
1012
1056
|
if (bodyEditing.editing) bodyEditing.requestExit();
|
|
1057
|
+
|
|
1013
1058
|
const index = grouped.ordered.findIndex((thread) => thread.id === id);
|
|
1014
1059
|
|
|
1015
1060
|
if (index >= 0) setInboxCursor(() => index);
|
|
1061
|
+
|
|
1016
1062
|
controller.open(id);
|
|
1017
1063
|
};
|
|
1018
1064
|
|
|
@@ -1038,7 +1084,9 @@ export function App({
|
|
|
1038
1084
|
!key.super
|
|
1039
1085
|
)
|
|
1040
1086
|
return true;
|
|
1087
|
+
|
|
1041
1088
|
if (!threadViewActive || threadViewSuspended) return false;
|
|
1089
|
+
|
|
1042
1090
|
if (!threadComposing) {
|
|
1043
1091
|
const chord = resolveSessionChord(key, { isOwner, resolved });
|
|
1044
1092
|
|
|
@@ -1050,14 +1098,19 @@ export function App({
|
|
|
1050
1098
|
|
|
1051
1099
|
useKeyboard((key) => {
|
|
1052
1100
|
if (quitKeyHandled(key, onExit)) return;
|
|
1101
|
+
|
|
1053
1102
|
// the inline body editor owns the pane and every key while open
|
|
1054
1103
|
if (bodyEditing.editing) return;
|
|
1104
|
+
|
|
1055
1105
|
// the share dialog owns its own keys while open; the shell grammar stands down
|
|
1056
1106
|
if (shareDialogOpen) return;
|
|
1107
|
+
|
|
1057
1108
|
if (menuModalHandled(menuControl, key)) return;
|
|
1109
|
+
|
|
1058
1110
|
if (paneCycleRequested(key, overlay, menuOwnsKeyboard, threadComposing)) {
|
|
1059
1111
|
return cyclePanes(Boolean(key.shift));
|
|
1060
1112
|
}
|
|
1113
|
+
|
|
1061
1114
|
if (
|
|
1062
1115
|
threadsNavHandled({
|
|
1063
1116
|
focusedPane,
|
|
@@ -1069,17 +1122,24 @@ export function App({
|
|
|
1069
1122
|
})
|
|
1070
1123
|
)
|
|
1071
1124
|
return;
|
|
1125
|
+
|
|
1072
1126
|
if (threadSurfaceHandledKey(key)) return;
|
|
1127
|
+
|
|
1073
1128
|
// A compose textarea owns the keyboard while open: let it receive the typed note instead of the
|
|
1074
1129
|
// global keymap acting on each letter (the prototype, and the bare-shell welcome playground).
|
|
1075
1130
|
if (prototypeComposing || welcomeComposing) return;
|
|
1131
|
+
|
|
1076
1132
|
if (menuDialog === "settings") return void handleSettingsKey(key.name);
|
|
1133
|
+
|
|
1077
1134
|
if (menuDialog) return void (key.name === "escape" && setMenuDialog(null));
|
|
1135
|
+
|
|
1078
1136
|
// the toast is non-modal: escape only dismisses it when nothing else owns
|
|
1079
1137
|
// escape, so an open overlay (compose, submit, prompt, walk) still cancels
|
|
1080
1138
|
if (toastDismissRequested(toast !== null, key, overlay, mode.type === "span"))
|
|
1081
1139
|
return controller.dismissToast();
|
|
1140
|
+
|
|
1082
1141
|
if (bareShellDefersKeys(session === null, overlay === "none", focusedPane)) return;
|
|
1142
|
+
|
|
1083
1143
|
const state = buildKeyState({
|
|
1084
1144
|
keys: keysRef.current,
|
|
1085
1145
|
observer,
|
|
@@ -1136,6 +1196,7 @@ export function App({
|
|
|
1136
1196
|
|
|
1137
1197
|
// ── render ──────────────────────────────────
|
|
1138
1198
|
if (error) return <ErrorScreen error={error} theme={theme} />;
|
|
1199
|
+
|
|
1139
1200
|
if (!session)
|
|
1140
1201
|
return inbox ? (
|
|
1141
1202
|
<SlashSkillsContext.Provider value={skills}>
|
|
@@ -1229,15 +1290,20 @@ export function App({
|
|
|
1229
1290
|
// A share viewer/observer has no Edit affordance (the button is hidden), so
|
|
1230
1291
|
// this is owner-only; stay silent rather than nag if it is ever reached.
|
|
1231
1292
|
if (!isOwner) return;
|
|
1293
|
+
|
|
1232
1294
|
if (resolved) return controller.setStatus("review submitted - read-only");
|
|
1295
|
+
|
|
1233
1296
|
openBodyEditor();
|
|
1234
1297
|
};
|
|
1235
1298
|
|
|
1236
1299
|
// clicking the rail Submit button: same read-only answer as the submit key
|
|
1237
1300
|
const onSubmitRequest = (): void => {
|
|
1238
1301
|
if (observer) return controller.setStatus("observer - read-only");
|
|
1302
|
+
|
|
1239
1303
|
if (!isOwner) return controller.setStatus("shared view - your notes save as you go");
|
|
1304
|
+
|
|
1240
1305
|
if (resolved) return;
|
|
1306
|
+
|
|
1241
1307
|
dispatch({ type: "openSubmit" });
|
|
1242
1308
|
};
|
|
1243
1309
|
|
|
@@ -32,6 +32,7 @@ function hasSpanColor(
|
|
|
32
32
|
for (const line of setup.captureSpans().lines) {
|
|
33
33
|
for (const span of line.spans) {
|
|
34
34
|
if (!span.text.includes(needle) || !span.fg) continue;
|
|
35
|
+
|
|
35
36
|
const [red, green, blue] = span.fg.toInts();
|
|
36
37
|
|
|
37
38
|
if (red === color[0] && green === color[1] && blue === color[2]) return true;
|
|
@@ -88,6 +88,7 @@ function foregroundsOf(setup: Setup, needle: string): string[] {
|
|
|
88
88
|
for (const line of setup.captureSpans().lines) {
|
|
89
89
|
for (const span of line.spans) {
|
|
90
90
|
if (!span.text.includes(needle)) continue;
|
|
91
|
+
|
|
91
92
|
const [red, green, blue] = span.fg.toInts();
|
|
92
93
|
|
|
93
94
|
foregrounds.push(
|
|
@@ -244,6 +244,7 @@ describe("the four-pane workbench", () => {
|
|
|
244
244
|
for (const [index, direction] of Array.from({ length: 500 }, () => "down" as const).entries()) {
|
|
245
245
|
// eslint-disable-next-line no-await-in-loop
|
|
246
246
|
await setup.mockMouse.scroll(75, 10, direction);
|
|
247
|
+
|
|
247
248
|
if (index % 25 === 0) {
|
|
248
249
|
// eslint-disable-next-line no-await-in-loop
|
|
249
250
|
await setup.waitForVisualIdle();
|
package/src/app/app-screens.tsx
CHANGED
|
@@ -395,6 +395,7 @@ export function NoThreadShell(props: {
|
|
|
395
395
|
message={confirming ? `Delete "${confirming.title}"? This removes the thread.` : ""}
|
|
396
396
|
onConfirm={() => {
|
|
397
397
|
if (confirming) controller.deleteSession(confirming.sessionId);
|
|
398
|
+
|
|
398
399
|
setMode({ type: "normal" });
|
|
399
400
|
}}
|
|
400
401
|
onCancel={() => setMode({ type: "normal" })}
|
|
@@ -49,8 +49,11 @@ export function resolveOverlay(
|
|
|
49
49
|
walking: boolean,
|
|
50
50
|
): KeyState["overlay"] {
|
|
51
51
|
if (mode.type === "compose" || mode.type === "railEdit") return "compose";
|
|
52
|
+
|
|
52
53
|
if (mode.type === "submit") return "submit";
|
|
54
|
+
|
|
53
55
|
if (mode.type === "confirmDelete") return "confirm";
|
|
56
|
+
|
|
54
57
|
if (
|
|
55
58
|
mode.type === "rename" ||
|
|
56
59
|
mode.type === "renameThread" ||
|
|
@@ -58,9 +61,13 @@ export function resolveOverlay(
|
|
|
58
61
|
mode.type === "treePrompt"
|
|
59
62
|
)
|
|
60
63
|
return "prompt";
|
|
64
|
+
|
|
61
65
|
if (mode.type === "spanActions") return "spanActions";
|
|
66
|
+
|
|
62
67
|
if (completionPhase === "prompt") return "completion-prompt";
|
|
68
|
+
|
|
63
69
|
if (completionPhase === "counting") return "completion-counting";
|
|
70
|
+
|
|
64
71
|
if (walking) return "walk";
|
|
65
72
|
|
|
66
73
|
return "none";
|
|
@@ -53,6 +53,7 @@ export function WorkspacePanels(props: {
|
|
|
53
53
|
|
|
54
54
|
useKeyboard((key) => {
|
|
55
55
|
if (!props.focused || (key.name !== "[" && key.name !== "]")) return;
|
|
56
|
+
|
|
56
57
|
const choices = [null, ...views];
|
|
57
58
|
const current = selected ? views.indexOf(selected) + 1 : 0;
|
|
58
59
|
const step = key.name === "]" ? 1 : -1;
|
|
@@ -194,6 +194,7 @@ describe("move", () => {
|
|
|
194
194
|
const advance = setCursor.mock.calls[0]![0];
|
|
195
195
|
|
|
196
196
|
if (!isUpdater(advance)) throw new Error("expected setCursor to receive an updater");
|
|
197
|
+
|
|
197
198
|
expect(advance(2)).toBe(2); // already at the end, stays
|
|
198
199
|
expect(advance(0)).toBe(1);
|
|
199
200
|
});
|
|
@@ -125,7 +125,9 @@ function isLandableRow(rows: DiffRow[], index: number): boolean {
|
|
|
125
125
|
const row = rows[index];
|
|
126
126
|
|
|
127
127
|
if (isCodeRow(row)) return true;
|
|
128
|
+
|
|
128
129
|
if (row?.kind !== "file") return false;
|
|
130
|
+
|
|
129
131
|
const next = rows[index + 1];
|
|
130
132
|
|
|
131
133
|
return next === undefined || next.kind === "file";
|
|
@@ -138,6 +140,7 @@ function nextCodeRowIndex(rows: DiffRow[], from: number, to: IntentOfType<"move"
|
|
|
138
140
|
|
|
139
141
|
return from;
|
|
140
142
|
}
|
|
143
|
+
|
|
141
144
|
if (to === "bottom") {
|
|
142
145
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
143
146
|
if (isLandableRow(rows, index)) return index;
|
|
@@ -145,6 +148,7 @@ function nextCodeRowIndex(rows: DiffRow[], from: number, to: IntentOfType<"move"
|
|
|
145
148
|
|
|
146
149
|
return from;
|
|
147
150
|
}
|
|
151
|
+
|
|
148
152
|
const step = to === "down" ? 1 : -1;
|
|
149
153
|
|
|
150
154
|
for (let index = from + step; index >= 0 && index < rows.length; index += step) {
|
|
@@ -160,6 +164,7 @@ function handleMove(intent: IntentOfType<"move">, deps: IntentDispatchDeps): voi
|
|
|
160
164
|
|
|
161
165
|
return;
|
|
162
166
|
}
|
|
167
|
+
|
|
163
168
|
const navigableCount = deps.display.length;
|
|
164
169
|
|
|
165
170
|
if (intent.to === "down") deps.setCursor((current) => Math.min(navigableCount - 1, current + 1));
|
|
@@ -206,6 +211,7 @@ function handleOpenRename(_intent: IntentOfType<"openRename">, deps: IntentDispa
|
|
|
206
211
|
|
|
207
212
|
return;
|
|
208
213
|
}
|
|
214
|
+
|
|
209
215
|
deps.setMode({
|
|
210
216
|
type: "rename",
|
|
211
217
|
authorId: focused.author,
|
|
@@ -225,6 +231,7 @@ function handleConfirmDialog(
|
|
|
225
231
|
else if (mode.type === "nameSelf") controller.setSelfName(mode.text.trim());
|
|
226
232
|
else if (mode.type === "renameSelf") deps.setLocalIdentityName(mode.text.trim());
|
|
227
233
|
else if (mode.type === "treePrompt") confirmTreePrompt(mode, deps);
|
|
234
|
+
|
|
228
235
|
deps.setMode({ type: "normal" });
|
|
229
236
|
}
|
|
230
237
|
|
|
@@ -232,6 +239,7 @@ function handleStartSpan(_intent: IntentOfType<"startSpan">, deps: IntentDispatc
|
|
|
232
239
|
const block = deps.display[deps.cursor];
|
|
233
240
|
|
|
234
241
|
if (!block?.work) return;
|
|
242
|
+
|
|
235
243
|
const span = startSpan(deps.cursor, displayText(block));
|
|
236
244
|
|
|
237
245
|
if (span) deps.setMode({ type: "span", span });
|
|
@@ -301,6 +309,7 @@ function handlePickSpanAction(
|
|
|
301
309
|
const { mode, session, controller } = deps;
|
|
302
310
|
|
|
303
311
|
if (mode.type !== "spanActions") return;
|
|
312
|
+
|
|
304
313
|
const action = deps.quickActions[intent.index ?? mode.index];
|
|
305
314
|
|
|
306
315
|
if (session && action) {
|
|
@@ -315,6 +324,7 @@ function handlePickSpanAction(
|
|
|
315
324
|
|
|
316
325
|
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
317
326
|
}
|
|
327
|
+
|
|
318
328
|
deps.setMode({ type: "normal" });
|
|
319
329
|
}
|
|
320
330
|
|
|
@@ -322,6 +332,7 @@ function handleOpenCompose(intent: IntentOfType<"openCompose">, deps: IntentDisp
|
|
|
322
332
|
const { mode } = deps;
|
|
323
333
|
|
|
324
334
|
deps.liveInput.current = "";
|
|
335
|
+
|
|
325
336
|
if (intent.from === "span" && mode.type === "span") {
|
|
326
337
|
deps.setMode({
|
|
327
338
|
type: "compose",
|
|
@@ -362,6 +373,7 @@ function handleOpenSubmit(_intent: IntentOfType<"openSubmit">, deps: IntentDispa
|
|
|
362
373
|
const { session } = deps;
|
|
363
374
|
|
|
364
375
|
if (!session) return;
|
|
376
|
+
|
|
365
377
|
deps.liveInput.current = "";
|
|
366
378
|
deps.setMode({ type: "submit", message: deps.defaultMessage, summary: "" });
|
|
367
379
|
}
|
|
@@ -387,6 +399,7 @@ function handleFoldFile(_intent: IntentOfType<"foldFile">, deps: IntentDispatchD
|
|
|
387
399
|
const file = deps.rows[deps.cursor]?.file;
|
|
388
400
|
|
|
389
401
|
if (!file || deps.controller.isFileCollapsed(file)) return;
|
|
402
|
+
|
|
390
403
|
deps.controller.setFileCollapsed(file, true);
|
|
391
404
|
// the file band is unchanged in index by its own collapse, so land the cursor on it
|
|
392
405
|
const headerIndex = deps.controller
|
|
@@ -400,6 +413,7 @@ function handleUnfoldFile(_intent: IntentOfType<"unfoldFile">, deps: IntentDispa
|
|
|
400
413
|
const file = deps.rows[deps.cursor]?.file;
|
|
401
414
|
|
|
402
415
|
if (!file || !deps.controller.isFileCollapsed(file)) return;
|
|
416
|
+
|
|
403
417
|
deps.controller.setFileCollapsed(file, false);
|
|
404
418
|
// drop the cursor onto the file's first code line now that its body is back
|
|
405
419
|
const firstCode = deps.controller.rows().findIndex((row) => row.file === file && isCodeRow(row));
|
|
@@ -423,6 +437,7 @@ function handleRestoreCuration(
|
|
|
423
437
|
const items = deps.controller.curationItems();
|
|
424
438
|
|
|
425
439
|
if (!items.length) return;
|
|
440
|
+
|
|
426
441
|
const targetId = deps.selectedCurationId ?? items[items.length - 1]!.id;
|
|
427
442
|
|
|
428
443
|
deps.controller.restoreCuration(targetId);
|
|
@@ -447,6 +462,7 @@ function handleAnnotationCycle(
|
|
|
447
462
|
);
|
|
448
463
|
|
|
449
464
|
if (!annotations.length) return;
|
|
465
|
+
|
|
450
466
|
const focusedIndex = annotations.findIndex(
|
|
451
467
|
(annotation) => annotation.id === deps.focusedAnnotationId,
|
|
452
468
|
);
|
|
@@ -500,11 +516,14 @@ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDis
|
|
|
500
516
|
|
|
501
517
|
if (mode.type === "railEdit") {
|
|
502
518
|
if (session && body) controller.updateAnnotation(mode.id, body);
|
|
519
|
+
|
|
503
520
|
deps.setMode({ type: "normal" });
|
|
504
521
|
|
|
505
522
|
return;
|
|
506
523
|
}
|
|
524
|
+
|
|
507
525
|
if (mode.type !== "compose") return;
|
|
526
|
+
|
|
508
527
|
if (session && body) {
|
|
509
528
|
const annotationId = controller.annotate(
|
|
510
529
|
mode.kind,
|
|
@@ -516,6 +535,7 @@ function handleSaveCompose(_intent: IntentOfType<"saveCompose">, deps: IntentDis
|
|
|
516
535
|
|
|
517
536
|
if (annotationId) deps.setFocusedAnnotationId(annotationId);
|
|
518
537
|
}
|
|
538
|
+
|
|
519
539
|
deps.setMode({ type: "normal" });
|
|
520
540
|
}
|
|
521
541
|
|
|
@@ -526,6 +546,7 @@ function handleSubmitMessage(
|
|
|
526
546
|
const { mode } = deps;
|
|
527
547
|
|
|
528
548
|
if (mode.type === "submit") deps.controller.submit(mode.message, deps.liveInput.current);
|
|
549
|
+
|
|
529
550
|
deps.setMode({ type: "normal" });
|
|
530
551
|
}
|
|
531
552
|
|
|
@@ -533,6 +554,7 @@ function handleCycleMessage(intent: IntentOfType<"cycleMessage">, deps: IntentDi
|
|
|
533
554
|
const { mode } = deps;
|
|
534
555
|
|
|
535
556
|
if (mode.type !== "submit") return;
|
|
557
|
+
|
|
536
558
|
const messageIndex =
|
|
537
559
|
(MESSAGE_OUTCOMES.indexOf(mode.message) + intent.direction + MESSAGE_OUTCOMES.length) %
|
|
538
560
|
MESSAGE_OUTCOMES.length;
|
|
@@ -582,6 +604,7 @@ function handleTreeMove(intent: IntentOfType<"treeMove">, deps: IntentDispatchDe
|
|
|
582
604
|
const rows = deps.controller.treeRows();
|
|
583
605
|
|
|
584
606
|
if (rows.length === 0) return;
|
|
607
|
+
|
|
585
608
|
const current = selectedTreeIndex(deps, rows);
|
|
586
609
|
const next = Math.min(rows.length - 1, Math.max(0, current + intent.direction));
|
|
587
610
|
|
|
@@ -593,9 +616,12 @@ function handleTreeGo(_intent: IntentOfType<"treeGo">, deps: IntentDispatchDeps)
|
|
|
593
616
|
const row = rows[selectedTreeIndex(deps, rows)];
|
|
594
617
|
|
|
595
618
|
if (!row) return deps.controller.setStatus("select an entry in the tree first");
|
|
619
|
+
|
|
596
620
|
if (row.isCurrentTip) return deps.controller.setStatus("already at the tip");
|
|
621
|
+
|
|
597
622
|
// a switch needs no summary; a move back may leave one, so it asks
|
|
598
623
|
if (row.tips.length > 0) return deps.controller.goToEntry(row.entryId);
|
|
624
|
+
|
|
599
625
|
deps.setMode({ type: "treePrompt", ask: "navigate", entryId: row.entryId, text: "" });
|
|
600
626
|
}
|
|
601
627
|
|