@cueloop/client 0.1.0-alpha.74 → 0.1.0-alpha.76
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.focus.test.ts +147 -0
- package/src/App.regression.test.tsx +94 -0
- package/src/App.test.tsx +11 -8
- package/src/App.tsx +476 -220
- package/src/App.visual.test.tsx +57 -0
- package/src/App.workbench.test.tsx +41 -22
- package/src/app-screens.tsx +168 -123
- package/src/components/AppShell.brand.test.tsx +1 -1
- package/src/components/AppShell.tsx +14 -1
- package/src/components/ChangesColumn.tsx +40 -12
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/ConfirmDialog.tsx +8 -1
- package/src/components/DiffContentView.tsx +7 -1
- package/src/components/DiscussionMarkerRail.tsx +2 -2
- package/src/components/EditorGrid.tsx +84 -26
- package/src/components/GridTabContent.tsx +10 -1
- package/src/components/PanelColumn.tsx +3 -0
- package/src/components/ProjectTreeView.test.tsx +39 -12
- package/src/components/ProjectTreeView.tsx +38 -10
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +16 -3
- package/src/components/RootOverlay.stories.tsx +1 -0
- package/src/components/RootOverlay.tsx +19 -6
- package/src/components/SettingsDialog.stories.tsx +3 -3
- package/src/components/SettingsDialog.tsx +3 -4
- package/src/components/ThreadFooter.test.tsx +12 -7
- package/src/components/ThreadFooter.tsx +18 -4
- package/src/components/ThreadTree.tsx +79 -36
- package/src/components/ThreadView.slash.test.tsx +36 -0
- package/src/components/ThreadView.tsx +7 -1
- package/src/components/WelcomePlayground.tsx +2 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +60 -51
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +5 -0
- package/src/components/primitives/icons.ts +1 -1
- package/src/components/quick-actions-editor.stories.tsx +2 -0
- package/src/components/quick-actions-editor.test.tsx +35 -1
- package/src/components/quick-actions-editor.tsx +23 -22
- package/src/components/session-tree.test.ts +3 -3
- package/src/components/session-tree.ts +1 -1
- package/src/config.ts +1 -0
- package/src/diff-intraline.ts +26 -5
- 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/run.ts +22 -2
- package/src/thread-chords.test.ts +33 -1
- package/src/thread-chords.ts +114 -29
- package/src/thread-controller.ts +4 -3
- package/src/use-annotation-surface.tsx +22 -0
- package/src/use-frame-measure.ts +4 -1
- package/src/use-settings-dialog.tsx +9 -7
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
|
@@ -793,7 +793,7 @@ exports[`stories catalog Overlays/PromptDialog/NameYourself renders, snapshots,
|
|
|
793
793
|
│ Your name (optional) - it attributes the notes │
|
|
794
794
|
│ you leave: │
|
|
795
795
|
│ your name │
|
|
796
|
-
│ enter save
|
|
796
|
+
│ enter save esc cancel │
|
|
797
797
|
└────────────────────────────────────────────────────┘
|
|
798
798
|
|
|
799
799
|
|
|
@@ -813,7 +813,7 @@ exports[`stories catalog Overlays/PromptDialog/RenameAuthor renders, snapshots,
|
|
|
813
813
|
│ Display name for this collaborator: │
|
|
814
814
|
│ Robin │
|
|
815
815
|
│ │
|
|
816
|
-
│ enter save
|
|
816
|
+
│ enter save esc cancel │
|
|
817
817
|
└────────────────────────────────────────────────────┘
|
|
818
818
|
|
|
819
819
|
|
|
@@ -878,10 +878,10 @@ exports[`stories catalog Overlays/SettingsDialog/AppearanceThemed renders, snaps
|
|
|
878
878
|
|
|
879
879
|
|
|
880
880
|
┌─ Settings ───────────────────────────────────────────────────────────────┐
|
|
881
|
-
│ General │
|
|
882
|
-
│ Display │
|
|
881
|
+
│ General │ Theme Rosé Pine Moon │
|
|
882
|
+
│ Display │ │
|
|
883
883
|
│ Appearance │ │
|
|
884
|
-
│ │
|
|
884
|
+
│ │ │
|
|
885
885
|
│ │ │
|
|
886
886
|
│ │ │
|
|
887
887
|
│ │ │
|
|
@@ -912,11 +912,11 @@ exports[`stories catalog Overlays/SettingsDialog/BodyZone renders, snapshots, an
|
|
|
912
912
|
|
|
913
913
|
|
|
914
914
|
┌─ Settings ───────────────────────────────────────────────────────────────┐
|
|
915
|
-
│ General │
|
|
916
|
-
│ Display │
|
|
915
|
+
│ General │ Plan width default │
|
|
916
|
+
│ Display │ Line numbers on │
|
|
917
917
|
│ Appearance │ │
|
|
918
|
-
│ │
|
|
919
|
-
│ │
|
|
918
|
+
│ │ │
|
|
919
|
+
│ │ │
|
|
920
920
|
│ │ │
|
|
921
921
|
│ │ │
|
|
922
922
|
│ │ │
|
|
@@ -946,11 +946,11 @@ exports[`stories catalog Overlays/SettingsDialog/NavZone renders, snapshots, and
|
|
|
946
946
|
|
|
947
947
|
|
|
948
948
|
┌─ Settings ───────────────────────────────────────────────────────────────┐
|
|
949
|
-
│ General │
|
|
950
|
-
│ Display │
|
|
949
|
+
│ General │ Display name amber-heron │
|
|
950
|
+
│ Display │ Auto-close on submit 3s │
|
|
951
951
|
│ Appearance │ │
|
|
952
|
-
│ │
|
|
953
|
-
│ │
|
|
952
|
+
│ │ │
|
|
953
|
+
│ │ │
|
|
954
954
|
│ │ │
|
|
955
955
|
│ │ │
|
|
956
956
|
│ │ │
|
|
@@ -1044,6 +1044,7 @@ exports[`stories catalog Surfaces/ThreadTree/TwoPending renders, snapshots, and
|
|
|
1044
1044
|
" Projects
|
|
1045
1045
|
widget
|
|
1046
1046
|
Migration Plan
|
|
1047
|
+
|
|
1047
1048
|
Threads
|
|
1048
1049
|
working tree
|
|
1049
1050
|
|
|
@@ -1052,7 +1053,6 @@ exports[`stories catalog Surfaces/ThreadTree/TwoPending renders, snapshots, and
|
|
|
1052
1053
|
|
|
1053
1054
|
|
|
1054
1055
|
|
|
1055
|
-
|
|
1056
1056
|
"
|
|
1057
1057
|
`;
|
|
1058
1058
|
|
|
@@ -1098,6 +1098,7 @@ exports[`stories catalog Surfaces/ThreadsSidebar/Open renders, snapshots, and ca
|
|
|
1098
1098
|
" Projects │
|
|
1099
1099
|
widget │
|
|
1100
1100
|
Migration Plan │
|
|
1101
|
+
│
|
|
1101
1102
|
Threads │
|
|
1102
1103
|
working tree │
|
|
1103
1104
|
│
|
|
@@ -1106,7 +1107,6 @@ exports[`stories catalog Surfaces/ThreadsSidebar/Open renders, snapshots, and ca
|
|
|
1106
1107
|
│
|
|
1107
1108
|
│
|
|
1108
1109
|
│
|
|
1109
|
-
│
|
|
1110
1110
|
"
|
|
1111
1111
|
`;
|
|
1112
1112
|
|
|
@@ -1753,54 +1753,54 @@ exports[`stories catalog Primitives/Tree/ProjectsThreads renders, snapshots, and
|
|
|
1753
1753
|
`;
|
|
1754
1754
|
|
|
1755
1755
|
exports[`stories catalog Chrome/QuickActionsEditor/Collapsed renders, snapshots, and carries its colors: Chrome/QuickActionsEditor/Collapsed 1`] = `
|
|
1756
|
-
"
|
|
1757
|
-
prompt defaults ▸
|
|
1756
|
+
" reset
|
|
1758
1757
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1758
|
+
Zoom out, research in depth
|
|
1759
|
+
Research the broader problem, prior art, an…
|
|
1760
|
+
Restate simplified
|
|
1761
|
+
Restate this as the simplest thing that wor…
|
|
1762
|
+
Out of scope
|
|
1763
|
+
This is out of scope for the task, so captu…
|
|
1764
|
+
Let's chat about this
|
|
1765
|
+
Do not implement yet, and surface the open …
|
|
1766
|
+
Prototype this
|
|
1767
|
+
Build a throwaway prototype to answer the q…
|
|
1768
|
+
Ensure 0 regressions
|
|
1769
|
+
Characterize the current behavior with test…
|
|
1770
|
+
Consider existing repo patterns
|
|
1771
|
+
Follow the nearest existing pattern in this…
|
|
1772
|
+
LGTM
|
|
1773
|
+
This looks good to me.
|
|
1775
1774
|
+ Add action
|
|
1776
1775
|
|
|
1777
1776
|
|
|
1777
|
+
|
|
1778
1778
|
"
|
|
1779
1779
|
`;
|
|
1780
1780
|
|
|
1781
1781
|
exports[`stories catalog Chrome/QuickActionsEditor/RowExpanded renders, snapshots, and carries its colors: Chrome/QuickActionsEditor/RowExpanded 1`] = `
|
|
1782
|
-
"
|
|
1783
|
-
prompt defaults ▸
|
|
1782
|
+
" reset
|
|
1784
1783
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1784
|
+
Zoom out, research in depth
|
|
1785
|
+
Research the broader problem, prior art, an…
|
|
1786
|
+
Restate simplified
|
|
1787
|
+
Restate this as the simplest thing that works and
|
|
1788
|
+
Out of scope
|
|
1789
|
+
This is out of scope for the task, so captu…
|
|
1790
|
+
Let's chat about this
|
|
1791
|
+
Do not implement yet, and surface the open …
|
|
1792
|
+
Prototype this
|
|
1793
|
+
Build a throwaway prototype to answer the q…
|
|
1794
|
+
Ensure 0 regressions
|
|
1795
|
+
Characterize the current behavior with test…
|
|
1796
|
+
Consider existing repo patterns
|
|
1797
|
+
Follow the nearest existing pattern in this…
|
|
1798
|
+
LGTM
|
|
1799
|
+
This looks good to me.
|
|
1801
1800
|
+ Add action
|
|
1802
1801
|
|
|
1803
1802
|
|
|
1803
|
+
|
|
1804
1804
|
"
|
|
1805
1805
|
`;
|
|
1806
1806
|
|
|
@@ -1885,5 +1885,14 @@ exports[`stories catalog Foundations/Theme/ProviderSwap renders, snapshots, and
|
|
|
1885
1885
|
|
|
1886
1886
|
|
|
1887
1887
|
|
|
1888
|
+
"
|
|
1889
|
+
`;
|
|
1890
|
+
|
|
1891
|
+
exports[`stories catalog Layout/MenuControl/OneOpenAtATime renders, snapshots, and carries its colors: Layout/MenuControl/OneOpenAtATime 1`] = `
|
|
1892
|
+
"
|
|
1893
|
+
alpha: open
|
|
1894
|
+
beta: closed
|
|
1895
|
+
|
|
1896
|
+
|
|
1888
1897
|
"
|
|
1889
1898
|
`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { MenuControlProvider, useMenuControl } from "./menu-control";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Layout/MenuControl" };
|
|
7
|
+
|
|
8
|
+
// One open menu at a time: with "alpha" open, only its row reads as open.
|
|
9
|
+
function MenuControlDemo(): React.ReactNode {
|
|
10
|
+
const { openMenuId } = useMenuControl();
|
|
11
|
+
const row = (id: string): React.ReactNode => (
|
|
12
|
+
<text fg={openMenuId === id ? DARK.accent : DARK.textDim}>
|
|
13
|
+
{`${id}: ${openMenuId === id ? "open" : "closed"}`}
|
|
14
|
+
</text>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<box style={{ flexDirection: "column" }}>
|
|
19
|
+
{row("alpha")}
|
|
20
|
+
{row("beta")}
|
|
21
|
+
</box>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const OneOpenAtATime: Story = {
|
|
26
|
+
render: () => (
|
|
27
|
+
<MenuControlProvider value={{ openMenuId: "alpha", toggleMenu: () => {}, closeMenu: () => {} }}>
|
|
28
|
+
<box style={{ padding: 1, backgroundColor: DARK.panel }}>
|
|
29
|
+
<MenuControlDemo />
|
|
30
|
+
</box>
|
|
31
|
+
</MenuControlProvider>
|
|
32
|
+
),
|
|
33
|
+
expectedColors: [DARK.accent],
|
|
34
|
+
size: { width: 30, height: 5 },
|
|
35
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// One open menu at a time across a shell: the thread row menu and the editor split menu share this
|
|
2
|
+
// single open id, so opening either closes the other. Owned by App and by the no-thread shell, one each.
|
|
3
|
+
|
|
4
|
+
import React, { createContext, useContext, useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
export interface MenuControlApi {
|
|
7
|
+
openMenuId: string | null;
|
|
8
|
+
toggleMenu: (id: string) => void;
|
|
9
|
+
closeMenu: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MenuControlContext = createContext<MenuControlApi>({
|
|
13
|
+
openMenuId: null,
|
|
14
|
+
toggleMenu: () => {},
|
|
15
|
+
closeMenu: () => {},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function useMenuControl(): MenuControlApi {
|
|
19
|
+
return useContext(MenuControlContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useMenuControlState(): MenuControlApi {
|
|
23
|
+
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
return useMemo(
|
|
26
|
+
() => ({
|
|
27
|
+
openMenuId,
|
|
28
|
+
toggleMenu: (id: string) => setOpenMenuId((current) => (current === id ? null : id)),
|
|
29
|
+
closeMenu: () => setOpenMenuId(null),
|
|
30
|
+
}),
|
|
31
|
+
[openMenuId],
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MenuControlProvider({
|
|
36
|
+
value,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
value: MenuControlApi;
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
}): React.ReactNode {
|
|
42
|
+
return <MenuControlContext.Provider value={value}>{children}</MenuControlContext.Provider>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The dialog dismisses on a backdrop click (outside the panel) but never on a click inside it. */
|
|
2
|
+
|
|
3
|
+
import { test, expect } from "bun:test";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
6
|
+
import { Dialog } from "./Dialog";
|
|
7
|
+
import { settle } from "../../test-support";
|
|
8
|
+
import { DARK } from "../../theme";
|
|
9
|
+
|
|
10
|
+
test("clicking the backdrop dismisses; clicking inside the panel does not", async () => {
|
|
11
|
+
let dismissed = 0;
|
|
12
|
+
const setup = await testRender(
|
|
13
|
+
<Dialog
|
|
14
|
+
isOpen
|
|
15
|
+
title=" test "
|
|
16
|
+
width={20}
|
|
17
|
+
height={5}
|
|
18
|
+
background={DARK.elevated}
|
|
19
|
+
onDismiss={() => (dismissed += 1)}
|
|
20
|
+
theme={DARK}
|
|
21
|
+
>
|
|
22
|
+
<text>body</text>
|
|
23
|
+
</Dialog>,
|
|
24
|
+
{ width: 60, height: 20 },
|
|
25
|
+
);
|
|
26
|
+
await settle(setup);
|
|
27
|
+
|
|
28
|
+
// the top-left corner is backdrop, outside the centered panel
|
|
29
|
+
await setup.mockMouse.click(0, 0);
|
|
30
|
+
expect(dismissed).toBe(1);
|
|
31
|
+
|
|
32
|
+
// the screen centre lands inside the panel, so it must not dismiss
|
|
33
|
+
await setup.mockMouse.click(30, 10);
|
|
34
|
+
expect(dismissed).toBe(1);
|
|
35
|
+
|
|
36
|
+
setup.renderer.destroy();
|
|
37
|
+
});
|
|
@@ -17,6 +17,8 @@ export interface DialogProps {
|
|
|
17
17
|
height: number;
|
|
18
18
|
/** Solid panel fill for content-heavy dialogs; default transparent. */
|
|
19
19
|
background?: string;
|
|
20
|
+
/** Dismiss on a click outside the panel. */
|
|
21
|
+
onDismiss?: () => void;
|
|
20
22
|
theme?: Theme;
|
|
21
23
|
children: React.ReactNode;
|
|
22
24
|
}
|
|
@@ -27,6 +29,7 @@ export function Dialog({
|
|
|
27
29
|
width,
|
|
28
30
|
height,
|
|
29
31
|
background,
|
|
32
|
+
onDismiss,
|
|
30
33
|
theme,
|
|
31
34
|
children,
|
|
32
35
|
}: DialogProps): React.ReactNode {
|
|
@@ -36,6 +39,7 @@ export function Dialog({
|
|
|
36
39
|
|
|
37
40
|
return (
|
|
38
41
|
<box
|
|
42
|
+
onMouseUp={onDismiss}
|
|
39
43
|
style={{
|
|
40
44
|
position: "absolute",
|
|
41
45
|
left: 0,
|
|
@@ -48,6 +52,7 @@ export function Dialog({
|
|
|
48
52
|
}}
|
|
49
53
|
>
|
|
50
54
|
<box
|
|
55
|
+
onMouseUp={(event) => event.stopPropagation()}
|
|
51
56
|
style={{
|
|
52
57
|
width,
|
|
53
58
|
height,
|
|
@@ -24,6 +24,7 @@ export const Collapsed: Story = {
|
|
|
24
24
|
actions={DEFAULT_QUICK_ACTIONS}
|
|
25
25
|
selectedIndex={0}
|
|
26
26
|
expandedIndex={null}
|
|
27
|
+
expandedField="prompt"
|
|
27
28
|
onToggleExpand={noop}
|
|
28
29
|
onEditPrompt={() => {}}
|
|
29
30
|
onEditMetadata={noop}
|
|
@@ -43,6 +44,7 @@ export const RowExpanded: Story = {
|
|
|
43
44
|
actions={DEFAULT_QUICK_ACTIONS}
|
|
44
45
|
selectedIndex={1}
|
|
45
46
|
expandedIndex={1}
|
|
47
|
+
expandedField="prompt"
|
|
46
48
|
onToggleExpand={noop}
|
|
47
49
|
onEditPrompt={() => {}}
|
|
48
50
|
onEditMetadata={noop}
|
|
@@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { testRender } from "@opentui/react/test-utils";
|
|
6
6
|
import type { QuickAction } from "../config";
|
|
7
|
-
import { settle, typeText } from "../test-support";
|
|
7
|
+
import { locateText, settle, typeText } from "../test-support";
|
|
8
8
|
import { QuickActionsEditor } from "./quick-actions-editor";
|
|
9
9
|
|
|
10
10
|
const MANY: QuickAction[] = Array.from({ length: 14 }, (_, index) => ({
|
|
@@ -17,6 +17,7 @@ async function mount(selectedIndex: number) {
|
|
|
17
17
|
actions={MANY}
|
|
18
18
|
selectedIndex={selectedIndex}
|
|
19
19
|
expandedIndex={null}
|
|
20
|
+
expandedField="prompt"
|
|
20
21
|
onToggleExpand={() => {}}
|
|
21
22
|
onEditPrompt={() => {}}
|
|
22
23
|
onEditMetadata={() => {}}
|
|
@@ -58,6 +59,7 @@ describe("editing an expanded action", () => {
|
|
|
58
59
|
actions={[{ prompt: "Ship it", metadata: "be terse" }]}
|
|
59
60
|
selectedIndex={0}
|
|
60
61
|
expandedIndex={0}
|
|
62
|
+
expandedField="prompt"
|
|
61
63
|
onToggleExpand={() => {}}
|
|
62
64
|
onEditPrompt={(_index, prompt) => prompts.push(prompt)}
|
|
63
65
|
onEditMetadata={(_index, metadata) => metadatas.push(metadata)}
|
|
@@ -80,4 +82,36 @@ describe("editing an expanded action", () => {
|
|
|
80
82
|
|
|
81
83
|
setup.renderer.destroy();
|
|
82
84
|
});
|
|
85
|
+
|
|
86
|
+
test("clicking the description row opens the editor on the metadata field directly", async () => {
|
|
87
|
+
const toggles: Array<{ index: number; field: "prompt" | "metadata" }> = [];
|
|
88
|
+
const setup = await testRender(
|
|
89
|
+
<QuickActionsEditor
|
|
90
|
+
actions={[{ prompt: "Ship it", metadata: "be terse" }]}
|
|
91
|
+
selectedIndex={0}
|
|
92
|
+
expandedIndex={null}
|
|
93
|
+
expandedField="prompt"
|
|
94
|
+
onToggleExpand={(index, field) => toggles.push({ index, field })}
|
|
95
|
+
onEditPrompt={() => {}}
|
|
96
|
+
onEditMetadata={() => {}}
|
|
97
|
+
onReset={() => {}}
|
|
98
|
+
onAdd={() => {}}
|
|
99
|
+
/>,
|
|
100
|
+
{ width: 60, height: 8 },
|
|
101
|
+
);
|
|
102
|
+
await settle(setup);
|
|
103
|
+
|
|
104
|
+
// clicking the title opens the title field; clicking the description opens the metadata field
|
|
105
|
+
const title = locateText(setup, "Ship it");
|
|
106
|
+
await setup.mockMouse.click(title.column, title.row);
|
|
107
|
+
const description = locateText(setup, "be terse");
|
|
108
|
+
await setup.mockMouse.click(description.column, description.row);
|
|
109
|
+
|
|
110
|
+
expect(toggles).toEqual([
|
|
111
|
+
{ index: 0, field: "prompt" },
|
|
112
|
+
{ index: 0, field: "metadata" },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
setup.renderer.destroy();
|
|
116
|
+
});
|
|
83
117
|
});
|
|
@@ -22,8 +22,10 @@ export interface QuickActionsEditorProps {
|
|
|
22
22
|
selectedIndex: number;
|
|
23
23
|
/** The row whose system-prompt input is open and focused, or null. */
|
|
24
24
|
expandedIndex: number | null;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/** Which field the open editor focuses first - set by the clicked row. */
|
|
26
|
+
expandedField: "prompt" | "metadata";
|
|
27
|
+
/** Select and toggle a row's editor open/closed, focusing the given field. */
|
|
28
|
+
onToggleExpand: (index: number, field: "prompt" | "metadata") => void;
|
|
27
29
|
onEditPrompt: (index: number, prompt: string) => void;
|
|
28
30
|
onEditMetadata: (index: number, metadata: string) => void;
|
|
29
31
|
onReset: () => void;
|
|
@@ -35,6 +37,7 @@ export function QuickActionsEditor({
|
|
|
35
37
|
actions,
|
|
36
38
|
selectedIndex,
|
|
37
39
|
expandedIndex,
|
|
40
|
+
expandedField,
|
|
38
41
|
onToggleExpand,
|
|
39
42
|
onEditPrompt,
|
|
40
43
|
onEditMetadata,
|
|
@@ -52,11 +55,9 @@ export function QuickActionsEditor({
|
|
|
52
55
|
return (
|
|
53
56
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
54
57
|
<box style={{ flexDirection: "row" }}>
|
|
55
|
-
<box style={{ flexGrow: 1 }}
|
|
56
|
-
<text fg={tokens.textDim}>click a prompt to edit its system prompt</text>
|
|
57
|
-
</box>
|
|
58
|
+
<box style={{ flexGrow: 1 }} />
|
|
58
59
|
<box onMouseUp={onReset}>
|
|
59
|
-
<text fg={tokens.textDim}>reset
|
|
60
|
+
<text fg={tokens.textDim}>reset</text>
|
|
60
61
|
</box>
|
|
61
62
|
</box>
|
|
62
63
|
<box style={{ height: 1 }} />
|
|
@@ -68,7 +69,8 @@ export function QuickActionsEditor({
|
|
|
68
69
|
action={action}
|
|
69
70
|
isSelected={index === selectedIndex}
|
|
70
71
|
isExpanded={index === expandedIndex}
|
|
71
|
-
|
|
72
|
+
initialField={expandedField}
|
|
73
|
+
onToggleExpand={(field) => onToggleExpand(index, field)}
|
|
72
74
|
onEditPrompt={(prompt) => onEditPrompt(index, prompt)}
|
|
73
75
|
onEditMetadata={(metadata) => onEditMetadata(index, metadata)}
|
|
74
76
|
theme={theme}
|
|
@@ -91,6 +93,7 @@ function ActionRow({
|
|
|
91
93
|
action,
|
|
92
94
|
isSelected,
|
|
93
95
|
isExpanded,
|
|
96
|
+
initialField,
|
|
94
97
|
onToggleExpand,
|
|
95
98
|
onEditPrompt,
|
|
96
99
|
onEditMetadata,
|
|
@@ -100,7 +103,8 @@ function ActionRow({
|
|
|
100
103
|
action: QuickAction;
|
|
101
104
|
isSelected: boolean;
|
|
102
105
|
isExpanded: boolean;
|
|
103
|
-
|
|
106
|
+
initialField: "prompt" | "metadata";
|
|
107
|
+
onToggleExpand: (field: "prompt" | "metadata") => void;
|
|
104
108
|
onEditPrompt: (prompt: string) => void;
|
|
105
109
|
onEditMetadata: (metadata: string) => void;
|
|
106
110
|
theme?: Theme;
|
|
@@ -113,9 +117,10 @@ function ActionRow({
|
|
|
113
117
|
<ActionEditor
|
|
114
118
|
action={action}
|
|
115
119
|
isSelected={isSelected}
|
|
120
|
+
initialField={initialField}
|
|
116
121
|
onEditPrompt={onEditPrompt}
|
|
117
122
|
onEditMetadata={onEditMetadata}
|
|
118
|
-
onDone={onToggleExpand}
|
|
123
|
+
onDone={() => onToggleExpand("prompt")}
|
|
119
124
|
tokens={tokens}
|
|
120
125
|
/>
|
|
121
126
|
</box>
|
|
@@ -126,12 +131,12 @@ function ActionRow({
|
|
|
126
131
|
<box id={rowId} style={{ flexDirection: "column" }}>
|
|
127
132
|
<box
|
|
128
133
|
style={{ backgroundColor: isSelected ? tokens.border : undefined }}
|
|
129
|
-
onMouseUp={onToggleExpand}
|
|
134
|
+
onMouseUp={() => onToggleExpand("prompt")}
|
|
130
135
|
>
|
|
131
|
-
<text fg={isSelected ? tokens.text : tokens.textMuted}>{
|
|
136
|
+
<text fg={isSelected ? tokens.text : tokens.textMuted}>{action.prompt}</text>
|
|
132
137
|
</box>
|
|
133
138
|
{action.metadata ? (
|
|
134
|
-
<box
|
|
139
|
+
<box onMouseUp={() => onToggleExpand("metadata")}>
|
|
135
140
|
<text fg={tokens.textDim}>{truncateMetadata(action.metadata)}</text>
|
|
136
141
|
</box>
|
|
137
142
|
) : null}
|
|
@@ -141,12 +146,13 @@ function ActionRow({
|
|
|
141
146
|
|
|
142
147
|
/**
|
|
143
148
|
* The two-field editor for an expanded row: the action title over its system
|
|
144
|
-
* prompt. Mounts fresh per expand, so focus starts on the
|
|
145
|
-
* effect; ⏎ steps title -> description -> close, and a click focuses either.
|
|
149
|
+
* prompt. Mounts fresh per expand, so focus starts on the clicked field without a
|
|
150
|
+
* reset effect; ⏎ steps title -> description -> close, and a click focuses either.
|
|
146
151
|
*/
|
|
147
152
|
function ActionEditor({
|
|
148
153
|
action,
|
|
149
154
|
isSelected,
|
|
155
|
+
initialField,
|
|
150
156
|
onEditPrompt,
|
|
151
157
|
onEditMetadata,
|
|
152
158
|
onDone,
|
|
@@ -154,6 +160,7 @@ function ActionEditor({
|
|
|
154
160
|
}: {
|
|
155
161
|
action: QuickAction;
|
|
156
162
|
isSelected: boolean;
|
|
163
|
+
initialField: "prompt" | "metadata";
|
|
157
164
|
onEditPrompt: (prompt: string) => void;
|
|
158
165
|
onEditMetadata: (metadata: string) => void;
|
|
159
166
|
onDone: () => void;
|
|
@@ -161,7 +168,7 @@ function ActionEditor({
|
|
|
161
168
|
}): React.ReactNode {
|
|
162
169
|
const promptRef = useRef<TextareaRenderable | null>(null);
|
|
163
170
|
const metadataRef = useRef<TextareaRenderable | null>(null);
|
|
164
|
-
const [activeField, setActiveField] = useState<"prompt" | "metadata">(
|
|
171
|
+
const [activeField, setActiveField] = useState<"prompt" | "metadata">(initialField);
|
|
165
172
|
|
|
166
173
|
// the focused field types from its end, not from the caret parked at the start
|
|
167
174
|
useEffect(() => {
|
|
@@ -184,9 +191,6 @@ function ActionEditor({
|
|
|
184
191
|
<box
|
|
185
192
|
style={{ flexDirection: "row", backgroundColor: isSelected ? tokens.border : undefined }}
|
|
186
193
|
>
|
|
187
|
-
<box style={{ width: 2, flexShrink: 0 }}>
|
|
188
|
-
<text fg={isSelected ? tokens.text : tokens.textMuted}>{"▾ "}</text>
|
|
189
|
-
</box>
|
|
190
194
|
<textarea
|
|
191
195
|
ref={promptRef}
|
|
192
196
|
focused={activeField === "prompt"}
|
|
@@ -199,10 +203,7 @@ function ActionEditor({
|
|
|
199
203
|
style={fieldStyle}
|
|
200
204
|
/>
|
|
201
205
|
</box>
|
|
202
|
-
<box style={{ flexDirection: "row"
|
|
203
|
-
<box style={{ width: 2, flexShrink: 0 }}>
|
|
204
|
-
<text fg={tokens.textDim}>{">"}</text>
|
|
205
|
-
</box>
|
|
206
|
+
<box style={{ flexDirection: "row" }}>
|
|
206
207
|
<textarea
|
|
207
208
|
ref={metadataRef}
|
|
208
209
|
focused={activeField === "metadata"}
|
|
@@ -112,19 +112,19 @@ describe("groupInbox", () => {
|
|
|
112
112
|
expect(rows.some((row) => row.kind === "section" && row.label === "Threads")).toBe(true);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("a pinned thread lifts into a
|
|
115
|
+
test("a pinned thread lifts into a Starred section at the top, out of its normal group", () => {
|
|
116
116
|
// Arrange - one repo-bound thread, one standalone; pin the repo-bound one
|
|
117
117
|
const sessions = [session("a", "Pinned one", "root-1"), session("b", "Loose two")];
|
|
118
118
|
|
|
119
119
|
// Act
|
|
120
120
|
const { rows, ordered } = groupInbox(sessions, new Set(["a"]));
|
|
121
121
|
|
|
122
|
-
// Assert -
|
|
122
|
+
// Assert - Starred is the first section and holds thread a; a is gone from Projects
|
|
123
123
|
const sections = rows
|
|
124
124
|
.filter((row) => row.kind === "section")
|
|
125
125
|
.map((row) => (row.kind === "section" ? row.label : ""));
|
|
126
126
|
|
|
127
|
-
expect(sections[0]).toBe("
|
|
127
|
+
expect(sections[0]).toBe("Starred");
|
|
128
128
|
expect(sections).not.toContain("Projects"); // its only member was pinned away
|
|
129
129
|
expect(ordered[0]!.id).toBe("a"); // the cursor walks pinned threads first
|
|
130
130
|
const pinnedThreadIds = rows.filter((row) => row.kind === "thread").map((row) => row.id);
|
|
@@ -84,7 +84,7 @@ export function groupInbox(
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
if (pinned.length > 0) {
|
|
87
|
-
rows.push({ kind: "section", id: "section:pinned", label: "
|
|
87
|
+
rows.push({ kind: "section", id: "section:pinned", label: "Starred" });
|
|
88
88
|
for (const session of pinned) pushThread(session);
|
|
89
89
|
}
|
|
90
90
|
|