Merge branch 'main' into dev
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
@@ -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\""
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user