@cueloop/client 0.1.0-alpha.6 → 0.1.0-alpha.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +37 -33
- package/src/App.appearance.test.tsx +51 -0
- package/src/App.autoclose.test.tsx +73 -47
- package/src/App.diff.test.tsx +169 -22
- package/src/App.export.test.tsx +49 -37
- package/src/App.plan-review.test.tsx +714 -0
- package/src/App.prototype.test.tsx +194 -0
- package/src/App.readonly.test.tsx +69 -35
- package/src/App.rendering.test.tsx +59 -31
- package/src/App.review-panel.test.tsx +143 -0
- package/src/App.submit-confirm.test.tsx +229 -0
- package/src/App.test.tsx +139 -68
- package/src/App.toast.test.tsx +77 -0
- package/src/App.tsx +0 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +270 -0
- package/src/app-view-model.ts +307 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotationCard.compose-rows.test.ts +49 -0
- package/src/components/AnnotationCard.stories.tsx +94 -0
- package/src/components/AnnotationCard.tsx +250 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +23 -0
- package/src/components/CompletionOverlay.tsx +60 -0
- package/src/components/ConfirmCard.stories.tsx +35 -0
- package/src/components/ConfirmCard.tsx +129 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +66 -0
- package/src/components/DiffSheet.scroll.test.tsx +135 -0
- package/src/components/DiffSheet.stories.tsx +78 -0
- package/src/components/DiffSheet.syntax.test.tsx +65 -0
- package/src/components/DiffSheet.tsx +374 -0
- package/src/components/InboxList.stories.tsx +31 -0
- package/src/components/InboxList.tsx +64 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +59 -0
- package/src/components/MarkerPopover.stories.tsx +58 -0
- package/src/components/MarkerPopover.tsx +109 -0
- package/src/components/MenuBar.stories.tsx +37 -0
- package/src/components/MenuBar.tsx +75 -0
- package/src/components/PlanSheet.stories.tsx +113 -0
- package/src/components/PlanSheet.tsx +422 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.tsx +80 -0
- package/src/components/PrototypeSheet.stories.tsx +20 -0
- package/src/components/PrototypeSheet.tsx +450 -0
- package/src/components/ReviewPanel.stories.tsx +86 -0
- package/src/components/ReviewPanel.tsx +103 -0
- package/src/components/ReviewRail.stories.tsx +218 -0
- package/src/components/ReviewRail.tsx +318 -0
- package/src/components/SettingsDialog.stories.tsx +111 -0
- package/src/components/SettingsDialog.tsx +147 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/Toast.stories.tsx +12 -0
- package/src/components/Toast.tsx +45 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +152 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +1857 -0
- package/src/components/agent-launcher.stories.tsx +26 -0
- package/src/components/agent-launcher.test.tsx +50 -0
- package/src/components/agent-launcher.tsx +149 -0
- package/src/components/diff-sheet-layout.test.ts +128 -0
- package/src/components/diff-sheet-layout.ts +184 -0
- package/src/components/plan-sheet-run-style.ts +90 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +52 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +60 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.tsx +66 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +14 -0
- package/src/components/quick-actions-editor.stories.tsx +54 -0
- package/src/components/quick-actions-editor.tsx +138 -0
- package/src/components/stories-app.tsx +124 -0
- package/src/components/stories.test.tsx +66 -0
- package/src/components/story-fixtures.ts +98 -0
- package/src/components/story.ts +75 -0
- package/src/components/syntax-highlight.ts +102 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +193 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/config.test.ts +448 -31
- package/src/config.ts +339 -57
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +253 -0
- package/src/diff-intraline.test.ts +165 -0
- package/src/diff-intraline.ts +146 -0
- package/src/diff-syntax.test.ts +96 -0
- package/src/diff-syntax.ts +176 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +135 -9
- package/src/ghostty-terminal.test.ts +84 -0
- package/src/ghostty-terminal.ts +176 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +17 -21
- package/src/index.ts +10 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +499 -0
- package/src/intent-dispatch.ts +585 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +452 -0
- package/src/keymap.test.ts +635 -0
- package/src/keymap.ts +404 -0
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/prototype-browser.test.ts +56 -0
- package/src/prototype-browser.ts +289 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +237 -0
- package/src/review-panel.test.ts +113 -0
- package/src/review-panel.ts +83 -0
- package/src/run.ts +16 -5
- package/src/serve.test.ts +25 -9
- package/src/serve.ts +19 -8
- package/src/session-controller.curate.test.ts +260 -0
- package/src/session-controller.share.test.ts +320 -0
- package/src/session-controller.ts +993 -0
- package/src/share.ts +120 -0
- package/src/test-support.test.ts +60 -0
- package/src/test-support.ts +197 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +187 -0
- package/src/theme.ts +64 -17
- package/src/use-settings-dialog.tsx +275 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +22 -8
- package/src/view-diff.ts +39 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +406 -0
- package/src/view-plan.ts +554 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -0
package/src/theme.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Semantic theme tokens
|
|
2
|
+
* Semantic theme tokens. Values carry over from the design-system
|
|
3
3
|
* prototypes (tokens.css). One built-in dark theme for now; the theme
|
|
4
4
|
* registry (palette-derived themes, terminal theme, user overrides) layers
|
|
5
5
|
* on top of these slots without touching consumers.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface Theme {
|
|
9
|
-
|
|
9
|
+
background: string;
|
|
10
10
|
panel: string;
|
|
11
11
|
elevated: string;
|
|
12
12
|
border: string;
|
|
@@ -18,16 +18,37 @@ export interface Theme {
|
|
|
18
18
|
green: string;
|
|
19
19
|
red: string;
|
|
20
20
|
blue: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
cursorBackground: string;
|
|
22
|
+
markCommentBackground: string;
|
|
23
|
+
insertedForeground: string;
|
|
24
|
+
deletedForeground: string;
|
|
25
|
+
/** Layer behind centered dialogs; transparent keeps the session visible. */
|
|
26
|
+
backdrop: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The dimmed variant behind the walk wizard: every reading color drops to
|
|
31
|
+
* the dim token so the file list stays legible context, not competition.
|
|
32
|
+
* Layout tokens (backgrounds, cursor) keep their values.
|
|
33
|
+
*/
|
|
34
|
+
export function dimmedTheme(theme: Theme): Theme {
|
|
35
|
+
return {
|
|
36
|
+
...theme,
|
|
37
|
+
text: theme.textDim,
|
|
38
|
+
textMuted: theme.textDim,
|
|
39
|
+
accent: theme.textDim,
|
|
40
|
+
green: theme.textDim,
|
|
41
|
+
red: theme.textDim,
|
|
42
|
+
blue: theme.textDim,
|
|
43
|
+
insertedForeground: theme.textDim,
|
|
44
|
+
deletedForeground: theme.textDim,
|
|
45
|
+
};
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
export const DARK: Theme = {
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
/* Unpainted so the terminal background shows through; override in [theme]. */
|
|
50
|
+
background: "transparent",
|
|
51
|
+
panel: "transparent",
|
|
31
52
|
elevated: "#23232f",
|
|
32
53
|
border: "#3a3a4a",
|
|
33
54
|
text: "#e4e6ec",
|
|
@@ -36,11 +57,37 @@ export const DARK: Theme = {
|
|
|
36
57
|
accent: "#f5a3a3",
|
|
37
58
|
accentInk: "#2a1416",
|
|
38
59
|
green: "#62d96b",
|
|
39
|
-
red: "#
|
|
40
|
-
blue: "#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
60
|
+
red: "#f08080",
|
|
61
|
+
blue: "#a9c8f5",
|
|
62
|
+
cursorBackground: "#33262a",
|
|
63
|
+
markCommentBackground: "#3d2a2e",
|
|
64
|
+
insertedForeground: "#62d96b",
|
|
65
|
+
deletedForeground: "#ff6b6b",
|
|
66
|
+
backdrop: "transparent",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The branded transparent theme for a LIGHT terminal: same unpainted
|
|
71
|
+
* background, but every reading color darkens so text is legible on a white
|
|
72
|
+
* terminal instead of light-on-light. Chosen when the terminal reports a light
|
|
73
|
+
* background; the dark variant above is the default and the fallback.
|
|
74
|
+
*/
|
|
75
|
+
export const LIGHT: Theme = {
|
|
76
|
+
background: "transparent",
|
|
77
|
+
panel: "transparent",
|
|
78
|
+
elevated: "#ececf2",
|
|
79
|
+
border: "#c7c7d2",
|
|
80
|
+
text: "#1c1d24",
|
|
81
|
+
textMuted: "#4b5162",
|
|
82
|
+
textDim: "#868c9c",
|
|
83
|
+
accent: "#b5495b",
|
|
84
|
+
accentInk: "#ffffff",
|
|
85
|
+
green: "#2f8a3e",
|
|
86
|
+
red: "#c0392b",
|
|
87
|
+
blue: "#2f6fb0",
|
|
88
|
+
cursorBackground: "#e6e2ea",
|
|
89
|
+
markCommentBackground: "#f2e2e8",
|
|
90
|
+
insertedForeground: "#2f8a3e",
|
|
91
|
+
deletedForeground: "#c0392b",
|
|
92
|
+
backdrop: "transparent",
|
|
93
|
+
};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React, { useState, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_QUICK_ACTIONS,
|
|
4
|
+
persistActions,
|
|
5
|
+
persistAutoClose,
|
|
6
|
+
persistTheme,
|
|
7
|
+
type AutoClose,
|
|
8
|
+
type QuickAction,
|
|
9
|
+
} from "./config";
|
|
10
|
+
import {
|
|
11
|
+
composeTheme,
|
|
12
|
+
THEME_LABELS,
|
|
13
|
+
THEME_NAMES,
|
|
14
|
+
type Appearance,
|
|
15
|
+
type ThemeName,
|
|
16
|
+
} from "./theme-presets";
|
|
17
|
+
import type { Theme } from "./theme";
|
|
18
|
+
import type { ReviewPanelMode } from "./review-panel";
|
|
19
|
+
import type { SettingsCategory } from "./components/SettingsDialog";
|
|
20
|
+
import { QuickActionsEditor } from "./components/quick-actions-editor";
|
|
21
|
+
import type { ReviewController } from "./session-controller";
|
|
22
|
+
|
|
23
|
+
export interface SettingsNav {
|
|
24
|
+
categoryId: string;
|
|
25
|
+
rowIndex: number;
|
|
26
|
+
zone: "nav" | "body";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SettingsDialogModel {
|
|
30
|
+
settingsNav: SettingsNav;
|
|
31
|
+
actionsExpandedIndex: number | null;
|
|
32
|
+
setActionsExpandedIndex: Dispatch<SetStateAction<number | null>>;
|
|
33
|
+
settingsCategories: SettingsCategory[];
|
|
34
|
+
settingsValues: Record<string, string>;
|
|
35
|
+
cycleSetting: (rowKey: string) => void;
|
|
36
|
+
handleSettingsKey: (name: string) => void;
|
|
37
|
+
onCategorySelect: (categoryId: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DOWN_KEYS = new Set(["j", "down"]);
|
|
41
|
+
const UP_KEYS = new Set(["k", "up"]);
|
|
42
|
+
const ENTER_BODY_KEYS = new Set(["l", "tab", "return"]);
|
|
43
|
+
const ACTIVATE_KEYS = new Set(["return", "space", "l"]);
|
|
44
|
+
|
|
45
|
+
function moveNavZone(
|
|
46
|
+
name: string,
|
|
47
|
+
categoryIndex: number,
|
|
48
|
+
categories: SettingsCategory[],
|
|
49
|
+
): SettingsNav | null {
|
|
50
|
+
if (DOWN_KEYS.has(name))
|
|
51
|
+
return {
|
|
52
|
+
categoryId: categories[Math.min(categories.length - 1, categoryIndex + 1)]!.id,
|
|
53
|
+
rowIndex: 0,
|
|
54
|
+
zone: "nav",
|
|
55
|
+
};
|
|
56
|
+
if (UP_KEYS.has(name))
|
|
57
|
+
return { categoryId: categories[Math.max(0, categoryIndex - 1)]!.id, rowIndex: 0, zone: "nav" };
|
|
58
|
+
if (ENTER_BODY_KEYS.has(name))
|
|
59
|
+
return { categoryId: categories[categoryIndex]!.id, rowIndex: 0, zone: "body" };
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function moveBodyRow(name: string, nav: SettingsNav, rowCount: number): SettingsNav | null {
|
|
65
|
+
if (DOWN_KEYS.has(name)) return { ...nav, rowIndex: Math.min(rowCount - 1, nav.rowIndex + 1) };
|
|
66
|
+
if (UP_KEYS.has(name)) return { ...nav, rowIndex: Math.max(0, nav.rowIndex - 1) };
|
|
67
|
+
if (name === "h" || name === "tab") return { ...nav, zone: "nav" };
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useSettingsDialog(params: {
|
|
73
|
+
theme: Theme;
|
|
74
|
+
appearance: Appearance;
|
|
75
|
+
autoClose: AutoClose;
|
|
76
|
+
setAutoClose: Dispatch<SetStateAction<AutoClose>>;
|
|
77
|
+
reviewMode: ReviewPanelMode;
|
|
78
|
+
setReviewMode: Dispatch<SetStateAction<ReviewPanelMode>>;
|
|
79
|
+
themeName: ThemeName;
|
|
80
|
+
setThemeName: Dispatch<SetStateAction<ThemeName>>;
|
|
81
|
+
themeOverrides: Partial<Theme>;
|
|
82
|
+
setTheme: Dispatch<SetStateAction<Theme>>;
|
|
83
|
+
quickActions: QuickAction[];
|
|
84
|
+
setQuickActions: Dispatch<SetStateAction<QuickAction[]>>;
|
|
85
|
+
controller: ReviewController;
|
|
86
|
+
setMenuDialog: Dispatch<SetStateAction<"keybinds" | "settings" | null>>;
|
|
87
|
+
}): SettingsDialogModel {
|
|
88
|
+
const {
|
|
89
|
+
theme,
|
|
90
|
+
appearance,
|
|
91
|
+
autoClose,
|
|
92
|
+
setAutoClose,
|
|
93
|
+
reviewMode,
|
|
94
|
+
setReviewMode,
|
|
95
|
+
themeName,
|
|
96
|
+
setThemeName,
|
|
97
|
+
themeOverrides,
|
|
98
|
+
setTheme,
|
|
99
|
+
quickActions,
|
|
100
|
+
setQuickActions,
|
|
101
|
+
controller,
|
|
102
|
+
setMenuDialog,
|
|
103
|
+
} = params;
|
|
104
|
+
|
|
105
|
+
const [settingsNav, setSettingsNav] = useState<SettingsNav>({
|
|
106
|
+
categoryId: "general",
|
|
107
|
+
rowIndex: 0,
|
|
108
|
+
zone: "body",
|
|
109
|
+
});
|
|
110
|
+
const [actionsExpandedIndex, setActionsExpandedIndex] = useState<number | null>(null);
|
|
111
|
+
|
|
112
|
+
const commitActions = (next: QuickAction[]): void => {
|
|
113
|
+
setQuickActions(next);
|
|
114
|
+
persistActions(next);
|
|
115
|
+
};
|
|
116
|
+
const editActionMetadata = (index: number, metadata: string): void =>
|
|
117
|
+
commitActions(
|
|
118
|
+
quickActions.map((action, actionIndex) =>
|
|
119
|
+
actionIndex === index
|
|
120
|
+
? { ...action, metadata: metadata.trim() ? metadata : undefined }
|
|
121
|
+
: action,
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
const resetActions = (): void => {
|
|
125
|
+
setActionsExpandedIndex(null);
|
|
126
|
+
commitActions(DEFAULT_QUICK_ACTIONS.map((action) => ({ ...action })));
|
|
127
|
+
};
|
|
128
|
+
const addAction = (): void => {
|
|
129
|
+
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: quickActions.length }));
|
|
130
|
+
commitActions([...quickActions, { prompt: "New action" }]);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const settingsCategories: SettingsCategory[] = [
|
|
134
|
+
{
|
|
135
|
+
id: "general",
|
|
136
|
+
name: "General",
|
|
137
|
+
description: "submission behaviour",
|
|
138
|
+
rows: [
|
|
139
|
+
{
|
|
140
|
+
key: "autoClose",
|
|
141
|
+
label: "Auto-close on submit",
|
|
142
|
+
kind: "cycle",
|
|
143
|
+
options: ["off", "3s", "10s"],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "display",
|
|
149
|
+
name: "Display",
|
|
150
|
+
description: "the review panel",
|
|
151
|
+
rows: [
|
|
152
|
+
{
|
|
153
|
+
key: "reviewPanel",
|
|
154
|
+
label: "Review panel",
|
|
155
|
+
kind: "cycle",
|
|
156
|
+
options: ["expanded", "compact", "hidden"],
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "appearance",
|
|
162
|
+
name: "Appearance",
|
|
163
|
+
description: "the color theme",
|
|
164
|
+
rows: [
|
|
165
|
+
{
|
|
166
|
+
key: "theme",
|
|
167
|
+
label: "Theme",
|
|
168
|
+
kind: "cycle",
|
|
169
|
+
options: THEME_NAMES.map((name) => THEME_LABELS[name]),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "actions",
|
|
175
|
+
name: "Actions",
|
|
176
|
+
description: "quick-action comments",
|
|
177
|
+
rows: [],
|
|
178
|
+
customBody: (
|
|
179
|
+
<QuickActionsEditor
|
|
180
|
+
actions={quickActions}
|
|
181
|
+
selectedIndex={settingsNav.categoryId === "actions" ? settingsNav.rowIndex : -1}
|
|
182
|
+
expandedIndex={actionsExpandedIndex}
|
|
183
|
+
onToggleExpand={(index) => {
|
|
184
|
+
setSettingsNav((state) => ({ ...state, zone: "body", rowIndex: index }));
|
|
185
|
+
setActionsExpandedIndex((current) => (current === index ? null : index));
|
|
186
|
+
}}
|
|
187
|
+
onEditMetadata={editActionMetadata}
|
|
188
|
+
onReset={resetActions}
|
|
189
|
+
onAdd={addAction}
|
|
190
|
+
theme={theme}
|
|
191
|
+
/>
|
|
192
|
+
),
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
const settingsValues = {
|
|
196
|
+
autoClose: autoClose === "off" ? "off" : `${autoClose}s`,
|
|
197
|
+
reviewPanel: reviewMode,
|
|
198
|
+
theme: THEME_LABELS[themeName],
|
|
199
|
+
};
|
|
200
|
+
const cycleSetting = (rowKey: string): void => {
|
|
201
|
+
if (rowKey === "autoClose") {
|
|
202
|
+
const next: AutoClose = autoClose === "off" ? 3 : autoClose === 3 ? 10 : "off";
|
|
203
|
+
|
|
204
|
+
setAutoClose(next);
|
|
205
|
+
persistAutoClose(next);
|
|
206
|
+
} else if (rowKey === "reviewPanel") {
|
|
207
|
+
const order: ReviewPanelMode[] = ["expanded", "compact", "hidden"];
|
|
208
|
+
const next = order[(order.indexOf(reviewMode) + 1) % order.length]!;
|
|
209
|
+
|
|
210
|
+
setReviewMode(next);
|
|
211
|
+
controller.saveReviewPanel({ mode: next });
|
|
212
|
+
} else if (rowKey === "theme") {
|
|
213
|
+
const next = THEME_NAMES[(THEME_NAMES.indexOf(themeName) + 1) % THEME_NAMES.length]!;
|
|
214
|
+
|
|
215
|
+
setThemeName(next);
|
|
216
|
+
setTheme(composeTheme(next, themeOverrides, appearance));
|
|
217
|
+
persistTheme(next);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const onCategorySelect = (categoryId: string): void => {
|
|
222
|
+
setActionsExpandedIndex(null);
|
|
223
|
+
setSettingsNav({ categoryId, rowIndex: 0, zone: "body" });
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleSettingsKey = (name: string): void => {
|
|
227
|
+
// an open system-prompt input owns typing; only esc (close it) escapes here
|
|
228
|
+
if (actionsExpandedIndex !== null) {
|
|
229
|
+
if (name === "escape") setActionsExpandedIndex(null);
|
|
230
|
+
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (name === "escape") return void setMenuDialog(null);
|
|
234
|
+
const categoryIndex = settingsCategories.findIndex(
|
|
235
|
+
(category) => category.id === settingsNav.categoryId,
|
|
236
|
+
);
|
|
237
|
+
const category = settingsCategories[categoryIndex]!;
|
|
238
|
+
|
|
239
|
+
if (settingsNav.zone === "nav") {
|
|
240
|
+
const next = moveNavZone(name, categoryIndex, settingsCategories);
|
|
241
|
+
|
|
242
|
+
if (next) setSettingsNav(next);
|
|
243
|
+
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
// the Actions category is a list of quick actions plus a trailing "add" row
|
|
247
|
+
if (category.id === "actions") {
|
|
248
|
+
const moved = moveBodyRow(name, settingsNav, quickActions.length + 1);
|
|
249
|
+
|
|
250
|
+
if (moved) return void setSettingsNav(moved);
|
|
251
|
+
if (ACTIVATE_KEYS.has(name)) {
|
|
252
|
+
if (settingsNav.rowIndex === quickActions.length) addAction();
|
|
253
|
+
else setActionsExpandedIndex(settingsNav.rowIndex);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const moved = moveBodyRow(name, settingsNav, category.rows.length);
|
|
259
|
+
|
|
260
|
+
if (moved) return void setSettingsNav(moved);
|
|
261
|
+
if (name === "return" || name === "space")
|
|
262
|
+
cycleSetting(category.rows[settingsNav.rowIndex]!.key);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
settingsNav,
|
|
267
|
+
actionsExpandedIndex,
|
|
268
|
+
setActionsExpandedIndex,
|
|
269
|
+
settingsCategories,
|
|
270
|
+
settingsValues,
|
|
271
|
+
cycleSetting,
|
|
272
|
+
handleSettingsKey,
|
|
273
|
+
onCategorySelect,
|
|
274
|
+
};
|
|
275
|
+
}
|
package/src/version.ts
ADDED
package/src/view-diff.test.ts
CHANGED
|
@@ -21,23 +21,32 @@ index 111..222 100644
|
|
|
21
21
|
|
|
22
22
|
describe("diffRows", () => {
|
|
23
23
|
test("flattens files, hunks, and signed lines with line numbers", () => {
|
|
24
|
+
// Act
|
|
24
25
|
const rows = diffRows(PATCH);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(rows[0]).toMatchObject({ kind: "file", file: "src/store.ts" });
|
|
29
|
+
expect(rows[1]!.kind).toBe("hunk");
|
|
30
|
+
const del = rows.find((row) => row.kind === "del")!;
|
|
31
|
+
|
|
28
32
|
expect(del.text).toContain("private items = [];");
|
|
29
33
|
expect(del.oldLine).toBe(3);
|
|
30
|
-
const adds = rows.filter((
|
|
34
|
+
const adds = rows.filter((row) => row.kind === "add");
|
|
35
|
+
|
|
31
36
|
expect(adds[0]!.text).toContain("new Map()");
|
|
32
37
|
expect(adds[0]!.newLine).toBe(3);
|
|
33
38
|
expect(adds[1]!.newLine).toBe(4);
|
|
34
39
|
// two hunks
|
|
35
|
-
expect(rows.filter((
|
|
40
|
+
expect(rows.filter((row) => row.kind === "hunk").length).toBe(2);
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
test("context lines carry both line numbers", () => {
|
|
44
|
+
// Act
|
|
39
45
|
const rows = diffRows(PATCH);
|
|
40
|
-
|
|
46
|
+
|
|
47
|
+
// Assert
|
|
48
|
+
const ctx = rows.find((row) => row.kind === "ctx")!;
|
|
49
|
+
|
|
41
50
|
expect(ctx.oldLine).toBe(1);
|
|
42
51
|
expect(ctx.newLine).toBe(1);
|
|
43
52
|
});
|
|
@@ -45,9 +54,14 @@ describe("diffRows", () => {
|
|
|
45
54
|
|
|
46
55
|
describe("diffRowAnchor", () => {
|
|
47
56
|
test("quote is the line, neighbors are the context selectors", () => {
|
|
57
|
+
// Arrange
|
|
48
58
|
const rows = diffRows(PATCH);
|
|
49
|
-
const
|
|
50
|
-
|
|
59
|
+
const rowIndex = rows.findIndex((row) => row.text.includes("new Map()"));
|
|
60
|
+
|
|
61
|
+
// Act
|
|
62
|
+
const anchor = diffRowAnchor(rows, rowIndex);
|
|
63
|
+
|
|
64
|
+
// Assert
|
|
51
65
|
expect(anchor.quote).toContain("new Map()");
|
|
52
66
|
expect(anchor.prefix.length).toBeGreaterThan(0);
|
|
53
67
|
expect(anchor.suffix.length).toBeGreaterThan(0);
|
package/src/view-diff.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Diff artifact projection
|
|
2
|
+
* Diff artifact projection: flatten @pierre/diffs' parsed
|
|
3
3
|
* patch model into render rows. Line-anchored annotations use the same
|
|
4
4
|
* quote-primary anchors as plans: quote = the line content, prefix/suffix =
|
|
5
5
|
* the neighbor lines - so the whole anchor/feedback pipeline is shared.
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs";
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type DiffRowKind = "file" | "hunk" | "ctx" | "add" | "del";
|
|
11
11
|
|
|
12
12
|
export interface DiffRow {
|
|
13
|
-
|
|
13
|
+
kind: DiffRowKind;
|
|
14
14
|
text: string;
|
|
15
15
|
file: string;
|
|
16
16
|
oldLine?: number;
|
|
@@ -20,37 +20,39 @@ export interface DiffRow {
|
|
|
20
20
|
export function diffRows(patchText: string): DiffRow[] {
|
|
21
21
|
const rows: DiffRow[] = [];
|
|
22
22
|
const patches = parsePatchFiles(patchText);
|
|
23
|
+
|
|
23
24
|
for (const patch of patches) {
|
|
24
25
|
for (const file of patch.files) {
|
|
25
|
-
rows.push({
|
|
26
|
+
rows.push({ kind: "file", text: fileLabel(file), file: file.name });
|
|
26
27
|
for (const hunk of file.hunks) {
|
|
27
|
-
rows.push({
|
|
28
|
+
rows.push({ kind: "hunk", text: hunk.hunkSpecs ?? "@@", file: file.name });
|
|
28
29
|
let oldLine = hunk.deletionStart;
|
|
29
30
|
let newLine = hunk.additionStart;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
for (const segment of hunk.hunkContent) {
|
|
33
|
+
if (segment.type === "context") {
|
|
34
|
+
for (let lineOffset = 0; lineOffset < segment.lines; lineOffset++) {
|
|
33
35
|
rows.push({
|
|
34
|
-
|
|
35
|
-
text: file.additionLines[
|
|
36
|
+
kind: "ctx",
|
|
37
|
+
text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
|
|
36
38
|
file: file.name,
|
|
37
39
|
oldLine: oldLine++,
|
|
38
40
|
newLine: newLine++,
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
} else {
|
|
42
|
-
for (let
|
|
44
|
+
for (let lineOffset = 0; lineOffset < segment.deletions; lineOffset++) {
|
|
43
45
|
rows.push({
|
|
44
|
-
|
|
45
|
-
text: file.deletionLines[
|
|
46
|
+
kind: "del",
|
|
47
|
+
text: file.deletionLines[segment.deletionLineIndex + lineOffset] ?? "",
|
|
46
48
|
file: file.name,
|
|
47
49
|
oldLine: oldLine++,
|
|
48
50
|
});
|
|
49
51
|
}
|
|
50
|
-
for (let
|
|
52
|
+
for (let lineOffset = 0; lineOffset < segment.additions; lineOffset++) {
|
|
51
53
|
rows.push({
|
|
52
|
-
|
|
53
|
-
text: file.additionLines[
|
|
54
|
+
kind: "add",
|
|
55
|
+
text: file.additionLines[segment.additionLineIndex + lineOffset] ?? "",
|
|
54
56
|
file: file.name,
|
|
55
57
|
newLine: newLine++,
|
|
56
58
|
});
|
|
@@ -60,27 +62,41 @@ export function diffRows(patchText: string): DiffRow[] {
|
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
|
|
63
66
|
return rows;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
function fileLabel(file: FileDiffMetadata): string {
|
|
67
|
-
return file.prevName && file.prevName !== file.name
|
|
70
|
+
return file.prevName && file.prevName !== file.name
|
|
71
|
+
? `${file.prevName} → ${file.name}`
|
|
72
|
+
: file.name;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/** Quote-primary anchor for a diff row: neighbors as context selectors. */
|
|
71
|
-
export function diffRowAnchor(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
export function diffRowAnchor(
|
|
77
|
+
rows: DiffRow[],
|
|
78
|
+
rowIndex: number,
|
|
79
|
+
): { quote: string; prefix: string; suffix: string } {
|
|
80
|
+
const row = rows[rowIndex]!;
|
|
81
|
+
const prev = rows[rowIndex - 1];
|
|
82
|
+
const next = rows[rowIndex + 1];
|
|
83
|
+
|
|
75
84
|
return {
|
|
76
85
|
quote: row.text,
|
|
77
|
-
prefix:
|
|
78
|
-
|
|
86
|
+
prefix:
|
|
87
|
+
prev && (prev.kind === "ctx" || prev.kind === "add" || prev.kind === "del")
|
|
88
|
+
? prev.text.slice(-24)
|
|
89
|
+
: "",
|
|
90
|
+
suffix:
|
|
91
|
+
next && (next.kind === "ctx" || next.kind === "add" || next.kind === "del")
|
|
92
|
+
? next.text.slice(0, 24)
|
|
93
|
+
: "",
|
|
79
94
|
};
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
/** Location label for the rail and feedback: file:newLine (or old for del). */
|
|
83
98
|
export function diffRowLocation(row: DiffRow): string {
|
|
84
99
|
const line = row.newLine ?? row.oldLine;
|
|
100
|
+
|
|
85
101
|
return line !== undefined ? `${row.file}:${line}` : row.file;
|
|
86
102
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
blockRuns,
|
|
4
|
+
buildDisplay,
|
|
5
|
+
displayText,
|
|
6
|
+
renderedOffsetFor,
|
|
7
|
+
workRangeForRendered,
|
|
8
|
+
} from "./view-plan";
|
|
9
|
+
|
|
10
|
+
// M0 (#232): lock the renderer's current StyleRun output and offset round-trips
|
|
11
|
+
// before the inline-tokenizer milestones (M1-M5) change blockRuns. The snapshots
|
|
12
|
+
// capture today's runs, so a later milestone's diff is explicit and reviewable.
|
|
13
|
+
// The round-trip assertions are the durable invariant: a visible word's rendered
|
|
14
|
+
// selection must always recover that word's *source* (block.text) offsets, even
|
|
15
|
+
// once inline markers are concealed - this is what keeps quote anchors exact.
|
|
16
|
+
|
|
17
|
+
const DOC = `# Heading with **bold**
|
|
18
|
+
|
|
19
|
+
A paragraph with **strong**, *em*, \`code\`, ~~strike~~ and a [label](https://example.com).
|
|
20
|
+
|
|
21
|
+
- a list item with *emphasis*
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const blocks = buildDisplay(DOC);
|
|
25
|
+
|
|
26
|
+
describe("view-plan regression harness (M0)", () => {
|
|
27
|
+
test("blockRuns output is stable across the representative blocks", () => {
|
|
28
|
+
// Arrange / Act
|
|
29
|
+
const runsByBlock = blocks.map((block) => ({
|
|
30
|
+
type: block.type,
|
|
31
|
+
text: displayText(block),
|
|
32
|
+
runs: blockRuns(block, true),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Assert
|
|
36
|
+
expect(runsByBlock).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// A visible word's source offsets survive the rendered round-trip. Holds at M0
|
|
40
|
+
// (1:1 today) and must keep holding once M2 conceals markers and the rendered
|
|
41
|
+
// offset shifts - the recovered work range stays the word's block.text span.
|
|
42
|
+
const paragraph = blocks.find((block) => displayText(block).includes("strong"))!;
|
|
43
|
+
const paragraphRuns = blockRuns(paragraph, true);
|
|
44
|
+
const paragraphText = displayText(paragraph);
|
|
45
|
+
|
|
46
|
+
for (const word of ["paragraph", "strong", "strike", "label"]) {
|
|
47
|
+
test(`rendered selection of "${word}" recovers its block.text offsets`, () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
const start = paragraphText.indexOf(word);
|
|
50
|
+
const rendered = renderedOffsetFor(paragraphRuns, start);
|
|
51
|
+
|
|
52
|
+
// Assert
|
|
53
|
+
expect(rendered).not.toBeNull();
|
|
54
|
+
expect(workRangeForRendered(paragraphRuns, rendered!, rendered! + word.length)).toEqual({
|
|
55
|
+
start,
|
|
56
|
+
end: start + word.length,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|