feat: add Tauri desktop app support and terminal management
This commit is contained in:
@@ -101,10 +101,6 @@ export function AgentInputArea({
|
||||
|
||||
const setQueuedMessages = useSessionStore((state) => state.setQueuedMessages);
|
||||
|
||||
// Noop fallback for sendAgentAudio (required by MessageInput)
|
||||
const noopSendAgentAudio = useCallback(async () => {}, []);
|
||||
const sendAgentAudio = methods?.sendAgentAudio ?? noopSendAgentAudio;
|
||||
|
||||
const { startRealtime, stopRealtime, isRealtimeMode } = useRealtime();
|
||||
|
||||
const [internalInput, setInternalInput] = useState("");
|
||||
@@ -448,52 +444,47 @@ export function AgentInputArea({
|
||||
]
|
||||
);
|
||||
|
||||
const rightContent = (
|
||||
<>
|
||||
<Pressable
|
||||
onPress={handleRealtimePress}
|
||||
disabled={!isConnected && !isRealtimeMode}
|
||||
style={[
|
||||
styles.realtimeButton as any,
|
||||
(isRealtimeMode ? styles.realtimeButtonActive : undefined) as any,
|
||||
(!isConnected && !isRealtimeMode
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isRealtimeMode ? (
|
||||
<Square
|
||||
size={18}
|
||||
color={theme.colors.background}
|
||||
fill={theme.colors.background}
|
||||
/>
|
||||
) : (
|
||||
<AudioLines size={20} color={theme.colors.background} />
|
||||
)}
|
||||
</Pressable>
|
||||
{isAgentRunning && (
|
||||
<Pressable
|
||||
onPress={handleCancelAgent}
|
||||
disabled={!isConnected || isCancellingAgent}
|
||||
accessibilityLabel={
|
||||
isCancellingAgent ? "Canceling agent" : "Stop agent"
|
||||
}
|
||||
accessibilityRole="button"
|
||||
style={[
|
||||
styles.cancelButton as any,
|
||||
(!isConnected || isCancellingAgent
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isCancellingAgent ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Square size={18} color="white" fill="white" />
|
||||
)}
|
||||
</Pressable>
|
||||
const rightContent = isAgentRunning ? (
|
||||
<Pressable
|
||||
onPress={handleCancelAgent}
|
||||
disabled={!isConnected || isCancellingAgent}
|
||||
accessibilityLabel={isCancellingAgent ? "Canceling agent" : "Stop agent"}
|
||||
accessibilityRole="button"
|
||||
style={[
|
||||
styles.cancelButton as any,
|
||||
(!isConnected || isCancellingAgent
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isCancellingAgent ? (
|
||||
<ActivityIndicator size="small" color="white" />
|
||||
) : (
|
||||
<Square size={18} color="white" fill="white" />
|
||||
)}
|
||||
</>
|
||||
</Pressable>
|
||||
) : (
|
||||
<Pressable
|
||||
onPress={handleRealtimePress}
|
||||
disabled={!isConnected && !isRealtimeMode}
|
||||
style={[
|
||||
styles.realtimeButton as any,
|
||||
(isRealtimeMode ? styles.realtimeButtonActive : undefined) as any,
|
||||
(!isConnected && !isRealtimeMode
|
||||
? styles.buttonDisabled
|
||||
: undefined) as any,
|
||||
]}
|
||||
>
|
||||
{isRealtimeMode ? (
|
||||
<Square
|
||||
size={18}
|
||||
color={theme.colors.background}
|
||||
fill={theme.colors.background}
|
||||
/>
|
||||
) : (
|
||||
<AudioLines size={20} color={theme.colors.background} />
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const leftContent = <AgentStatusBar agentId={agentId} serverId={serverId} />;
|
||||
@@ -574,7 +565,6 @@ export function AgentInputArea({
|
||||
onPickImages={handlePickImage}
|
||||
onRemoveImage={handleRemoveImage}
|
||||
client={client}
|
||||
sendAgentAudio={sendAgentAudio}
|
||||
placeholder="Message agent..."
|
||||
autoFocus={autoFocus}
|
||||
disabled={isRealtimeMode}
|
||||
@@ -630,10 +620,10 @@ const styles = StyleSheet.create(((theme: Theme) => ({
|
||||
backgroundColor: theme.colors.palette.blue[600],
|
||||
},
|
||||
cancelButton: {
|
||||
width: BUTTON_SIZE,
|
||||
height: BUTTON_SIZE,
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.palette.red[500],
|
||||
backgroundColor: theme.colors.palette.red[600],
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useCallback, useState, type ReactElement } from "react";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import { shortenPath } from "@/utils/shorten-path";
|
||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
@@ -144,7 +145,7 @@ export function AgentList({
|
||||
</View>
|
||||
|
||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
||||
{shortenPath(agent.cwd)} · {timeAgo}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -495,12 +495,6 @@ export function AgentStreamView({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{showWorkingIndicator ? (
|
||||
<View style={stylesheet.workingIndicatorWrapper}>
|
||||
<WorkingIndicator />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{hasHeadItems
|
||||
? [...streamHead].reverse().map((item, index) => {
|
||||
const rendered = renderStreamItemContent(item, index);
|
||||
@@ -511,6 +505,12 @@ export function AgentStreamView({
|
||||
) : null;
|
||||
})
|
||||
: null}
|
||||
|
||||
{showWorkingIndicator ? (
|
||||
<View style={stylesheet.workingIndicatorWrapper}>
|
||||
<WorkingIndicator />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -555,10 +555,12 @@ export function AgentStreamView({
|
||||
}
|
||||
ListHeaderComponent={listHeaderComponent}
|
||||
extraData={flatListExtraData}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
autoscrollToTopThreshold: 40,
|
||||
}}
|
||||
maintainVisibleContentPosition={
|
||||
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
|
||||
agent.status === "running" && isNearBottom
|
||||
? undefined
|
||||
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
|
||||
}
|
||||
initialNumToRender={12}
|
||||
windowSize={10}
|
||||
inverted
|
||||
@@ -777,8 +779,8 @@ function PermissionRequestCard({
|
||||
}, [request]);
|
||||
|
||||
const toolCallDisplay = useMemo(
|
||||
() => parseToolCallDisplay(request.input, null),
|
||||
[request.input]
|
||||
() => parseToolCallDisplay(request.name ?? "unknown", request.input, null),
|
||||
[request.name, request.input]
|
||||
);
|
||||
|
||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||
@@ -792,7 +794,7 @@ function PermissionRequestCard({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -806,7 +808,6 @@ function PermissionRequestCard({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.textgroup]}
|
||||
selectable
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
@@ -821,7 +822,6 @@ function PermissionRequestCard({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_block]}
|
||||
selectable
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
@@ -833,7 +833,7 @@ function PermissionRequestCard({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -847,7 +847,6 @@ function PermissionRequestCard({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_inline]}
|
||||
selectable
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
@@ -893,7 +892,6 @@ function PermissionRequestCard({
|
||||
<Text style={iconStyle}>{bullet}</Text>
|
||||
<Text
|
||||
style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}
|
||||
selectable
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
@@ -117,13 +117,13 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: "rgba(46, 160, 67, 0.15)",
|
||||
},
|
||||
addText: {
|
||||
color: theme.colors.palette.green[800],
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
removeLine: {
|
||||
backgroundColor: "rgba(248, 81, 73, 0.1)",
|
||||
},
|
||||
removeText: {
|
||||
color: theme.colors.palette.red[800],
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
addHighlight: {
|
||||
backgroundColor: "rgba(46, 160, 67, 0.4)",
|
||||
|
||||
@@ -14,14 +14,13 @@ import {
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
type ViewMode,
|
||||
type ExplorerTab,
|
||||
} from "@/stores/panel-store";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
||||
import { GitDiffPane } from "./git-diff-pane";
|
||||
import { FileExplorerPane } from "./file-explorer-pane";
|
||||
|
||||
type ExplorerTab = "changes" | "files";
|
||||
|
||||
interface ExplorerSidebarProps {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
@@ -309,13 +308,11 @@ function SidebarContent({
|
||||
|
||||
{/* Content based on active tab */}
|
||||
<View style={styles.contentArea} testID="explorer-content-area">
|
||||
{activeTab === "changes" ? (
|
||||
{activeTab === "changes" && (
|
||||
<GitDiffPane serverId={serverId} agentId={agentId} />
|
||||
) : (
|
||||
<FileExplorerPane
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
/>
|
||||
)}
|
||||
{activeTab === "files" && (
|
||||
<FileExplorerPane serverId={serverId} agentId={agentId} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -130,7 +130,6 @@ export function FileExplorerPane({
|
||||
return sorted;
|
||||
}, [rawEntries, sortOption]);
|
||||
const showInitialListLoading = isListingLoading && entries.length === 0;
|
||||
const showListLoadingBanner = isListingLoading && entries.length > 0;
|
||||
const isPreviewLoading = Boolean(
|
||||
isExplorerLoading &&
|
||||
pendingRequest?.mode === "file" &&
|
||||
@@ -585,14 +584,9 @@ export function FileExplorerPane({
|
||||
<View style={styles.entryIcon}>
|
||||
{renderEntryIcon(displayKind, theme.colors)}
|
||||
</View>
|
||||
<View style={styles.entryTextContainer}>
|
||||
<Text style={styles.entryName} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.entryMeta} numberOfLines={1}>
|
||||
{formatFileSize({ size: item.size })} · {formatTimeAgo(new Date(item.modifiedAt))}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.entryName} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={(event) => handleOpenMenu(item, event)}
|
||||
@@ -642,17 +636,9 @@ export function FileExplorerPane({
|
||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
</View>
|
||||
{showListLoadingBanner && (
|
||||
<View style={styles.loadingBanner}>
|
||||
<ActivityIndicator size="small" />
|
||||
<Text style={styles.loadingBannerText}>
|
||||
Loading {formatDirectoryLabel(activePath)}...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}, [activePath, currentSortLabel, explorerState?.history?.length, handleNavigateBack, handleSortCycle, showListLoadingBanner, theme.colors.mutedForeground]);
|
||||
}, [activePath, currentSortLabel, explorerState?.history?.length, handleNavigateBack, handleSortCycle]);
|
||||
|
||||
// Watch for completed file previews and process queue
|
||||
useEffect(() => {
|
||||
@@ -789,6 +775,15 @@ export function FileExplorerPane({
|
||||
]}
|
||||
onLayout={handleMenuLayout}
|
||||
>
|
||||
<View style={styles.entryMenuHeader}>
|
||||
<Text style={styles.entryMenuMeta}>
|
||||
{formatFileSize({ size: menuEntry.size })}
|
||||
</Text>
|
||||
<Text style={styles.entryMenuMeta}>
|
||||
{formatTimeAgo(new Date(menuEntry.modifiedAt))}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.entryMenuDivider} />
|
||||
<Pressable
|
||||
style={styles.entryMenuItem}
|
||||
onPress={() => {
|
||||
@@ -866,7 +861,7 @@ export function FileExplorerPane({
|
||||
) : (
|
||||
<View style={styles.sheetCenterState}>
|
||||
<Text style={styles.emptyText}>Binary preview unavailable</Text>
|
||||
<Text style={styles.entryMeta}>
|
||||
<Text style={styles.entryMenuMeta}>
|
||||
{formatFileSize({ size: preview.size })}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -1035,17 +1030,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.lg,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
loadingBanner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
},
|
||||
loadingBannerText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
sortButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
@@ -1114,8 +1098,8 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[1],
|
||||
paddingLeft: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
@@ -1128,24 +1112,16 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
columnGap: theme.spacing[2],
|
||||
marginRight: theme.spacing[3],
|
||||
gap: theme.spacing[2],
|
||||
minWidth: 0,
|
||||
},
|
||||
entryIcon: {
|
||||
width: 28,
|
||||
alignItems: "center",
|
||||
},
|
||||
entryTextContainer: {
|
||||
flex: 1,
|
||||
flexShrink: 0,
|
||||
},
|
||||
entryName: {
|
||||
flex: 1,
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
entryMeta: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
marginTop: theme.spacing[1],
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
menuButton: {
|
||||
width: 36,
|
||||
@@ -1168,6 +1144,22 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: theme.colors.surface2,
|
||||
padding: theme.spacing[1],
|
||||
},
|
||||
entryMenuHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
},
|
||||
entryMenuMeta: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
},
|
||||
entryMenuDivider: {
|
||||
height: 1,
|
||||
backgroundColor: theme.colors.border,
|
||||
marginHorizontal: theme.spacing[2],
|
||||
marginVertical: theme.spacing[1],
|
||||
},
|
||||
entryMenuItem: {
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, UnistylesRuntime } from "react-native-unistyles";
|
||||
import { HEADER_INNER_HEIGHT } from "@/constants/layout";
|
||||
import { useTauriDragHandlers } from "@/utils/tauri-window";
|
||||
|
||||
interface ScreenHeaderProps {
|
||||
left?: ReactNode;
|
||||
@@ -21,10 +22,14 @@ export function ScreenHeader({ left, right, leftStyle, rightStyle }: ScreenHeade
|
||||
// Only add extra padding on mobile for better touch targets; on desktop, only use safe area insets
|
||||
const topPadding = isMobile ? 8 : 0;
|
||||
|
||||
// On Tauri macOS, enable window dragging and double-click to maximize
|
||||
// Left padding for traffic lights is handled by _layout.tsx when sidebar is collapsed
|
||||
const dragHandlers = useTauriDragHandlers();
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.inner, { paddingTop: insets.top + topPadding }]}>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.row} {...dragHandlers}>
|
||||
<View style={[styles.left, leftStyle]}>{left}</View>
|
||||
<View style={[styles.right, rightStyle]}>{right}</View>
|
||||
</View>
|
||||
@@ -46,6 +51,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
borderBottomWidth: theme.borderWidth[1],
|
||||
borderBottomColor: theme.colors.border,
|
||||
userSelect: "none",
|
||||
},
|
||||
left: {
|
||||
flex: 1,
|
||||
|
||||
@@ -27,7 +27,6 @@ import Animated, {
|
||||
import { useDictation } from "@/hooks/use-dictation";
|
||||
import { DictationOverlay } from "./dictation-controls";
|
||||
import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
|
||||
import type { SessionContextValue } from "@/contexts/session-context";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
|
||||
export interface ImageAttachment {
|
||||
@@ -52,7 +51,6 @@ export interface MessageInputProps {
|
||||
onPickImages?: () => void;
|
||||
onRemoveImage?: (index: number) => void;
|
||||
client: DaemonClientV2 | null;
|
||||
sendAgentAudio: SessionContextValue["sendAgentAudio"];
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
@@ -104,7 +102,6 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
onPickImages,
|
||||
onRemoveImage,
|
||||
client,
|
||||
sendAgentAudio,
|
||||
placeholder = "Message...",
|
||||
autoFocus = false,
|
||||
disabled = false,
|
||||
@@ -200,9 +197,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
retryFailedDictation,
|
||||
discardFailedDictation,
|
||||
} = useDictation({
|
||||
sendAgentAudio,
|
||||
client,
|
||||
mode: "transcribe_only",
|
||||
onTranscript: handleDictationTranscript,
|
||||
onError: handleDictationError,
|
||||
canStart: canStartDictation,
|
||||
@@ -538,8 +533,23 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
{leftContent}
|
||||
</View>
|
||||
|
||||
{/* Right: send/queue button, voice button, rightContent slot */}
|
||||
{/* Right: voice button, contextual button (realtime/send/cancel) */}
|
||||
<View style={styles.rightButtonGroup}>
|
||||
<Pressable
|
||||
onPress={handleVoicePress}
|
||||
disabled={!isConnected || disabled}
|
||||
style={[
|
||||
styles.voiceButton,
|
||||
(!isConnected || disabled) && styles.buttonDisabled,
|
||||
isDictating && styles.voiceButtonRecording,
|
||||
]}
|
||||
>
|
||||
{isDictating ? (
|
||||
<Square size={14} color="white" fill="white" />
|
||||
) : (
|
||||
<Mic size={20} color={theme.colors.foreground} />
|
||||
)}
|
||||
</Pressable>
|
||||
{shouldShowSendButton && (
|
||||
<Pressable
|
||||
onPress={handleSendMessage}
|
||||
@@ -561,22 +571,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
<ArrowUp size={20} color="white" />
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={handleVoicePress}
|
||||
disabled={!isConnected || disabled}
|
||||
style={[
|
||||
styles.voiceButton,
|
||||
(!isConnected || disabled) && styles.buttonDisabled,
|
||||
isDictating && styles.voiceButtonRecording,
|
||||
]}
|
||||
>
|
||||
{isDictating ? (
|
||||
<Square size={14} color="white" fill="white" />
|
||||
) : (
|
||||
<Mic size={20} color={theme.colors.foreground} />
|
||||
)}
|
||||
</Pressable>
|
||||
{rightContent}
|
||||
{!shouldShowSendButton && rightContent}
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
@@ -387,16 +387,21 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
flexShrink: 0,
|
||||
},
|
||||
detailWrapper: {
|
||||
marginTop: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderBottomLeftRadius: theme.borderRadius.lg,
|
||||
borderBottomRightRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderTopWidth: 0,
|
||||
borderColor: theme.colors.borderAccent,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
padding: theme.spacing[2],
|
||||
gap: theme.spacing[2],
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
pressableExpanded: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
function isLikelyPathToken(value: string): boolean {
|
||||
@@ -511,7 +516,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -525,7 +530,6 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.textgroup]}
|
||||
selectable
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
@@ -540,7 +544,6 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_block]}
|
||||
selectable
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
@@ -552,7 +555,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -576,7 +579,6 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
inheritedStyles,
|
||||
assistantMessageStylesheet.markdownCodeInline,
|
||||
]}
|
||||
selectable
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
@@ -1153,6 +1155,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
pressed && hasDetails
|
||||
? expandableBadgeStylesheet.pressablePressed
|
||||
: null,
|
||||
isExpanded && expandableBadgeStylesheet.pressableExpanded,
|
||||
]}
|
||||
>
|
||||
<View style={expandableBadgeStylesheet.headerRow}>
|
||||
@@ -1181,12 +1184,12 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{detailContent ? (
|
||||
<View style={expandableBadgeStylesheet.detailWrapper}>
|
||||
{detailContent}
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
{detailContent ? (
|
||||
<View style={expandableBadgeStylesheet.detailWrapper}>
|
||||
{detailContent}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -1226,7 +1229,7 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -1240,7 +1243,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.textgroup]}
|
||||
selectable
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
@@ -1255,7 +1257,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_block]}
|
||||
selectable
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
@@ -1267,7 +1268,7 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
styles: any,
|
||||
inheritedStyles: any = {}
|
||||
) => (
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
|
||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||
{node.content}
|
||||
</Text>
|
||||
),
|
||||
@@ -1281,7 +1282,6 @@ export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||
<Text
|
||||
key={node.key}
|
||||
style={[inheritedStyles, styles.code_inline]}
|
||||
selectable
|
||||
>
|
||||
{node.content}
|
||||
</Text>
|
||||
@@ -1432,7 +1432,7 @@ export const ToolCall = memo(function ToolCall({
|
||||
args !== undefined || result !== undefined || error !== undefined;
|
||||
|
||||
// Parse tool call details for inline rendering
|
||||
const { display, errorText } = useToolCallDetails({ args, result, error });
|
||||
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (!isMobile && isPerfLoggingEnabled()) {
|
||||
@@ -1498,7 +1498,7 @@ export const ToolCall = memo(function ToolCall({
|
||||
return (
|
||||
<ExpandableBadge
|
||||
testID="tool-call-badge"
|
||||
label={toolName}
|
||||
label={display.toolName}
|
||||
secondaryLabel={principalParam}
|
||||
icon={IconComponent}
|
||||
isExpanded={!isMobile && isExpanded}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { usePanelStore } from "@/stores/panel-store";
|
||||
import { AgentList } from "./agent-list";
|
||||
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
|
||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||
import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-window";
|
||||
|
||||
const DESKTOP_SIDEBAR_WIDTH = 320;
|
||||
const SIDEBAR_AGENT_LIMIT = 15;
|
||||
@@ -44,6 +45,8 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
animateToClose,
|
||||
isGesturing,
|
||||
} = useSidebarAnimation();
|
||||
const trafficLightPadding = useTrafficLightPadding();
|
||||
const dragHandlers = useTauriDragHandlers();
|
||||
|
||||
// Track user-initiated refresh to avoid showing spinner on background revalidation
|
||||
const [isManualRefresh, setIsManualRefresh] = useState(false);
|
||||
@@ -249,8 +252,11 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
|
||||
return (
|
||||
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
||||
{/* Header: New Agent button */}
|
||||
<View style={styles.sidebarHeader}>
|
||||
{/* Header: New Agent button - top padding area is draggable on Tauri */}
|
||||
<View
|
||||
style={[styles.sidebarHeader, { paddingTop: trafficLightPadding.top || styles.sidebarHeader.paddingTop }]}
|
||||
{...dragHandlers}
|
||||
>
|
||||
<Pressable
|
||||
style={({ hovered }) => [
|
||||
styles.newAgentButton,
|
||||
@@ -316,6 +322,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingTop: theme.spacing[4],
|
||||
paddingBottom: theme.spacing[2],
|
||||
userSelect: "none",
|
||||
},
|
||||
newAgentButton: {
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import {
|
||||
parseToolCallDisplay,
|
||||
buildLineDiff,
|
||||
parseUnifiedDiff,
|
||||
type ToolCallDisplay,
|
||||
} from "@/utils/tool-call-parsers";
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
@@ -14,6 +15,7 @@ import { DiffViewer } from "./diff-viewer";
|
||||
// ---- Types ----
|
||||
|
||||
export interface ToolCallDetailsData {
|
||||
toolName: string;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
@@ -139,6 +141,10 @@ export function ToolCallDetailsContent({
|
||||
// Compute diff lines for edit type
|
||||
const diffLines = useMemo(() => {
|
||||
if (display.type !== "edit") return undefined;
|
||||
// Use pre-computed unified diff if available (e.g., from apply_patch)
|
||||
if (display.unifiedDiff) {
|
||||
return parseUnifiedDiff(display.unifiedDiff);
|
||||
}
|
||||
return buildLineDiff(display.oldString, display.newString);
|
||||
}, [display]);
|
||||
|
||||
@@ -304,12 +310,12 @@ export function ToolCallDetailsContent({
|
||||
// ---- Hook for parsing tool call data ----
|
||||
|
||||
export function useToolCallDetails(data: ToolCallDetailsData) {
|
||||
const { args, result, error } = data;
|
||||
const { toolName, args, result, error } = data;
|
||||
|
||||
return useMemo(() => {
|
||||
const shouldLog = isPerfLoggingEnabled();
|
||||
const startMs = shouldLog ? getNowMs() : 0;
|
||||
const display = parseToolCallDisplay(args, result);
|
||||
const display = parseToolCallDisplay(toolName, args, result);
|
||||
const errorText = error !== undefined ? formatValue(error) : undefined;
|
||||
if (shouldLog) {
|
||||
const durationMs = getNowMs() - startMs;
|
||||
@@ -329,7 +335,7 @@ export function useToolCallDetails(data: ToolCallDetailsData) {
|
||||
}
|
||||
}
|
||||
return { display, errorText };
|
||||
}, [args, result, error]);
|
||||
}, [toolName, args, result, error]);
|
||||
}
|
||||
|
||||
// ---- Styles ----
|
||||
|
||||
@@ -144,7 +144,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
? toolKindIcons[kind.toLowerCase()] || Wrench
|
||||
: Wrench;
|
||||
|
||||
const { display, errorText } = useToolCallDetails({ args, result, error });
|
||||
const { display, errorText } = useToolCallDetails({ toolName, args, result, error });
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -153,7 +153,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
<View style={styles.headerLeft}>
|
||||
<IconComponent size={20} color={styles.headerIcon.color} />
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{toolName}
|
||||
{display.toolName}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
|
||||
Reference in New Issue
Block a user