Merge branch 'main' into dev

This commit is contained in:
Mohamed Boudra
2026-04-10 17:15:58 +07:00
20 changed files with 641 additions and 228 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { PortalProvider } from "@gorhom/portal"; import { PortalProvider } from "@gorhom/portal";
import { VoiceProvider } from "@/contexts/voice-context"; import { VoiceProvider } from "@/contexts/voice-context";
import { useAppSettings } from "@/hooks/use-settings"; import { useAppSettings } from "@/hooks/use-settings";
import { THEME_TO_UNISTYLES } from "@/styles/theme";
import { useFaviconStatus } from "@/hooks/use-favicon-status"; import { useFaviconStatus } from "@/hooks/use-favicon-status";
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import { UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { UnistylesRuntime, useUnistyles } from "react-native-unistyles";
@@ -564,7 +565,7 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
UnistylesRuntime.setAdaptiveThemes(true); UnistylesRuntime.setAdaptiveThemes(true);
} else { } else {
UnistylesRuntime.setAdaptiveThemes(false); UnistylesRuntime.setAdaptiveThemes(false);
UnistylesRuntime.setTheme(settings.theme); UnistylesRuntime.setTheme(THEME_TO_UNISTYLES[settings.theme]);
} }
}, [settingsLoading, settings.theme]); }, [settingsLoading, settings.theme]);
@@ -472,7 +472,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
}, },
tabActive: { tabActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
tabText: { tabText: {
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
@@ -849,7 +849,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
}, },
entryRowActive: { entryRowActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
indentGuide: { indentGuide: {
position: "absolute", position: "absolute",
+49 -40
View File
@@ -58,7 +58,12 @@ import {
import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout"; import { WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
import { Fonts } from "@/constants/theme"; import { Fonts } from "@/constants/theme";
import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll"; import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll";
import { buildSplitDiffRows, type SplitDiffDisplayLine, type SplitDiffRow } from "@/utils/diff-layout"; import {
buildSplitDiffRows,
buildUnifiedDiffLines,
type SplitDiffDisplayLine,
type SplitDiffRow,
} from "@/utils/diff-layout";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -76,6 +81,11 @@ import { openExternalUrl } from "@/utils/open-external-url";
import { GitActionsSplitButton } from "@/components/git-actions-split-button"; import { GitActionsSplitButton } from "@/components/git-actions-split-button";
import { usePanelStore } from "@/stores/panel-store"; import { usePanelStore } from "@/stores/panel-store";
import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-actions"; import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-actions";
import {
formatDiffContentText,
formatDiffGutterText,
hasVisibleDiffTokens,
} from "@/utils/diff-rendering";
export type { GitActionId, GitAction, GitActions } from "@/components/git-actions-policy"; export type { GitActionId, GitAction, GitActions } from "@/components/git-actions-policy";
@@ -166,7 +176,7 @@ function DiffGutterCell({
type === "remove" && styles.removeLineNumberText, type === "remove" && styles.removeLineNumberText,
]} ]}
> >
{lineNumber != null ? String(lineNumber) : ""} {formatDiffGutterText(lineNumber)}
</Text> </Text>
</View> </View>
); );
@@ -179,10 +189,12 @@ function DiffTextLine({
line: DiffLine; line: DiffLine;
wrapLines: boolean; wrapLines: boolean;
}) { }) {
const visibleTokens = hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
return ( return (
<View style={[styles.textLineContainer, lineTypeBackground(line.type)]}> <View style={[styles.textLineContainer, lineTypeBackground(line.type)]}>
{line.tokens && line.type !== "header" ? ( {line.type !== "header" && visibleTokens ? (
<HighlightedText tokens={line.tokens} wrapLines={wrapLines} /> <HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
) : ( ) : (
<Text <Text
style={[ style={[
@@ -194,7 +206,7 @@ function DiffTextLine({
line.type === "context" && styles.contextLineText, line.type === "context" && styles.contextLineText,
]} ]}
> >
{line.content || " "} {formatDiffContentText(line.content)}
</Text> </Text>
)} )}
</View> </View>
@@ -208,10 +220,12 @@ function SplitTextLine({
line: SplitDiffDisplayLine | null; line: SplitDiffDisplayLine | null;
wrapLines: boolean; wrapLines: boolean;
}) { }) {
const visibleTokens = line && hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
return ( return (
<View style={[styles.textLineContainer, lineTypeBackground(line?.type)]}> <View style={[styles.textLineContainer, lineTypeBackground(line?.type)]}>
{line?.tokens ? ( {visibleTokens ? (
<HighlightedText tokens={line.tokens} wrapLines={wrapLines} /> <HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
) : ( ) : (
<Text <Text
style={[ style={[
@@ -223,7 +237,7 @@ function SplitTextLine({
!line && styles.emptySplitCellText, !line && styles.emptySplitCellText,
]} ]}
> >
{line?.content ?? ""} {formatDiffContentText(line?.content)}
</Text> </Text>
)} )}
</View> </View>
@@ -241,6 +255,8 @@ function DiffLineView({
gutterWidth: number; gutterWidth: number;
wrapLines: boolean; wrapLines: boolean;
}) { }) {
const visibleTokens = hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
return ( return (
<View <View
style={[ style={[
@@ -256,11 +272,11 @@ function DiffLineView({
line.type === "remove" && styles.removeLineNumberText, line.type === "remove" && styles.removeLineNumberText,
]} ]}
> >
{lineNumber != null ? String(lineNumber) : ""} {formatDiffGutterText(lineNumber)}
</Text> </Text>
</View> </View>
{line.tokens && line.type !== "header" ? ( {line.type !== "header" && visibleTokens ? (
<HighlightedText tokens={line.tokens} wrapLines={wrapLines} /> <HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
) : ( ) : (
<Text <Text
style={[ style={[
@@ -272,7 +288,7 @@ function DiffLineView({
line.type === "context" && styles.contextLineText, line.type === "context" && styles.contextLineText,
]} ]}
> >
{line.content || " "} {formatDiffContentText(line.content)}
</Text> </Text>
)} )}
</View> </View>
@@ -288,6 +304,8 @@ function SplitDiffLine({
gutterWidth: number; gutterWidth: number;
wrapLines: boolean; wrapLines: boolean;
}) { }) {
const visibleTokens = line && hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
return ( return (
<View <View
style={[ style={[
@@ -303,11 +321,11 @@ function SplitDiffLine({
line?.type === "remove" && styles.removeLineNumberText, line?.type === "remove" && styles.removeLineNumberText,
]} ]}
> >
{line?.lineNumber != null ? String(line.lineNumber) : ""} {formatDiffGutterText(line?.lineNumber ?? null)}
</Text> </Text>
</View> </View>
{line?.tokens ? ( {visibleTokens ? (
<HighlightedText tokens={line.tokens} wrapLines={wrapLines} /> <HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
) : ( ) : (
<Text <Text
style={[ style={[
@@ -319,7 +337,7 @@ function SplitDiffLine({
!line && styles.emptySplitCellText, !line && styles.emptySplitCellText,
]} ]}
> >
{line?.content ?? ""} {formatDiffContentText(line?.content)}
</Text> </Text>
)} )}
</View> </View>
@@ -535,26 +553,7 @@ function DiffFileBody({
); );
} }
const computedLines: { line: DiffLine; lineNumber: number | null; key: string }[] = []; const computedLines = buildUnifiedDiffLines(file);
for (const [hunkIndex, hunk] of file.hunks.entries()) {
let oldLineNo = hunk.oldStart;
let newLineNo = hunk.newStart;
for (const [lineIndex, line] of hunk.lines.entries()) {
let lineNumber: number | null = null;
if (line.type === "remove") {
lineNumber = oldLineNo;
oldLineNo++;
} else if (line.type === "add") {
lineNumber = newLineNo;
newLineNo++;
} else if (line.type === "context") {
lineNumber = newLineNo;
oldLineNo++;
newLineNo++;
}
computedLines.push({ line, lineNumber, key: `${hunkIndex}-${lineIndex}` });
}
}
if (wrapLines) { if (wrapLines) {
return ( return (
@@ -1725,7 +1724,7 @@ const styles = StyleSheet.create((theme) => ({
newBadgeText: { newBadgeText: {
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
color: theme.colors.palette.green[400], color: theme.colors.diffAddition,
}, },
deletedBadge: { deletedBadge: {
backgroundColor: "rgba(248, 81, 73, 0.2)", backgroundColor: "rgba(248, 81, 73, 0.2)",
@@ -1737,7 +1736,17 @@ const styles = StyleSheet.create((theme) => ({
deletedBadgeText: { deletedBadgeText: {
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal, fontWeight: theme.fontWeight.normal,
color: theme.colors.palette.red[500], color: theme.colors.diffDeletion,
},
additions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffAddition,
},
deletions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffDeletion,
}, },
diffContent: { diffContent: {
borderTopWidth: theme.borderWidth[1], borderTopWidth: theme.borderWidth[1],
@@ -1815,10 +1824,10 @@ const styles = StyleSheet.create((theme) => ({
userSelect: "none", userSelect: "none",
}, },
addLineNumberText: { addLineNumberText: {
color: theme.colors.palette.green[400], color: theme.colors.diffAddition,
}, },
removeLineNumberText: { removeLineNumberText: {
color: theme.colors.palette.red[500], color: theme.colors.diffDeletion,
}, },
diffLineText: { diffLineText: {
flex: 1, flex: 1,
+3 -3
View File
@@ -885,10 +885,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground, color: theme.colors.foreground,
}, },
newAgentButtonHovered: { newAgentButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
newAgentButtonActive: { newAgentButtonActive: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
hostTrigger: { hostTrigger: {
flexDirection: "row", flexDirection: "row",
@@ -901,7 +901,7 @@ const styles = StyleSheet.create((theme) => ({
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
}, },
hostTriggerHovered: { hostTriggerHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
hostStatusDot: { hostStatusDot: {
width: 8, width: 8,
@@ -2226,7 +2226,7 @@ const styles = StyleSheet.create((theme) => ({
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
projectRowHovered: { projectRowHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectRowPressed: { projectRowPressed: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
@@ -2296,7 +2296,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
projectActionButtonHovered: { projectActionButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectActionButtonText: { projectActionButtonText: {
color: theme.colors.foregroundMuted, color: theme.colors.foregroundMuted,
@@ -2311,7 +2311,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
projectIconActionButtonHovered: { projectIconActionButtonHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
projectIconActionButtonHidden: { projectIconActionButtonHidden: {
opacity: 0, opacity: 0,
@@ -2389,7 +2389,7 @@ const styles = StyleSheet.create((theme) => ({
flexShrink: 0, flexShrink: 0,
}, },
workspaceRowHovered: { workspaceRowHovered: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
workspaceRowPressed: { workspaceRowPressed: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
@@ -2403,7 +2403,7 @@ const styles = StyleSheet.create((theme) => ({
...theme.shadow.md, ...theme.shadow.md,
}, },
sidebarRowSelected: { sidebarRowSelected: {
backgroundColor: theme.colors.surface1, backgroundColor: theme.colors.surfaceSidebarHover,
}, },
workspaceRowContainer: { workspaceRowContainer: {
position: "relative", position: "relative",
@@ -2467,6 +2467,22 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
flexShrink: 0, flexShrink: 0,
}, },
diffStatRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
flexShrink: 0,
},
diffStatAdditions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffAddition,
},
diffStatDeletions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffDeletion,
},
kebabButton: { kebabButton: {
padding: 2, padding: 2,
borderRadius: 4, borderRadius: 4,
+3 -3
View File
@@ -550,9 +550,9 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[2], paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.xl, borderRadius: theme.borderRadius.xl,
backgroundColor: theme.colors.popover, backgroundColor: theme.colors.popover,
borderWidth: theme.borderWidth[2], borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border, borderColor: theme.colors.borderAccent,
...theme.shadow.sm, ...theme.shadow.md,
zIndex: 1000, zIndex: 1000,
}, },
})); }));
+8 -1
View File
@@ -6,10 +6,14 @@ export const APP_SETTINGS_KEY = "@paseo:app-settings";
const LEGACY_SETTINGS_KEY = "@paseo:settings"; const LEGACY_SETTINGS_KEY = "@paseo:settings";
const APP_SETTINGS_QUERY_KEY = ["app-settings"]; const APP_SETTINGS_QUERY_KEY = ["app-settings"];
import { THEME_TO_UNISTYLES, type ThemeName } from "@/styles/theme";
export type SendBehavior = "interrupt" | "queue"; export type SendBehavior = "interrupt" | "queue";
const VALID_THEMES = new Set<string>([...Object.keys(THEME_TO_UNISTYLES), "auto"]);
export interface AppSettings { export interface AppSettings {
theme: "dark" | "light" | "auto"; theme: ThemeName | "auto";
manageBuiltInDaemon: boolean; manageBuiltInDaemon: boolean;
sendBehavior: SendBehavior; sendBehavior: SendBehavior;
} }
@@ -78,6 +82,9 @@ export async function loadSettingsFromStorage(): Promise<AppSettings> {
const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY); const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY);
if (stored) { if (stored) {
const parsed = JSON.parse(stored) as Partial<AppSettings>; const parsed = JSON.parse(stored) as Partial<AppSettings>;
if (parsed.theme && !VALID_THEMES.has(parsed.theme)) {
parsed.theme = DEFAULT_APP_SETTINGS.theme;
}
return { ...DEFAULT_APP_SETTINGS, ...parsed }; return { ...DEFAULT_APP_SETTINGS, ...parsed };
} }
+96 -23
View File
@@ -10,6 +10,7 @@ import {
Sun, Sun,
Moon, Moon,
Monitor, Monitor,
ChevronDown,
Globe, Globe,
Settings, Settings,
RotateCw, RotateCw,
@@ -24,6 +25,7 @@ import {
Smartphone, Smartphone,
} from "lucide-react-native"; } from "lucide-react-native";
import { useAppSettings, type AppSettings, type SendBehavior } from "@/hooks/use-settings"; import { useAppSettings, type AppSettings, type SendBehavior } from "@/hooks/use-settings";
import { THEME_SWATCHES, type ThemeName } from "@/styles/theme";
import type { HostProfile, HostConnection } from "@/types/host-connection"; import type { HostProfile, HostConnection } from "@/types/host-connection";
import { useHosts, useHostMutations } from "@/runtime/host-runtime"; import { useHosts, useHostMutations } from "@/runtime/host-runtime";
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons"; import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
@@ -47,6 +49,7 @@ import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet"; import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
@@ -362,11 +365,53 @@ interface GeneralSectionProps {
handleSendBehaviorChange: (behavior: SendBehavior) => void; handleSendBehaviorChange: (behavior: SendBehavior) => void;
} }
function ThemeIcon({ theme, size, color }: { theme: AppSettings["theme"]; size: number; color: string }) {
switch (theme) {
case "light":
return <Sun size={size} color={color} />;
case "dark":
return <Moon size={size} color={color} />;
case "auto":
return <Monitor size={size} color={color} />;
default:
return <ThemeSwatch color={THEME_SWATCHES[theme]} size={size} />;
}
}
function ThemeSwatch({ color, size }: { color: string; size: number }) {
return (
<View
style={{
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: color,
borderWidth: 1,
borderColor: "rgba(255,255,255,0.15)",
}}
/>
);
}
const THEME_LABELS: Record<AppSettings["theme"], string> = {
light: "Light",
dark: "Dark",
zinc: "Zinc",
midnight: "Midnight",
claude: "Claude",
ghostty: "Ghostty",
auto: "System",
};
function GeneralSection({ function GeneralSection({
settings, settings,
handleThemeChange, handleThemeChange,
handleSendBehaviorChange, handleSendBehaviorChange,
}: GeneralSectionProps) { }: GeneralSectionProps) {
const { theme } = useUnistyles();
const iconSize = theme.iconSize.md;
const iconColor = theme.colors.foregroundMuted;
return ( return (
<View style={settingsStyles.section}> <View style={settingsStyles.section}>
<Text style={settingsStyles.sectionTitle}>General</Text> <Text style={settingsStyles.sectionTitle}>General</Text>
@@ -375,29 +420,43 @@ function GeneralSection({
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
<Text style={styles.audioRowTitle}>Theme</Text> <Text style={styles.audioRowTitle}>Theme</Text>
</View> </View>
<SegmentedControl <DropdownMenu>
size="sm" <DropdownMenuTrigger
hideLabels={Platform.OS !== "web"} style={({ pressed }) => [
value={settings.theme} styles.themeTrigger,
onValueChange={handleThemeChange} pressed && { opacity: 0.85 },
options={[ ]}
{ >
value: "light", <ThemeIcon theme={settings.theme} size={iconSize} color={iconColor} />
label: "Light", <Text style={styles.themeTriggerText}>
icon: ({ color, size }) => <Sun size={size} color={color} />, {THEME_LABELS[settings.theme]}
}, </Text>
{ <ChevronDown size={theme.iconSize.sm} color={iconColor} />
value: "dark", </DropdownMenuTrigger>
label: "Dark", <DropdownMenuContent side="bottom" align="end" width={200}>
icon: ({ color, size }) => <Moon size={size} color={color} />, {(["light", "dark", "auto"] as const).map((t) => (
}, <DropdownMenuItem
{ key={t}
value: "auto", selected={settings.theme === t}
label: "System", onSelect={() => handleThemeChange(t)}
icon: ({ color, size }) => <Monitor size={size} color={color} />, leading={<ThemeIcon theme={t} size={iconSize} color={iconColor} />}
}, >
]} {THEME_LABELS[t]}
/> </DropdownMenuItem>
))}
<DropdownMenuSeparator />
{(["zinc", "midnight", "claude", "ghostty"] as const).map((t) => (
<DropdownMenuItem
key={t}
selected={settings.theme === t}
onSelect={() => handleThemeChange(t)}
leading={<ThemeIcon theme={t} size={iconSize} color={iconColor} />}
>
{THEME_LABELS[t]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</View> </View>
<View style={styles.audioRow}> <View style={styles.audioRow}>
<View style={styles.audioRowContent}> <View style={styles.audioRowContent}>
@@ -1805,6 +1864,20 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.medium, fontWeight: theme.fontWeight.medium,
}, },
themeTrigger: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.border,
},
themeTriggerText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
disabled: { disabled: {
opacity: theme.opacity[50], opacity: theme.opacity[50],
}, },
@@ -2445,6 +2445,22 @@ const styles = StyleSheet.create((theme) => ({
sourceControlButtonHovered: { sourceControlButtonHovered: {
backgroundColor: theme.colors.surface2, backgroundColor: theme.colors.surface2,
}, },
diffStatRow: {
flexDirection: "row",
alignItems: "center",
gap: 6,
flexShrink: 0,
},
diffStatAdditions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffAddition,
},
diffStatDeletions: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.normal,
color: theme.colors.diffDeletion,
},
newTabActions: { newTabActions: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
+258 -96
View File
@@ -103,6 +103,19 @@ export const baseColors = {
}, },
} as const; } as const;
export type ThemeName = "light" | "dark" | "zinc" | "midnight" | "claude" | "ghostty";
// Diff stat colors — light uses muted tones, dark uses the brighter palette values
const lightDiffColors = {
diffAddition: "#15803d", // green-700 — readable on white without screaming
diffDeletion: "#b91c1c", // red-700
};
const darkDiffColors = {
diffAddition: "#4ade80", // green-400
diffDeletion: "#ef4444", // red-500
};
// Semantic color tokens - Layer-based system // Semantic color tokens - Layer-based system
const lightSemanticColors = { const lightSemanticColors = {
// Surfaces (layers) - shifted one step lighter // Surfaces (layers) - shifted one step lighter
@@ -113,10 +126,11 @@ const lightSemanticColors = {
surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300) surface4: "#d4d4d8", // Extra emphasis (was zinc-400, now zinc-300)
surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2 surfaceDiffEmpty: "#f6f6f6", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main) surfaceSidebar: "#f4f4f5", // Sidebar background (darker than main)
surfaceSidebarHover: "#e9e9ec", // Sidebar hover (darker in light mode)
surfaceWorkspace: "#ffffff", // Workspace main background surfaceWorkspace: "#ffffff", // Workspace main background
// Text // Text
foreground: "#09090b", foreground: "#1a1a1e",
foregroundMuted: "#71717a", foregroundMuted: "#71717a",
// Controls // Controls
@@ -140,26 +154,28 @@ const lightSemanticColors = {
// Legacy aliases (for gradual migration) // Legacy aliases (for gradual migration)
background: "#ffffff", background: "#ffffff",
popover: "#ffffff", popover: "#ffffff",
popoverForeground: "#09090b", popoverForeground: "#1a1a1e",
primary: "#18181b", primary: "#18181b",
primaryForeground: "#fafafa", primaryForeground: "#fafafa",
secondary: "#f4f4f5", secondary: "#f4f4f5",
secondaryForeground: "#09090b", secondaryForeground: "#1a1a1e",
muted: "#f4f4f5", muted: "#f4f4f5",
mutedForeground: "#71717a", mutedForeground: "#71717a",
accentBorder: "#ececf1", accentBorder: "#ececf1",
input: "#f4f4f5", input: "#f4f4f5",
ring: "#18181b", ring: "#18181b",
...lightDiffColors,
terminal: { terminal: {
background: "#ffffff", background: "#ffffff",
foreground: "#09090b", foreground: "#1a1a1e",
cursor: "#09090b", cursor: "#1a1a1e",
cursorAccent: "#ffffff", cursorAccent: "#ffffff",
selectionBackground: "rgba(0, 0, 0, 0.15)", selectionBackground: "rgba(0, 0, 0, 0.15)",
selectionForeground: "#09090b", selectionForeground: "#1a1a1e",
black: "#09090b", black: "#1a1a1e",
red: "#dc2626", red: "#dc2626",
green: "#16a34a", green: "#16a34a",
yellow: "#ca8a04", yellow: "#ca8a04",
@@ -179,80 +195,196 @@ const lightSemanticColors = {
}, },
} as const; } as const;
const darkSemanticColors = { // ---------------------------------------------------------------------------
// Surfaces (layers) — subtle teal tint // Dark theme variant builder
surface0: "#181B1A", // App background // ---------------------------------------------------------------------------
surface1: "#1E2120", // Subtle hover
surface2: "#272A29", // Elevated: badges, inputs, sheets
surface3: "#434645", // Highest elevation
surface4: "#595B5B", // Extra emphasis
surfaceDiffEmpty: "#252827", // Empty side of split diff rows, between surface1 and surface2 and biased toward surface2
surfaceSidebar: "#141716", // Sidebar background (darker than main)
surfaceWorkspace: "#1E2120", // Workspace main background (surface1)
// Text interface DarkThemeConfig {
foreground: "#fafafa", surface0: string;
surface1: string;
surface2: string;
surface3: string;
surface4: string;
surfaceDiffEmpty: string;
surfaceSidebar: string;
surfaceSidebarHover: string;
foregroundMuted: string;
scrollbarHandle: string;
border: string;
borderAccent: string;
accent: string;
accentBright: string;
}
const darkTerminalAnsi = {
red: "#e07070",
green: "#5dba80",
yellow: "#d4a44a",
blue: "#6a9de0",
magenta: "#b07ad0",
cyan: "#4aabb8",
white: "#d4d4d8",
brightRed: "#e89090",
brightGreen: "#7ecf9a",
brightYellow: "#e0be6e",
brightBlue: "#8ab4e8",
brightMagenta: "#c49ae0",
brightCyan: "#6ec2cc",
brightWhite: "#f0f0f2",
} as const;
function buildDarkSemanticColors(tint: DarkThemeConfig) {
return {
surface0: tint.surface0,
surface1: tint.surface1,
surface2: tint.surface2,
surface3: tint.surface3,
surface4: tint.surface4,
surfaceDiffEmpty: tint.surfaceDiffEmpty,
surfaceSidebar: tint.surfaceSidebar,
surfaceSidebarHover: tint.surfaceSidebarHover,
surfaceWorkspace: tint.surface1,
foreground: "#fafafa",
foregroundMuted: tint.foregroundMuted,
scrollbarHandle: tint.scrollbarHandle,
border: tint.border,
borderAccent: tint.borderAccent,
accent: tint.accent,
accentBright: tint.accentBright,
accentForeground: "#ffffff",
destructive: "#ef4444",
destructiveForeground: "#ffffff",
success: tint.accent,
successForeground: "#ffffff",
// Legacy aliases (for gradual migration)
background: tint.surface0,
popover: tint.surface2,
popoverForeground: "#fafafa",
primary: "#fafafa",
primaryForeground: tint.surface0,
secondary: tint.surface2,
secondaryForeground: "#fafafa",
muted: tint.surface2,
mutedForeground: tint.foregroundMuted,
accentBorder: tint.borderAccent,
input: tint.surface2,
ring: "#d4d4d8",
...darkDiffColors,
terminal: {
background: tint.surface0,
foreground: "#fafafa",
cursor: "#fafafa",
cursorAccent: tint.surface0,
selectionBackground: "rgba(255, 255, 255, 0.2)",
selectionForeground: "#fafafa",
black: tint.surfaceSidebar,
...darkTerminalAnsi,
brightBlack: tint.surface3,
},
};
}
// ---------------------------------------------------------------------------
// Dark tint definitions
// ---------------------------------------------------------------------------
// Paseo — subtle teal-green tint (default)
const paseoDarkColors = buildDarkSemanticColors({
surface0: "#181B1A",
surface1: "#1E2120",
surface2: "#272A29",
surface3: "#434645",
surface4: "#595B5B",
surfaceDiffEmpty: "#252827",
surfaceSidebar: "#141716",
surfaceSidebarHover: "#1c1f1e",
foregroundMuted: "#A1A5A4", foregroundMuted: "#A1A5A4",
scrollbarHandle: "#717574",
// Controls
scrollbarHandle: "#717574", // zinc-500 w/ teal tint
// Borders
border: "#252B2A", border: "#252B2A",
borderAccent: "#2F3534", borderAccent: "#2F3534",
// Brand
accent: "#20744A", accent: "#20744A",
accentBright: "#7ccba0", accentBright: "#7ccba0",
accentForeground: "#ffffff", });
// Semantic // Zinc — neutral gray, no tint
destructive: "#ef4444", const zincDarkColors = buildDarkSemanticColors({
destructiveForeground: "#ffffff", surface0: "#18181b",
success: "#20744A", surface1: "#1f1f22",
successForeground: "#ffffff", surface2: "#27272a",
surface3: "#3f3f46",
surface4: "#52525b",
surfaceDiffEmpty: "#242427",
surfaceSidebar: "#131316",
surfaceSidebarHover: "#1b1b1e",
foregroundMuted: "#a1a1aa",
scrollbarHandle: "#71717a",
border: "#27272a",
borderAccent: "#303036",
accent: "#20744A",
accentBright: "#7ccba0",
});
// Legacy aliases (for gradual migration) // Midnight — subtle blue tint
background: "#181B1A", const midnightDarkColors = buildDarkSemanticColors({
popover: "#272A29", surface0: "#161820",
popoverForeground: "#fafafa", surface1: "#1c1e27",
primary: "#fafafa", surface2: "#252731",
primaryForeground: "#181B1A", surface3: "#3c3e4c",
secondary: "#272A29", surface4: "#535564",
secondaryForeground: "#fafafa", surfaceDiffEmpty: "#222430",
muted: "#272A29", surfaceSidebar: "#121420",
mutedForeground: "#A1A5A4", surfaceSidebarHover: "#1a1c28",
accentBorder: "#2F3534", foregroundMuted: "#9a9db0",
input: "#272A29", scrollbarHandle: "#6b6e82",
ring: "#d4d4d8", border: "#242636",
borderAccent: "#2e3040",
accent: "#3b6fcf",
accentBright: "#7eaaeb",
});
terminal: { // Claude — warm neutral with subtle orange undertone
background: "#181B1A", const claudeDarkColors = buildDarkSemanticColors({
foreground: "#fafafa", surface0: "#1f1f1e",
cursor: "#fafafa", surface1: "#262523",
cursorAccent: "#181B1A", surface2: "#2f2d2b",
selectionBackground: "rgba(255, 255, 255, 0.2)", surface3: "#4a4745",
selectionForeground: "#fafafa", surface4: "#605d5b",
surfaceDiffEmpty: "#2a2826",
surfaceSidebar: "#1a1918",
surfaceSidebarHover: "#222120",
foregroundMuted: "#ada9a5",
scrollbarHandle: "#78746f",
border: "#2c2a27",
borderAccent: "#36332f",
accent: "#d97757",
accentBright: "#e89a7f",
});
black: "#141716", // Ghostty — blue-tinted dark based on Ghostty default background
red: "#ef4444", const ghosttyDarkColors = buildDarkSemanticColors({
green: "#22c55e", surface0: "#282c34",
yellow: "#f59e0b", surface1: "#2f333d",
blue: "#3b82f6", surface2: "#383c48",
magenta: "#a855f7", surface3: "#4a4f5e",
cyan: "#06b6d4", surface4: "#5b6175",
white: "#e4e4e7", surfaceDiffEmpty: "#323643",
surfaceSidebar: "#21252d",
brightBlack: "#434645", surfaceSidebarHover: "#292d36",
brightRed: "#f87171", foregroundMuted: "#c8ccd8",
brightGreen: "#4ade80", scrollbarHandle: "#a0a4b2",
brightYellow: "#fbbf24", border: "#353a47",
brightBlue: "#60a5fa", borderAccent: "#3f4454",
brightMagenta: "#c084fc", accent: "#89b4fa",
brightCyan: "#22d3ee", accentBright: "#b4d0fc",
brightWhite: "#ffffff", });
},
} as const;
const commonTheme = { const commonTheme = {
spacing: { spacing: {
@@ -324,35 +456,45 @@ const commonTheme = {
}, },
} as const; } as const;
export const darkTheme = { const darkShadow = {
colorScheme: "dark" as const, sm: {
colors: { shadowColor: "rgba(0, 0, 0, 0.25)",
...darkSemanticColors, shadowOffset: { width: 0, height: 2 },
palette: baseColors, shadowRadius: 4,
elevation: 2,
}, },
shadow: { md: {
sm: { shadowColor: "rgba(0, 0, 0, 0.20)",
shadowColor: "rgba(0, 0, 0, 0.25)", shadowOffset: { width: 0, height: 4 },
shadowOffset: { width: 0, height: 2 }, shadowRadius: 8,
shadowRadius: 4, elevation: 8,
elevation: 2, },
}, lg: {
md: { shadowColor: "rgba(0, 0, 0, 0.40)",
shadowColor: "rgba(0, 0, 0, 0.20)", shadowOffset: { width: 0, height: 12 },
shadowOffset: { width: 0, height: 4 }, shadowRadius: 24,
shadowRadius: 8, elevation: 8,
elevation: 8,
},
lg: {
shadowColor: "rgba(0, 0, 0, 0.40)",
shadowOffset: { width: 0, height: 12 },
shadowRadius: 24,
elevation: 8,
},
}, },
...commonTheme,
} as const; } as const;
function buildDarkTheme(semanticColors: ReturnType<typeof buildDarkSemanticColors>) {
return {
colorScheme: "dark" as const,
colors: {
...semanticColors,
palette: baseColors,
},
shadow: darkShadow,
...commonTheme,
} as const;
}
export const darkTheme = buildDarkTheme(paseoDarkColors);
export const darkZincTheme = buildDarkTheme(zincDarkColors);
export const darkMidnightTheme = buildDarkTheme(midnightDarkColors);
export const darkClaudeTheme = buildDarkTheme(claudeDarkColors);
export const darkGhosttyTheme = buildDarkTheme(ghosttyDarkColors);
export const lightTheme = { export const lightTheme = {
colorScheme: "light" as const, colorScheme: "light" as const,
colors: { colors: {
@@ -387,3 +529,23 @@ export const theme = darkTheme;
// Export a union type that works for both themes // Export a union type that works for both themes
export type Theme = typeof darkTheme | typeof lightTheme; export type Theme = typeof darkTheme | typeof lightTheme;
type UnistylesThemeKey = "light" | "dark" | "darkZinc" | "darkMidnight" | "darkClaude" | "darkGhostty";
export const THEME_TO_UNISTYLES: Record<ThemeName, UnistylesThemeKey> = {
light: "light",
dark: "dark",
zinc: "darkZinc",
midnight: "darkMidnight",
claude: "darkClaude",
ghostty: "darkGhostty",
};
export const THEME_SWATCHES: Record<ThemeName, string> = {
light: "#ffffff",
dark: "#2D8B62",
zinc: "#808080",
midnight: "#4A6BA8",
claude: "#D97757",
ghostty: "#8caaee",
};
+16 -4
View File
@@ -1,11 +1,21 @@
import { StyleSheet } from "react-native-unistyles"; import { StyleSheet } from "react-native-unistyles";
// import { UnistylesRuntime } from "react-native-unistyles"; import {
import { lightTheme, darkTheme } from "./theme"; lightTheme,
darkTheme,
darkZincTheme,
darkMidnightTheme,
darkClaudeTheme,
darkGhosttyTheme,
} from "./theme";
StyleSheet.configure({ StyleSheet.configure({
themes: { themes: {
light: lightTheme, light: lightTheme,
dark: darkTheme, dark: darkTheme,
darkZinc: darkZincTheme,
darkMidnight: darkMidnightTheme,
darkClaude: darkClaudeTheme,
darkGhostty: darkGhosttyTheme,
}, },
breakpoints: { breakpoints: {
xs: 0, xs: 0,
@@ -23,6 +33,10 @@ StyleSheet.configure({
type AppThemes = { type AppThemes = {
light: typeof lightTheme; light: typeof lightTheme;
dark: typeof darkTheme; dark: typeof darkTheme;
darkZinc: typeof darkZincTheme;
darkMidnight: typeof darkMidnightTheme;
darkClaude: typeof darkClaudeTheme;
darkGhostty: typeof darkGhosttyTheme;
}; };
type AppBreakpoints = { type AppBreakpoints = {
@@ -37,5 +51,3 @@ declare module "react-native-unistyles" {
export interface UnistylesThemes extends AppThemes {} export interface UnistylesThemes extends AppThemes {}
export interface UnistylesBreakpoints extends AppBreakpoints {} export interface UnistylesBreakpoints extends AppBreakpoints {}
} }
// UnistylesRuntime.setRootViewBackgroundColor(lightTheme.colors.background);
+74 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildSplitDiffRows } from "./diff-layout"; import { buildSplitDiffRows, buildUnifiedDiffLines } from "./diff-layout";
import type { ParsedDiffFile } from "@/hooks/use-checkout-diff-query"; import type { ParsedDiffFile } from "@/hooks/use-checkout-diff-query";
function makeFile(lines: ParsedDiffFile["hunks"][number]["lines"]): ParsedDiffFile { function makeFile(lines: ParsedDiffFile["hunks"][number]["lines"]): ParsedDiffFile {
@@ -79,3 +79,76 @@ describe("buildSplitDiffRows", () => {
}); });
}); });
}); });
describe("buildUnifiedDiffLines", () => {
it("computes line numbers per line type within a hunk", () => {
const lines = buildUnifiedDiffLines(
makeFile([
{ type: "header", content: "@@ -10,3 +10,4 @@" },
{ type: "context", content: "before" },
{ type: "add", content: "inserted" },
{ type: "remove", content: "removed" },
{ type: "context", content: "after" },
]),
);
expect(
lines.map(({ line, lineNumber }) => ({
type: line.type,
lineNumber,
content: line.content,
})),
).toEqual([
{ type: "header", lineNumber: null, content: "@@ -10,3 +10,4 @@" },
{ type: "context", lineNumber: 10, content: "before" },
{ type: "add", lineNumber: 11, content: "inserted" },
{ type: "remove", lineNumber: 11, content: "removed" },
{ type: "context", lineNumber: 12, content: "after" },
]);
});
it("restarts numbering at each hunk boundary", () => {
const file: ParsedDiffFile = {
path: "example.ts",
isNew: false,
isDeleted: false,
additions: 1,
deletions: 0,
status: "ok",
hunks: [
{
oldStart: 75,
oldCount: 2,
newStart: 75,
newCount: 3,
lines: [
{ type: "header", content: "@@ -75,2 +75,3 @@" },
{ type: "context", content: "first" },
{ type: "add", content: "inserted" },
{ type: "context", content: "second" },
],
},
{
oldStart: 165,
oldCount: 2,
newStart: 166,
newCount: 2,
lines: [
{ type: "header", content: "@@ -165,2 +166,2 @@" },
{ type: "context", content: "third" },
{ type: "context", content: "fourth" },
],
},
],
};
const lines = buildUnifiedDiffLines(file);
expect(lines[0]?.lineNumber).toBeNull();
expect(lines[1]?.lineNumber).toBe(75);
expect(lines[3]?.lineNumber).toBe(77);
expect(lines[4]?.lineNumber).toBeNull();
expect(lines[5]?.lineNumber).toBe(166);
expect(lines[6]?.lineNumber).toBe(167);
});
});
+39
View File
@@ -7,6 +7,12 @@ export interface SplitDiffDisplayLine {
lineNumber: number | null; lineNumber: number | null;
} }
export interface UnifiedDiffDisplayLine {
key: string;
line: DiffLine;
lineNumber: number | null;
}
export type SplitDiffRow = export type SplitDiffRow =
| { | {
kind: "header"; kind: "header";
@@ -61,6 +67,39 @@ function toDisplayLine(input: {
}; };
} }
export function buildUnifiedDiffLines(file: ParsedDiffFile): UnifiedDiffDisplayLine[] {
const lines: UnifiedDiffDisplayLine[] = [];
for (const [hunkIndex, hunk] of file.hunks.entries()) {
let oldLineNo = hunk.oldStart;
let newLineNo = hunk.newStart;
for (const [lineIndex, line] of hunk.lines.entries()) {
let lineNumber: number | null = null;
if (line.type === "remove") {
lineNumber = oldLineNo;
oldLineNo += 1;
} else if (line.type === "add") {
lineNumber = newLineNo;
newLineNo += 1;
} else if (line.type === "context") {
lineNumber = newLineNo;
oldLineNo += 1;
newLineNo += 1;
}
lines.push({
key: `${hunkIndex}-${lineIndex}`,
line,
lineNumber,
});
}
}
return lines;
}
export function buildSplitDiffRows(file: ParsedDiffFile): SplitDiffRow[] { export function buildSplitDiffRows(file: ParsedDiffFile): SplitDiffRow[] {
const rows: SplitDiffRow[] = []; const rows: SplitDiffRow[] = [];
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { formatDiffContentText, formatDiffGutterText, hasVisibleDiffTokens } from "./diff-rendering";
describe("diff-rendering", () => {
it("keeps header gutters tall even when they do not show a line number", () => {
expect(formatDiffGutterText(null)).toBe(" ");
expect(formatDiffGutterText(82)).toBe("82");
});
it("keeps empty split cells tall even when they have no visible content", () => {
expect(formatDiffContentText(undefined)).toBe(" ");
expect(formatDiffContentText("")).toBe(" ");
expect(formatDiffContentText("const value = 1;")).toBe("const value = 1;");
});
it("treats empty highlighted token rows as blank lines instead of visible content", () => {
expect(hasVisibleDiffTokens(undefined)).toBe(false);
expect(hasVisibleDiffTokens([])).toBe(false);
expect(hasVisibleDiffTokens([{ text: "" }])).toBe(false);
expect(hasVisibleDiffTokens([{ text: "const value = 1;" }])).toBe(true);
});
});
+16
View File
@@ -0,0 +1,16 @@
interface HighlightLikeToken {
text: string;
}
// Preserve row height when a gutter or diff cell is intentionally blank.
export function formatDiffGutterText(lineNumber: number | null): string {
return lineNumber == null ? " " : String(lineNumber);
}
export function formatDiffContentText(content: string | null | undefined): string {
return content && content.length > 0 ? content : " ";
}
export function hasVisibleDiffTokens(tokens: HighlightLikeToken[] | null | undefined): boolean {
return Boolean(tokens?.some((token) => token.text.length > 0));
}
+13 -5
View File
@@ -52,7 +52,7 @@ describe("terminal key helpers", () => {
}); });
}); });
it("intercepts special keys and modifier combos", () => { it("only intercepts when pending modifiers are active", () => {
expect( expect(
shouldInterceptDomTerminalKey({ shouldInterceptDomTerminalKey({
key: "Escape", key: "Escape",
@@ -60,7 +60,7 @@ describe("terminal key helpers", () => {
altKey: false, altKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false }, pendingModifiers: { ctrl: false, shift: false, alt: false },
}), }),
).toBe(true); ).toBe(false);
expect( expect(
shouldInterceptDomTerminalKey({ shouldInterceptDomTerminalKey({
key: "c", key: "c",
@@ -68,15 +68,23 @@ describe("terminal key helpers", () => {
altKey: false, altKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false }, pendingModifiers: { ctrl: false, shift: false, alt: false },
}), }),
).toBe(true); ).toBe(false);
expect( expect(
shouldInterceptDomTerminalKey({ shouldInterceptDomTerminalKey({
key: "c", key: "c",
ctrlKey: false, ctrlKey: false,
altKey: false, altKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: false }, pendingModifiers: { ctrl: true, shift: false, alt: false },
}), }),
).toBe(false); ).toBe(true);
expect(
shouldInterceptDomTerminalKey({
key: "Escape",
ctrlKey: false,
altKey: false,
pendingModifiers: { ctrl: false, shift: false, alt: true },
}),
).toBe(true);
}); });
it("detects pending modifier state", () => { it("detects pending modifier state", () => {
+1 -6
View File
@@ -88,12 +88,7 @@ export function shouldInterceptDomTerminalKey(args: {
altKey: boolean; altKey: boolean;
pendingModifiers: PendingTerminalModifiers; pendingModifiers: PendingTerminalModifiers;
}): boolean { }): boolean {
return ( return hasPendingTerminalModifiers(args.pendingModifiers);
args.key.length > 1 ||
args.ctrlKey ||
args.altKey ||
hasPendingTerminalModifiers(args.pendingModifiers)
);
} }
export function mergeTerminalModifiers(args: { export function mergeTerminalModifiers(args: {
-37
View File
@@ -220,8 +220,6 @@ function clientSupportsFlexibleEditorIds(appVersion: string | null): boolean {
const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500; const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__"; const WORKSPACE_GIT_WATCH_REMOVED_FINGERPRINT = "__removed__";
const TERMINAL_STREAM_HIGH_WATER_BYTES = 256 * 1024;
const TERMINAL_STREAM_LOW_WATER_BYTES = 16 * 1024;
const MAX_TERMINAL_STREAM_SLOTS = 256; const MAX_TERMINAL_STREAM_SLOTS = 256;
const pendingAgentInitializations = new Map<string, Promise<ManagedAgent>>(); const pendingAgentInitializations = new Map<string, Promise<ManagedAgent>>();
@@ -299,7 +297,6 @@ type ActiveTerminalStream = {
slot: number; slot: number;
unsubscribe: () => void; unsubscribe: () => void;
needsSnapshot: boolean; needsSnapshot: boolean;
snapshotRetryTimer: ReturnType<typeof setTimeout> | null;
}; };
export type SessionRuntimeMetrics = { export type SessionRuntimeMetrics = {
@@ -8618,7 +8615,6 @@ export class Session {
slot, slot,
unsubscribe: () => {}, unsubscribe: () => {},
needsSnapshot: true, needsSnapshot: true,
snapshotRetryTimer: null,
}; };
this.activeTerminalStreams.set(slot, activeStream); this.activeTerminalStreams.set(slot, activeStream);
@@ -8636,10 +8632,6 @@ export class Session {
if (activeStream.needsSnapshot || message.data.length === 0) { if (activeStream.needsSnapshot || message.data.length === 0) {
return; return;
} }
if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
this.markAllActiveTerminalStreamsForSnapshot();
return;
}
this.emitBinary( this.emitBinary(
encodeTerminalStreamFrame({ encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Output, opcode: TerminalStreamOpcode.Output,
@@ -8647,9 +8639,6 @@ export class Session {
payload: new Uint8Array(Buffer.from(message.data, "utf8")), payload: new Uint8Array(Buffer.from(message.data, "utf8")),
}), }),
); );
if (this.getCurrentBinaryBufferedAmount() >= TERMINAL_STREAM_HIGH_WATER_BYTES) {
this.markAllActiveTerminalStreamsForSnapshot();
}
}); });
return slot; return slot;
} }
@@ -8662,21 +8651,6 @@ export class Session {
return; return;
} }
if (this.getCurrentBinaryBufferedAmount() > TERMINAL_STREAM_LOW_WATER_BYTES) {
if (!activeStream.snapshotRetryTimer) {
activeStream.snapshotRetryTimer = setTimeout(() => {
activeStream.snapshotRetryTimer = null;
this.trySendTerminalSnapshot(activeStream);
}, 33);
}
return;
}
if (activeStream.snapshotRetryTimer) {
clearTimeout(activeStream.snapshotRetryTimer);
activeStream.snapshotRetryTimer = null;
}
const terminal = this.terminalManager?.getTerminal(activeStream.terminalId); const terminal = this.terminalManager?.getTerminal(activeStream.terminalId);
if (!terminal) { if (!terminal) {
this.detachTerminalStream(activeStream.terminalId, { emitExit: true }); this.detachTerminalStream(activeStream.terminalId, { emitExit: true });
@@ -8693,13 +8667,6 @@ export class Session {
); );
} }
private markAllActiveTerminalStreamsForSnapshot(): void {
for (const activeStream of this.activeTerminalStreams.values()) {
activeStream.needsSnapshot = true;
this.trySendTerminalSnapshot(activeStream);
}
}
private allocateTerminalSlot(): number | null { private allocateTerminalSlot(): number | null {
for (let attempt = 0; attempt < MAX_TERMINAL_STREAM_SLOTS; attempt += 1) { for (let attempt = 0; attempt < MAX_TERMINAL_STREAM_SLOTS; attempt += 1) {
const slot = (this.nextTerminalSlot + attempt) % MAX_TERMINAL_STREAM_SLOTS; const slot = (this.nextTerminalSlot + attempt) % MAX_TERMINAL_STREAM_SLOTS;
@@ -8724,10 +8691,6 @@ export class Session {
} }
this.activeTerminalStreams.delete(slot); this.activeTerminalStreams.delete(slot);
this.terminalIdToSlot.delete(terminalId); this.terminalIdToSlot.delete(terminalId);
if (activeStream.snapshotRetryTimer) {
clearTimeout(activeStream.snapshotRetryTimer);
activeStream.snapshotRetryTimer = null;
}
try { try {
activeStream.unsubscribe(); activeStream.unsubscribe();
} catch (error) { } catch (error) {
+1 -1
View File
@@ -2,7 +2,7 @@
"worktree": { "worktree": {
"setup": [ "setup": [
"npm ci", "npm ci",
"npm run build --workspace=@getpaseo/relay", "npm run build:daemon",
"cp \"$PASEO_SOURCE_CHECKOUT_PATH/packages/server/.env\" \"$PASEO_WORKTREE_PATH/packages/server/.env\"" "cp \"$PASEO_SOURCE_CHECKOUT_PATH/packages/server/.env\" \"$PASEO_WORKTREE_PATH/packages/server/.env\""
] ]
}, },