mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
checkpoint
This commit is contained in:
Generated
+2
@@ -4302,6 +4302,7 @@ dependencies = [
|
||||
"clap_complete",
|
||||
"cliclack",
|
||||
"console 0.16.2",
|
||||
"dirs 6.0.0",
|
||||
"dotenvy",
|
||||
"etcetera 0.11.0",
|
||||
"futures",
|
||||
@@ -4314,6 +4315,7 @@ dependencies = [
|
||||
"open",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"rmcp 0.14.0",
|
||||
"rustyline",
|
||||
"serde",
|
||||
|
||||
@@ -56,6 +56,8 @@ anstream = "0.6.18"
|
||||
open = "5.3.2"
|
||||
urlencoding = "2.1"
|
||||
clap_complete = "4.5.62"
|
||||
dirs = "6.0.0"
|
||||
reqwest.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["wincred"] }
|
||||
|
||||
Generated
+3150
-40
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
"start-alpha-gui": "ALPHA=true npm run start-gui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-coding-agent": "^0.51.6",
|
||||
"@mcp-ui/client": "^5.17.3",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ChatType } from '../types/chat';
|
||||
import { useIsMobile } from '../hooks/use-mobile';
|
||||
import { useSidebar } from './ui/sidebar';
|
||||
import { cn } from '../utils';
|
||||
import { useChatStream } from '../hooks/useChatStream';
|
||||
import { useAgentChat, type AgentBackend } from '../hooks/useAgentChat';
|
||||
import { useNavigation } from '../hooks/useNavigation';
|
||||
import { RecipeHeader } from './RecipeHeader';
|
||||
import { RecipeWarningModal } from './ui/RecipeWarningModal';
|
||||
@@ -78,6 +78,16 @@ export default function BaseChat({
|
||||
const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false);
|
||||
const [hasNotAcceptedRecipe, setHasNotAcceptedRecipe] = useState<boolean>();
|
||||
const [hasRecipeSecurityWarnings, setHasRecipeSecurityWarnings] = useState(false);
|
||||
const [agentBackend, setAgentBackend] = useState<AgentBackend>('goose');
|
||||
|
||||
// Load agent backend preference from settings
|
||||
useEffect(() => {
|
||||
window.electron.getSettings().then((settings) => {
|
||||
if (settings.agentBackend) {
|
||||
setAgentBackend(settings.agentBackend);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const { state: sidebarState } = useSidebar();
|
||||
@@ -106,8 +116,9 @@ export default function BaseChat({
|
||||
tokenState,
|
||||
notifications: toolCallNotifications,
|
||||
onMessageUpdate,
|
||||
} = useChatStream({
|
||||
} = useAgentChat({
|
||||
sessionId,
|
||||
backend: agentBackend,
|
||||
onStreamFinish,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import BlockLogoBlack from './icons/block-lockup_black.png';
|
||||
import BlockLogoWhite from './icons/block-lockup_white.png';
|
||||
import TelemetrySettings from './TelemetrySettings';
|
||||
import { trackSettingToggled } from '../../../utils/analytics';
|
||||
import type { AgentBackend } from '../../../utils/settings';
|
||||
|
||||
interface AppSettingsSectionProps {
|
||||
scrollToSection?: string;
|
||||
@@ -27,6 +28,8 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
const [showNotificationModal, setShowNotificationModal] = useState(false);
|
||||
const [showPricing, setShowPricing] = useState(true);
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
const [isPiAvailable, setIsPiAvailable] = useState(false);
|
||||
const [agentBackend, setAgentBackend] = useState<AgentBackend>('goose');
|
||||
const updateSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Check if GOOSE_VERSION is set to determine if Updates section should be shown
|
||||
@@ -62,6 +65,14 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
setShowPricing(stored !== 'false');
|
||||
}, []);
|
||||
|
||||
// Check Pi availability and load agent backend setting
|
||||
useEffect(() => {
|
||||
window.electron.pi.isAvailable().then(setIsPiAvailable);
|
||||
window.electron.getSettings().then((settings) => {
|
||||
setAgentBackend(settings.agentBackend || 'goose');
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle scrolling to update section
|
||||
useEffect(() => {
|
||||
if (scrollToSection === 'update' && updateSectionRef.current) {
|
||||
@@ -148,6 +159,17 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
window.dispatchEvent(new CustomEvent('storage'));
|
||||
};
|
||||
|
||||
const handleAgentBackendToggle = async (usePi: boolean) => {
|
||||
const newBackend: AgentBackend = usePi ? 'pi' : 'goose';
|
||||
const settings = await window.electron.getSettings();
|
||||
settings.agentBackend = newBackend;
|
||||
const success = await window.electron.saveSettings(settings);
|
||||
if (success) {
|
||||
setAgentBackend(newBackend);
|
||||
trackSettingToggled('agent_backend_pi', usePi);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pr-4 pb-8 mt-1">
|
||||
<Card className="rounded-lg">
|
||||
@@ -271,6 +293,41 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
|
||||
<TunnelSection />
|
||||
|
||||
{/* Experimental: Pi Agent Backend */}
|
||||
<Card className="rounded-lg border-amber-500/50">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="mb-1 flex items-center gap-2">
|
||||
<span className="text-amber-500 text-xs font-normal px-2 py-0.5 bg-amber-500/10 rounded">
|
||||
Experimental
|
||||
</span>
|
||||
Agent Backend
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Switch between the default Goose agent and the Pi coding agent
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-text-default text-xs">Use Pi Agent</h3>
|
||||
<p className="text-xs text-text-muted max-w-md mt-[2px]">
|
||||
{isPiAvailable
|
||||
? 'Pi is an alternative coding agent that runs natively in the app. New sessions will use the selected agent.'
|
||||
: 'Pi agent is not available. Check logs for loading errors.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Switch
|
||||
checked={agentBackend === 'pi'}
|
||||
onCheckedChange={handleAgentBackendToggle}
|
||||
disabled={!isPiAvailable}
|
||||
variant="mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<TelemetrySettings isWelcome={false} />
|
||||
|
||||
<Card className="rounded-lg">
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Unified agent chat hook that can switch between Goose and Pi backends.
|
||||
*
|
||||
* This hook provides a single interface for chat functionality,
|
||||
* delegating to either useChatStream (Goose/goosed) or usePiChat (Pi)
|
||||
* based on configuration.
|
||||
*
|
||||
* Usage:
|
||||
* const chat = useAgentChat({ sessionId, backend: 'goose' }); // or 'pi'
|
||||
*/
|
||||
|
||||
import { useChatStream } from './useChatStream';
|
||||
import { usePiChat } from './usePiChat';
|
||||
import { Message, Session, TokenState } from '../api';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { NotificationEvent, UserInput } from '../types/message';
|
||||
|
||||
export type AgentBackend = 'goose' | 'pi';
|
||||
|
||||
interface UseAgentChatProps {
|
||||
sessionId: string;
|
||||
backend?: AgentBackend;
|
||||
onStreamFinish: () => void;
|
||||
onSessionLoaded?: () => void;
|
||||
}
|
||||
|
||||
interface UseAgentChatReturn {
|
||||
session?: Session;
|
||||
messages: Message[];
|
||||
chatState: ChatState;
|
||||
setChatState: (state: ChatState) => void;
|
||||
handleSubmit: (input: UserInput) => Promise<void>;
|
||||
submitElicitationResponse: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
setRecipeUserParams: (values: Record<string, string>) => Promise<void>;
|
||||
stopStreaming: () => void;
|
||||
sessionLoadError?: string;
|
||||
tokenState: TokenState;
|
||||
notifications: Map<string, NotificationEvent[]>;
|
||||
onMessageUpdate: (
|
||||
messageId: string,
|
||||
newContent: string,
|
||||
editType?: 'fork' | 'edit'
|
||||
) => Promise<void>;
|
||||
backend: AgentBackend;
|
||||
}
|
||||
|
||||
export function useAgentChat({
|
||||
sessionId,
|
||||
backend = 'goose',
|
||||
onStreamFinish,
|
||||
onSessionLoaded,
|
||||
}: UseAgentChatProps): UseAgentChatReturn {
|
||||
// Use the appropriate hook based on backend selection
|
||||
const gooseChat = useChatStream({
|
||||
sessionId,
|
||||
onStreamFinish,
|
||||
onSessionLoaded,
|
||||
});
|
||||
|
||||
const piChat = usePiChat({
|
||||
sessionId,
|
||||
onStreamFinish,
|
||||
onSessionLoaded,
|
||||
});
|
||||
|
||||
if (backend === 'pi') {
|
||||
return {
|
||||
session: piChat.session,
|
||||
messages: piChat.messages,
|
||||
chatState: piChat.chatState,
|
||||
setChatState: piChat.setChatState,
|
||||
handleSubmit: piChat.handleSubmit,
|
||||
submitElicitationResponse: piChat.submitElicitationResponse,
|
||||
setRecipeUserParams: piChat.setRecipeUserParams,
|
||||
stopStreaming: piChat.stopStreaming,
|
||||
sessionLoadError: piChat.sessionLoadError,
|
||||
tokenState: piChat.tokenState,
|
||||
notifications: piChat.notifications,
|
||||
onMessageUpdate: piChat.onMessageUpdate,
|
||||
backend: 'pi',
|
||||
};
|
||||
}
|
||||
|
||||
// Default to Goose backend
|
||||
return {
|
||||
session: gooseChat.session,
|
||||
messages: gooseChat.messages,
|
||||
chatState: gooseChat.chatState,
|
||||
setChatState: gooseChat.setChatState,
|
||||
handleSubmit: gooseChat.handleSubmit,
|
||||
submitElicitationResponse: gooseChat.submitElicitationResponse,
|
||||
setRecipeUserParams: gooseChat.setRecipeUserParams,
|
||||
stopStreaming: gooseChat.stopStreaming,
|
||||
sessionLoadError: gooseChat.sessionLoadError,
|
||||
tokenState: gooseChat.tokenState,
|
||||
notifications: gooseChat.notifications,
|
||||
onMessageUpdate: gooseChat.onMessageUpdate,
|
||||
backend: 'goose',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* React hook for Pi agent integration.
|
||||
*
|
||||
* This hook provides the same interface as useChatStream but uses Pi
|
||||
* running in the Electron main process instead of goosed.
|
||||
*
|
||||
* Architecture:
|
||||
* - Renderer calls window.electron.pi.* IPC methods
|
||||
* - Main process manages Pi session and streams events back
|
||||
* - Events are translated to Goose-compatible Message format
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { Message, Session, TokenState } from '../api';
|
||||
import { createUserMessage, NotificationEvent, UserInput } from '../types/message';
|
||||
import { errorMessage } from '../utils/conversionUtils';
|
||||
|
||||
// Debug logging to file
|
||||
const piLog = (msg: string, ...args: unknown[]) => {
|
||||
const line = `[${new Date().toISOString()}] ${msg} ${args.map(a => JSON.stringify(a)).join(' ')}\n`;
|
||||
console.log('[usePiChat]', msg, ...args);
|
||||
// Write to localStorage for debugging (renderer can't write files directly)
|
||||
const existing = localStorage.getItem('pi-debug-log') || '';
|
||||
localStorage.setItem('pi-debug-log', existing + line);
|
||||
};
|
||||
|
||||
interface UsePiChatProps {
|
||||
sessionId: string;
|
||||
onStreamFinish: () => void;
|
||||
onSessionLoaded?: () => void;
|
||||
}
|
||||
|
||||
interface UsePiChatReturn {
|
||||
session?: Session;
|
||||
messages: Message[];
|
||||
chatState: ChatState;
|
||||
setChatState: (state: ChatState) => void;
|
||||
handleSubmit: (input: UserInput) => Promise<void>;
|
||||
submitElicitationResponse: (
|
||||
elicitationId: string,
|
||||
userData: Record<string, unknown>
|
||||
) => Promise<void>;
|
||||
setRecipeUserParams: (values: Record<string, string>) => Promise<void>;
|
||||
stopStreaming: () => void;
|
||||
sessionLoadError?: string;
|
||||
tokenState: TokenState;
|
||||
notifications: Map<string, NotificationEvent[]>;
|
||||
onMessageUpdate: (
|
||||
messageId: string,
|
||||
newContent: string,
|
||||
editType?: 'fork' | 'edit'
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
interface PiChatState {
|
||||
messages: Message[];
|
||||
session: Session | undefined;
|
||||
chatState: ChatState;
|
||||
sessionLoadError: string | undefined;
|
||||
tokenState: TokenState;
|
||||
isPiAvailable: boolean;
|
||||
}
|
||||
|
||||
type PiChatAction =
|
||||
| { type: 'SET_MESSAGES'; payload: Message[] }
|
||||
| { type: 'SET_SESSION'; payload: Session | undefined }
|
||||
| { type: 'SET_CHAT_STATE'; payload: ChatState }
|
||||
| { type: 'SET_SESSION_LOAD_ERROR'; payload: string | undefined }
|
||||
| { type: 'SET_TOKEN_STATE'; payload: TokenState }
|
||||
| { type: 'SET_PI_AVAILABLE'; payload: boolean }
|
||||
| { type: 'ADD_MESSAGE'; payload: Message }
|
||||
| { type: 'UPDATE_LAST_MESSAGE'; payload: Message }
|
||||
| { type: 'START_STREAMING' }
|
||||
| { type: 'STREAM_FINISH'; payload?: string }
|
||||
| { type: 'SESSION_STARTED' };
|
||||
|
||||
const initialTokenState: TokenState = {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalTokens: 0,
|
||||
accumulatedInputTokens: 0,
|
||||
accumulatedOutputTokens: 0,
|
||||
accumulatedTotalTokens: 0,
|
||||
};
|
||||
|
||||
const initialState: PiChatState = {
|
||||
messages: [],
|
||||
session: undefined,
|
||||
chatState: ChatState.Idle,
|
||||
sessionLoadError: undefined,
|
||||
tokenState: initialTokenState,
|
||||
isPiAvailable: false,
|
||||
};
|
||||
|
||||
function piChatReducer(state: PiChatState, action: PiChatAction): PiChatState {
|
||||
switch (action.type) {
|
||||
case 'SET_MESSAGES':
|
||||
return { ...state, messages: action.payload };
|
||||
|
||||
case 'SET_SESSION':
|
||||
return { ...state, session: action.payload };
|
||||
|
||||
case 'SET_CHAT_STATE':
|
||||
return { ...state, chatState: action.payload };
|
||||
|
||||
case 'SET_SESSION_LOAD_ERROR':
|
||||
return { ...state, sessionLoadError: action.payload };
|
||||
|
||||
case 'SET_TOKEN_STATE':
|
||||
return { ...state, tokenState: action.payload };
|
||||
|
||||
case 'SET_PI_AVAILABLE':
|
||||
return { ...state, isPiAvailable: action.payload };
|
||||
|
||||
case 'ADD_MESSAGE':
|
||||
return { ...state, messages: [...state.messages, action.payload] };
|
||||
|
||||
case 'UPDATE_LAST_MESSAGE': {
|
||||
const messages = [...state.messages];
|
||||
if (messages.length > 0 && messages[messages.length - 1].role === action.payload.role) {
|
||||
messages[messages.length - 1] = action.payload;
|
||||
} else {
|
||||
messages.push(action.payload);
|
||||
}
|
||||
return { ...state, messages };
|
||||
}
|
||||
|
||||
case 'START_STREAMING':
|
||||
return { ...state, chatState: ChatState.Streaming };
|
||||
|
||||
case 'STREAM_FINISH':
|
||||
return {
|
||||
...state,
|
||||
chatState: ChatState.Idle,
|
||||
sessionLoadError: action.payload,
|
||||
};
|
||||
|
||||
case 'SESSION_STARTED':
|
||||
return {
|
||||
...state,
|
||||
chatState: ChatState.Idle,
|
||||
session: {
|
||||
id: `pi-${Date.now()}`,
|
||||
name: 'Pi Session',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
} as Session,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function usePiChat({
|
||||
sessionId: _sessionId,
|
||||
onStreamFinish,
|
||||
onSessionLoaded,
|
||||
}: UsePiChatProps): UsePiChatReturn {
|
||||
const [state, dispatch] = useReducer(piChatReducer, initialState);
|
||||
const stateRef = useRef(state);
|
||||
stateRef.current = state;
|
||||
const sessionStartedRef = useRef(false);
|
||||
|
||||
// Get working directory from app config
|
||||
const workingDir = (window.appConfig?.get('GOOSE_WORKING_DIR') as string) || process.cwd();
|
||||
|
||||
// Check Pi availability and auto-start session on mount
|
||||
useEffect(() => {
|
||||
if (sessionStartedRef.current) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('[usePiChat] Checking Pi availability...');
|
||||
const available = await window.electron.pi.isAvailable();
|
||||
console.log('[usePiChat] Pi available:', available);
|
||||
dispatch({ type: 'SET_PI_AVAILABLE', payload: available });
|
||||
|
||||
if (available && !sessionStartedRef.current) {
|
||||
sessionStartedRef.current = true;
|
||||
dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.LoadingConversation });
|
||||
|
||||
console.log('[usePiChat] Starting Pi session with workingDir:', workingDir);
|
||||
const result = await window.electron.pi.startSession({ workingDir });
|
||||
console.log('[usePiChat] Session start result:', result);
|
||||
if (result.success) {
|
||||
dispatch({ type: 'SESSION_STARTED' });
|
||||
onSessionLoaded?.();
|
||||
} else {
|
||||
dispatch({ type: 'SET_SESSION_LOAD_ERROR', payload: 'Failed to start Pi session' });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[usePiChat] Error:', err);
|
||||
dispatch({ type: 'SET_PI_AVAILABLE', payload: false });
|
||||
dispatch({ type: 'SET_SESSION_LOAD_ERROR', payload: errorMessage(err) });
|
||||
}
|
||||
})();
|
||||
}, [workingDir, onSessionLoaded]);
|
||||
|
||||
// Listen for Pi events from main process
|
||||
useEffect(() => {
|
||||
const handlePiEvent = (_event: { type: string }) => {
|
||||
// Raw Pi events are for debugging - we use translated pi:message events
|
||||
};
|
||||
|
||||
const handlePiMessage = (gooseMessage: Message) => {
|
||||
// Handle translated Goose messages (these have proper metadata)
|
||||
if (gooseMessage && gooseMessage.metadata) {
|
||||
dispatch({ type: 'UPDATE_LAST_MESSAGE', payload: gooseMessage });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePiComplete = () => {
|
||||
dispatch({ type: 'STREAM_FINISH' });
|
||||
onStreamFinish();
|
||||
};
|
||||
|
||||
const handlePiError = (data: { error: string }) => {
|
||||
dispatch({ type: 'STREAM_FINISH', payload: data.error });
|
||||
onStreamFinish();
|
||||
};
|
||||
|
||||
// Register listeners via IPC
|
||||
window.electron.on('pi:event', (_e, data) => handlePiEvent(data as { type: string; message?: Message }));
|
||||
window.electron.on('pi:message', (_e, data) => handlePiMessage(data as Message));
|
||||
window.electron.on('pi:complete', handlePiComplete);
|
||||
window.electron.on('pi:error', (_e, data) => handlePiError(data as { error: string }));
|
||||
|
||||
return () => {
|
||||
window.electron.off('pi:event', handlePiEvent as () => void);
|
||||
window.electron.off('pi:message', handlePiMessage as () => void);
|
||||
window.electron.off('pi:complete', handlePiComplete);
|
||||
window.electron.off('pi:error', handlePiError as () => void);
|
||||
};
|
||||
}, [onStreamFinish]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (input: UserInput) => {
|
||||
console.log('[usePiChat] handleSubmit called with:', input.msg);
|
||||
const userMessage = createUserMessage(input.msg, input.images);
|
||||
dispatch({ type: 'ADD_MESSAGE', payload: userMessage });
|
||||
dispatch({ type: 'START_STREAMING' });
|
||||
|
||||
try {
|
||||
console.log('[usePiChat] Calling pi.prompt...');
|
||||
const result = await window.electron.pi.prompt(input.msg);
|
||||
console.log('[usePiChat] prompt result:', result);
|
||||
|
||||
if (!result.success && result.error) {
|
||||
dispatch({ type: 'STREAM_FINISH', payload: result.error });
|
||||
onStreamFinish();
|
||||
}
|
||||
// Success case is handled by IPC event listeners
|
||||
} catch (error) {
|
||||
console.error('[usePiChat] prompt error:', error);
|
||||
dispatch({ type: 'STREAM_FINISH', payload: errorMessage(error) });
|
||||
onStreamFinish();
|
||||
}
|
||||
},
|
||||
[onStreamFinish]
|
||||
);
|
||||
|
||||
const submitElicitationResponse = useCallback(
|
||||
async (_elicitationId: string, _userData: Record<string, unknown>) => {
|
||||
// Pi doesn't support elicitation yet - no-op
|
||||
console.warn('[usePiChat] Elicitation not supported by Pi agent');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const setRecipeUserParams = useCallback(async (_values: Record<string, string>) => {
|
||||
// Pi doesn't support recipe params yet - no-op
|
||||
console.warn('[usePiChat] Recipe params not supported by Pi agent');
|
||||
}, []);
|
||||
|
||||
const stopStreaming = useCallback(async () => {
|
||||
try {
|
||||
await window.electron.pi.stopSession();
|
||||
} catch {
|
||||
// Ignore abort errors
|
||||
}
|
||||
dispatch({ type: 'SET_CHAT_STATE', payload: ChatState.Idle });
|
||||
}, []);
|
||||
|
||||
const setChatState = useCallback((newState: ChatState) => {
|
||||
dispatch({ type: 'SET_CHAT_STATE', payload: newState });
|
||||
}, []);
|
||||
|
||||
const onMessageUpdate = useCallback(
|
||||
async (_messageId: string, _newContent: string, _editType?: 'fork' | 'edit') => {
|
||||
// Pi doesn't support message editing yet - no-op
|
||||
console.warn('[usePiChat] Message editing not supported by Pi agent');
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
session: state.session,
|
||||
messages: state.messages,
|
||||
chatState: state.chatState,
|
||||
setChatState,
|
||||
handleSubmit,
|
||||
submitElicitationResponse,
|
||||
setRecipeUserParams,
|
||||
stopStreaming,
|
||||
sessionLoadError: state.sessionLoadError,
|
||||
tokenState: state.tokenState,
|
||||
notifications: new Map(),
|
||||
onMessageUpdate,
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import os from 'node:os';
|
||||
import { spawn } from 'child_process';
|
||||
import 'dotenv/config';
|
||||
import { checkServerStatus, startGoosed } from './goosed';
|
||||
import { initializePi, registerPiIpcHandlers } from './pi/index';
|
||||
import { expandTilde } from './utils/pathUtils';
|
||||
import log from './utils/logger';
|
||||
import { ensureWinShims } from './utils/winShims';
|
||||
@@ -1649,6 +1650,16 @@ async function appMain() {
|
||||
|
||||
registerUpdateIpcHandlers();
|
||||
|
||||
// Initialize Pi agent (optional - loads if available)
|
||||
// Always register IPC handlers so renderer can check availability
|
||||
registerPiIpcHandlers();
|
||||
const piAvailable = await initializePi();
|
||||
if (piAvailable) {
|
||||
log.info('[Main] Pi agent initialized');
|
||||
} else {
|
||||
log.info('[Main] Pi agent not available (optional dependency)');
|
||||
}
|
||||
|
||||
// Handle microphone permission requests
|
||||
session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => {
|
||||
console.log('Permission requested:', permission);
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Translates Pi agent events to Goose-compatible message format.
|
||||
*
|
||||
* This allows the existing UI components to work with Pi as the backend,
|
||||
* maintaining a consistent user experience.
|
||||
*/
|
||||
|
||||
import type { Message, MessageContent, Role } from '../api';
|
||||
|
||||
// Pi event types (from @mariozechner/pi-agent-core)
|
||||
export interface PiAgentEvent {
|
||||
type: string;
|
||||
message?: PiAgentMessage;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PiMessageContent {
|
||||
type: 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'image';
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface PiAgentMessage {
|
||||
id: string;
|
||||
role: 'user' | 'assistant' | 'tool';
|
||||
content: PiMessageContent[];
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// Re-export Goose Message type for convenience
|
||||
export type GooseMessage = Message;
|
||||
|
||||
/**
|
||||
* Translates a Pi message to Goose message format.
|
||||
*/
|
||||
export function translatePiMessage(piMessage: PiAgentMessage): GooseMessage {
|
||||
const gooseContent: MessageContent[] = [];
|
||||
|
||||
for (const content of piMessage.content) {
|
||||
switch (content.type) {
|
||||
case 'text':
|
||||
gooseContent.push({
|
||||
type: 'text',
|
||||
text: content.text || '',
|
||||
});
|
||||
break;
|
||||
|
||||
case 'thinking':
|
||||
gooseContent.push({
|
||||
type: 'thinking',
|
||||
thinking: content.thinking || content.text || '',
|
||||
signature: '', // Pi doesn't use Anthropic-style thinking signatures
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tool_call':
|
||||
gooseContent.push({
|
||||
type: 'toolRequest',
|
||||
id: content.toolCallId || generateId(),
|
||||
toolCall: {
|
||||
name: content.toolName || 'unknown',
|
||||
arguments: content.args || {},
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case 'tool_result':
|
||||
gooseContent.push({
|
||||
type: 'toolResponse',
|
||||
id: content.toolCallId || generateId(),
|
||||
toolResult: {
|
||||
result: content.result,
|
||||
isError: content.isError || false,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const role: Role = piMessage.role === 'tool' ? 'assistant' : piMessage.role;
|
||||
const created = piMessage.createdAt
|
||||
? Math.floor(new Date(piMessage.createdAt).getTime() / 1000)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
return {
|
||||
id: piMessage.id,
|
||||
role,
|
||||
created,
|
||||
content: gooseContent,
|
||||
metadata: {
|
||||
userVisible: true,
|
||||
agentVisible: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return Math.random().toString(36).substring(2, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* State machine for accumulating Pi events into Goose messages.
|
||||
*/
|
||||
export class PiEventAccumulator {
|
||||
private currentMessage: PiAgentMessage | null = null;
|
||||
private messages: GooseMessage[] = [];
|
||||
private isStreaming = false;
|
||||
|
||||
/**
|
||||
* Process a Pi event and return any completed Goose messages.
|
||||
*/
|
||||
processEvent(event: PiAgentEvent): {
|
||||
message?: GooseMessage;
|
||||
isComplete: boolean;
|
||||
isStreaming: boolean;
|
||||
} {
|
||||
switch (event.type) {
|
||||
case 'agent_start':
|
||||
this.isStreaming = true;
|
||||
this.messages = [];
|
||||
return { isComplete: false, isStreaming: true };
|
||||
|
||||
case 'agent_end':
|
||||
this.isStreaming = false;
|
||||
return { isComplete: true, isStreaming: false };
|
||||
|
||||
case 'message_start': {
|
||||
const msg = event.message as PiAgentMessage | undefined;
|
||||
if (!msg) return { isComplete: false, isStreaming: true };
|
||||
this.currentMessage = msg;
|
||||
return {
|
||||
message: translatePiMessage(msg),
|
||||
isComplete: false,
|
||||
isStreaming: true,
|
||||
};
|
||||
}
|
||||
|
||||
case 'message_update': {
|
||||
const msg = event.message as PiAgentMessage | undefined;
|
||||
if (!msg) return { isComplete: false, isStreaming: true };
|
||||
this.currentMessage = msg;
|
||||
return {
|
||||
message: translatePiMessage(msg),
|
||||
isComplete: false,
|
||||
isStreaming: true,
|
||||
};
|
||||
}
|
||||
|
||||
case 'message_end': {
|
||||
const msg = event.message as PiAgentMessage | undefined;
|
||||
if (!msg) return { isComplete: false, isStreaming: true };
|
||||
this.currentMessage = null;
|
||||
const gooseMsg = translatePiMessage(msg);
|
||||
this.messages.push(gooseMsg);
|
||||
return {
|
||||
message: gooseMsg,
|
||||
isComplete: false,
|
||||
isStreaming: true,
|
||||
};
|
||||
}
|
||||
|
||||
case 'tool_execution_start': {
|
||||
// Tool execution events are already part of the message content
|
||||
// We can emit a partial update here if needed
|
||||
return { isComplete: false, isStreaming: true };
|
||||
}
|
||||
|
||||
case 'tool_execution_end': {
|
||||
// Tool result will be included in the next message
|
||||
return { isComplete: false, isStreaming: true };
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown event type, ignore
|
||||
return { isComplete: false, isStreaming: this.isStreaming };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all accumulated messages.
|
||||
*/
|
||||
getMessages(): GooseMessage[] {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current streaming message (if any).
|
||||
*/
|
||||
getCurrentMessage(): GooseMessage | null {
|
||||
return this.currentMessage ? translatePiMessage(this.currentMessage) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the accumulator.
|
||||
*/
|
||||
reset(): void {
|
||||
this.currentMessage = null;
|
||||
this.messages = [];
|
||||
this.isStreaming = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SSE-compatible event data from a Pi event.
|
||||
* This matches the format goosed uses for /reply SSE stream.
|
||||
*/
|
||||
export function piEventToSSE(event: PiAgentEvent): string | null {
|
||||
const result = new PiEventAccumulator().processEvent(event);
|
||||
|
||||
if (result.message) {
|
||||
return JSON.stringify({
|
||||
type: 'message',
|
||||
data: result.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (result.isComplete) {
|
||||
return JSON.stringify({
|
||||
type: 'done',
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Pi Agent integration for Electron.
|
||||
*
|
||||
* This module provides a bridge between the Electron app and Pi,
|
||||
* running Pi directly in the Node.js main process.
|
||||
*
|
||||
* Architecture:
|
||||
* - Pi runs in the Electron main process
|
||||
* - IPC handlers expose Pi to the renderer
|
||||
* - Events are translated to Goose-compatible format
|
||||
* - MCP extensions configured via goosed are passed to Pi
|
||||
*/
|
||||
|
||||
import { ipcMain, IpcMainInvokeEvent } from 'electron';
|
||||
import { appendFileSync } from 'node:fs';
|
||||
import log from '../utils/logger';
|
||||
import { PiEventAccumulator, type PiAgentEvent, type GooseMessage } from './eventTranslator';
|
||||
|
||||
const PI_LOG = '/tmp/pi-debug.log';
|
||||
const piLog = (msg: string, ...args: unknown[]) => {
|
||||
const line = `[${new Date().toISOString()}] ${msg} ${args.map(a => JSON.stringify(a)).join(' ')}\n`;
|
||||
console.log('[Pi]', msg, ...args);
|
||||
try { appendFileSync(PI_LOG, line); } catch {}
|
||||
};
|
||||
|
||||
// Pi types - these match the actual @mariozechner/pi-coding-agent exports
|
||||
import type {
|
||||
AgentSession,
|
||||
CreateAgentSessionOptions,
|
||||
CreateAgentSessionResult,
|
||||
AgentSessionEvent,
|
||||
} from '@mariozechner/pi-coding-agent';
|
||||
|
||||
interface PiModule {
|
||||
createAgentSession: (options?: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
|
||||
VERSION: string;
|
||||
}
|
||||
|
||||
// Module state
|
||||
let piModule: PiModule | null = null;
|
||||
let currentSession: AgentSession | null = null;
|
||||
let unsubscribeFn: (() => void) | null = null;
|
||||
let eventAccumulator = new PiEventAccumulator();
|
||||
|
||||
/**
|
||||
* Initialize Pi module.
|
||||
* Called once at app startup.
|
||||
*/
|
||||
export async function initializePi(): Promise<boolean> {
|
||||
try {
|
||||
piLog('Attempting to load Pi module...');
|
||||
piModule = (await import('@mariozechner/pi-coding-agent')) as PiModule;
|
||||
piLog('Module loaded successfully, version:', piModule.VERSION);
|
||||
return true;
|
||||
} catch (error) {
|
||||
piLog('Failed to load module:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Pi is available.
|
||||
*/
|
||||
export function isPiAvailable(): boolean {
|
||||
return piModule !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Pi version.
|
||||
*/
|
||||
export function getPiVersion(): string | null {
|
||||
return piModule?.VERSION || null;
|
||||
}
|
||||
|
||||
export interface StartSessionOptions {
|
||||
workingDir?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new Pi session.
|
||||
*/
|
||||
export async function startPiSession(options: StartSessionOptions = {}): Promise<void> {
|
||||
piLog('startPiSession called with options:', options);
|
||||
if (!piModule) {
|
||||
piLog('ERROR: Pi module not loaded');
|
||||
throw new Error('Pi module not loaded. Call initializePi() first.');
|
||||
}
|
||||
|
||||
// Stop existing session if any
|
||||
if (currentSession) {
|
||||
await stopPiSession();
|
||||
}
|
||||
|
||||
// Set working directory
|
||||
if (options.workingDir) {
|
||||
process.chdir(options.workingDir);
|
||||
}
|
||||
|
||||
const sessionConfig: CreateAgentSessionOptions = {
|
||||
cwd: options.workingDir || process.cwd(),
|
||||
};
|
||||
|
||||
piLog('Creating agent session with config:', sessionConfig);
|
||||
const result = await piModule.createAgentSession(sessionConfig);
|
||||
piLog('Session created, result:', { hasSession: !!result.session });
|
||||
currentSession = result.session;
|
||||
eventAccumulator.reset();
|
||||
|
||||
piLog('Session started successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current Pi session.
|
||||
*/
|
||||
export async function stopPiSession(): Promise<void> {
|
||||
if (unsubscribeFn) {
|
||||
unsubscribeFn();
|
||||
unsubscribeFn = null;
|
||||
}
|
||||
if (currentSession) {
|
||||
try {
|
||||
currentSession.abort();
|
||||
} catch {
|
||||
// Ignore abort errors
|
||||
}
|
||||
currentSession = null;
|
||||
eventAccumulator.reset();
|
||||
log.info('[Pi] Session stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt to Pi and stream events via callback.
|
||||
*/
|
||||
export async function promptPi(
|
||||
message: string,
|
||||
onEvent: (event: PiAgentEvent, gooseMessage?: GooseMessage) => void,
|
||||
onComplete: () => void,
|
||||
onError: (error: Error) => void
|
||||
): Promise<void> {
|
||||
piLog('promptPi called with message:', message.substring(0, 100));
|
||||
if (!currentSession) {
|
||||
piLog('ERROR: No Pi session');
|
||||
onError(new Error('No Pi session. Call startPiSession() first.'));
|
||||
return;
|
||||
}
|
||||
|
||||
eventAccumulator.reset();
|
||||
|
||||
// Subscribe to events
|
||||
if (unsubscribeFn) {
|
||||
unsubscribeFn();
|
||||
}
|
||||
piLog('Subscribing to session events...');
|
||||
unsubscribeFn = currentSession.subscribe((event: AgentSessionEvent) => {
|
||||
const piEvent = event as PiAgentEvent;
|
||||
piLog('Received event:', piEvent.type);
|
||||
const result = eventAccumulator.processEvent(piEvent);
|
||||
onEvent(piEvent, result.message);
|
||||
|
||||
if (result.isComplete) {
|
||||
piLog('Event stream complete');
|
||||
if (unsubscribeFn) {
|
||||
unsubscribeFn();
|
||||
unsubscribeFn = null;
|
||||
}
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
piLog('Calling currentSession.prompt()...');
|
||||
await currentSession.prompt(message);
|
||||
piLog('prompt() returned');
|
||||
} catch (error) {
|
||||
piLog('prompt() threw error:', error);
|
||||
if (unsubscribeFn) {
|
||||
unsubscribeFn();
|
||||
unsubscribeFn = null;
|
||||
}
|
||||
onError(error instanceof Error ? error : new Error(String(error)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort current Pi operation.
|
||||
*/
|
||||
export function abortPi(): void {
|
||||
if (currentSession) {
|
||||
currentSession.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Pi state.
|
||||
*/
|
||||
export function getPiState(): {
|
||||
available: boolean;
|
||||
version: string | null;
|
||||
hasSession: boolean;
|
||||
isStreaming: boolean;
|
||||
} {
|
||||
return {
|
||||
available: isPiAvailable(),
|
||||
version: getPiVersion(),
|
||||
hasSession: currentSession !== null,
|
||||
isStreaming: currentSession?.state.isStreaming || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IPC handlers for Pi.
|
||||
* Call this from main.ts after app is ready.
|
||||
*/
|
||||
export function registerPiIpcHandlers(): void {
|
||||
// Check if Pi is available
|
||||
ipcMain.handle('pi:isAvailable', () => {
|
||||
piLog('IPC pi:isAvailable called, returning:', isPiAvailable());
|
||||
return isPiAvailable();
|
||||
});
|
||||
|
||||
// Get Pi version
|
||||
ipcMain.handle('pi:getVersion', () => getPiVersion());
|
||||
|
||||
// Get Pi state
|
||||
ipcMain.handle('pi:getState', () => getPiState());
|
||||
|
||||
// Start Pi session
|
||||
ipcMain.handle('pi:startSession', async (_event: IpcMainInvokeEvent, options: StartSessionOptions) => {
|
||||
piLog('IPC pi:startSession called with options:', options);
|
||||
try {
|
||||
await startPiSession(options);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
piLog('IPC pi:startSession error:', error);
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
});
|
||||
|
||||
// Stop Pi session
|
||||
ipcMain.handle('pi:stopSession', async () => {
|
||||
await stopPiSession();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Abort current operation
|
||||
ipcMain.handle('pi:abort', () => {
|
||||
abortPi();
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// Send prompt - streams events back via IPC
|
||||
ipcMain.handle('pi:prompt', async (event: IpcMainInvokeEvent, message: string) => {
|
||||
const webContents = event.sender;
|
||||
|
||||
return new Promise<{ success: boolean; error?: string }>((resolve) => {
|
||||
promptPi(
|
||||
message,
|
||||
// onEvent
|
||||
(piEvent, gooseMessage) => {
|
||||
// Send raw Pi event
|
||||
webContents.send('pi:event', piEvent);
|
||||
|
||||
// Send translated Goose message if available
|
||||
if (gooseMessage) {
|
||||
webContents.send('pi:message', gooseMessage);
|
||||
}
|
||||
},
|
||||
// onComplete
|
||||
() => {
|
||||
webContents.send('pi:complete');
|
||||
resolve({ success: true });
|
||||
},
|
||||
// onError
|
||||
(error) => {
|
||||
webContents.send('pi:error', { error: error.message });
|
||||
resolve({ success: false, error: error.message });
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
log.info('[Pi] IPC handlers registered');
|
||||
}
|
||||
|
||||
// Re-export types and translator
|
||||
export * from './eventTranslator';
|
||||
@@ -51,6 +51,24 @@ interface UpdaterEvent {
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
// Pi Agent types
|
||||
interface PiSessionOptions {
|
||||
workingDir?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
interface PiState {
|
||||
available: boolean;
|
||||
hasSession: boolean;
|
||||
}
|
||||
|
||||
interface PiAgentEvent {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// Define the API types in a single place
|
||||
type ElectronAPI = {
|
||||
platform: string;
|
||||
@@ -130,6 +148,16 @@ type ElectronAPI = {
|
||||
refreshApp: (app: GooseApp) => Promise<void>;
|
||||
closeApp: (appName: string) => Promise<void>;
|
||||
addRecentDir: (dir: string) => Promise<boolean>;
|
||||
// Pi Agent API
|
||||
pi: {
|
||||
isAvailable: () => Promise<boolean>;
|
||||
getState: () => Promise<PiState>;
|
||||
startSession: (options: PiSessionOptions) => Promise<{ success: boolean }>;
|
||||
stopSession: () => Promise<{ success: boolean }>;
|
||||
prompt: (message: string) => Promise<{ success: boolean; error?: string }>;
|
||||
onEvent: (callback: (event: PiAgentEvent) => void) => void;
|
||||
offEvent: (callback: (event: PiAgentEvent) => void) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type AppConfigAPI = {
|
||||
@@ -263,6 +291,22 @@ const electronAPI: ElectronAPI = {
|
||||
refreshApp: (app: GooseApp) => ipcRenderer.invoke('refresh-app', app),
|
||||
closeApp: (appName: string) => ipcRenderer.invoke('close-app', appName),
|
||||
addRecentDir: (dir: string) => ipcRenderer.invoke('add-recent-dir', dir),
|
||||
// Pi Agent API
|
||||
pi: {
|
||||
isAvailable: () => ipcRenderer.invoke('pi:isAvailable'),
|
||||
getState: () => ipcRenderer.invoke('pi:getState'),
|
||||
startSession: (options: PiSessionOptions) => ipcRenderer.invoke('pi:startSession', options),
|
||||
stopSession: () => ipcRenderer.invoke('pi:stopSession'),
|
||||
prompt: (message: string) => ipcRenderer.invoke('pi:prompt', message),
|
||||
onEvent: (callback: (event: PiAgentEvent) => void) => {
|
||||
const wrappedCallback = (_event: Electron.IpcRendererEvent, data: PiAgentEvent) =>
|
||||
callback(data);
|
||||
ipcRenderer.on('pi:event', wrappedCallback);
|
||||
},
|
||||
offEvent: (callback: (event: PiAgentEvent) => void) => {
|
||||
ipcRenderer.removeListener('pi:event', callback as unknown as (...args: unknown[]) => void);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const appConfigAPI: AppConfigAPI = {
|
||||
|
||||
@@ -21,6 +21,8 @@ export type DefaultKeyboardShortcuts = {
|
||||
[K in keyof KeyboardShortcuts]: string;
|
||||
};
|
||||
|
||||
export type AgentBackend = 'goose' | 'pi';
|
||||
|
||||
export interface Settings {
|
||||
showMenuBarIcon: boolean;
|
||||
showDockIcon: boolean;
|
||||
@@ -29,6 +31,7 @@ export interface Settings {
|
||||
externalGoosed?: ExternalGoosedConfig;
|
||||
globalShortcut?: string | null;
|
||||
keyboardShortcuts?: KeyboardShortcuts;
|
||||
agentBackend?: AgentBackend;
|
||||
}
|
||||
|
||||
export const defaultKeyboardShortcuts: DefaultKeyboardShortcuts = {
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { builtinModules } from 'node:module';
|
||||
|
||||
// Pi and all its transitive deps that have native/ws issues
|
||||
const piExternals = [
|
||||
'@mariozechner/pi-coding-agent',
|
||||
'@mariozechner/pi-agent-core',
|
||||
'@mariozechner/pi-ai',
|
||||
'@mariozechner/pi-tui',
|
||||
'@mariozechner/jiti',
|
||||
'@mariozechner/clipboard',
|
||||
'@google/genai',
|
||||
'openai',
|
||||
'ws',
|
||||
'bufferutil',
|
||||
'utf-8-validate',
|
||||
];
|
||||
|
||||
// https://vitejs.dev/config
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env.GITHUB_OWNER': JSON.stringify(process.env.GITHUB_OWNER || 'block'),
|
||||
'process.env.GITHUB_REPO': JSON.stringify(process.env.GITHUB_REPO || 'goose'),
|
||||
// Disable ws native module lookups - they break Vite bundling
|
||||
'process.env.WS_NO_BUFFER_UTIL': JSON.stringify('1'),
|
||||
'process.env.WS_NO_UTF_8_VALIDATE': JSON.stringify('1'),
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: (id) => {
|
||||
// Node builtins
|
||||
if (builtinModules.includes(id) || id.startsWith('node:')) return true;
|
||||
// Pi and its problematic transitive deps
|
||||
for (const ext of piExternals) {
|
||||
if (id === ext || id.startsWith(ext + '/')) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user