fix(pi): add snap node compatibility diagnostics and documentation

- Added detailed error logging to detect snap node compatibility issues
- When Pi process exits with code 1, log helpful diagnostic info including node path
- Add hint to check for snap node and reinstall via apt/nvm
- Create SNAP_NODE_COMPATIBILITY.md with full troubleshooting guide
- Document root cause: snap node has file descriptor incompatibility with Bun.spawn stdin pipes
- Provide clear installation instructions for NodeSource and nvm alternatives
This commit is contained in:
2026-03-04 01:45:36 +00:00
parent 72d1341cbc
commit ef13f96d36
34 changed files with 2394 additions and 1466 deletions
@@ -85,12 +85,7 @@ function SessionChat({ sessionId, model }: SessionChatProps) {
navigate('/chat', { replace: true });
}}
/>
<EmbeddableChat
chat={chat}
sessionId={sessionId}
initialModel={model ?? undefined}
className="flex-1 min-h-0"
/>
<EmbeddableChat chat={chat} sessionId={sessionId} initialModel={model ?? undefined} className="flex-1 min-h-0" />
</div>
);
}
@@ -132,13 +127,13 @@ function NewChat({ allowHostMode }: { allowHostMode?: boolean }) {
const location = useLocation();
const locationState = location.state as ChatLocationState;
const { user } = useAuth();
const showToggle = allowHostMode && user?.role === 'Super Admin';
const [cwdMode, setCwdMode] = useState<'user' | 'host'>('user');
const isSuperAdmin = user?.role === 'Super Admin';
const chat = usePiChat(undefined, locationState?.model);
const sandboxed = showToggle ? cwdMode === 'user' : true;
const cwd = !sandboxed ? { path: getHostHome() } : locationState?.cwd;
// Super Admin always operates as host — no toggle needed
const sandboxed = !isSuperAdmin;
const cwd = isSuperAdmin ? { path: getHostHome() } : locationState?.cwd;
const initialMessage = locationState?.initialMessage
? {
@@ -157,7 +152,6 @@ function NewChat({ allowHostMode }: { allowHostMode?: boolean }) {
isGenerating={chat.isGenerating}
onDelete={undefined}
/>
{!chat.hasStarted && showToggle && <CwdToggle cwdMode={cwdMode} onChange={setCwdMode} />}
<EmbeddableChat
chat={chat}
sessionId={undefined}
@@ -1,29 +1,13 @@
import { TerminalSquare, Monitor, Columns2, PenLine, Sparkles } from 'lucide-react';
import { useWorkspace } from '../../components/Workspace';
import { useAuth } from 'hooks/useAuth';
import { useTerminalMode } from './useTerminalMode';
export const TerminalHeader = ({ panelId }: { panelId: string }) => {
export const TerminalHeader = () => {
const { cwd } = useWorkspace();
const { user } = useAuth();
const { mode, toggle } = useTerminalMode(panelId);
const isHost = mode === 'host';
const scoped = cwd !== '~';
const Icon = isHost && !scoped ? Monitor : TerminalSquare;
return (
<>
<Icon className="h-3.5 w-3.5 shrink-0" />
<TerminalSquare className="h-3.5 w-3.5 shrink-0" />
<span className="text-xs font-medium shrink-0">Terminal</span>
{user?.role === 'Super Admin' && !scoped && (
<button
type="button"
onClick={toggle}
className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-white/10 hover:bg-white/20 transition-colors cursor-pointer shrink-0"
>
{isHost ? 'Host' : 'Home'}
</button>
)}
<span className="text-[10px] font-mono truncate opacity-60">{cwd}</span>
</>
);
@@ -23,7 +23,16 @@ type UsePiChatOptions = {
};
export function usePiChat(initialSessionId?: string, initialModel?: string | null, options?: UsePiChatOptions) {
const { replaceUrl = true, storage, resourceChatDir, taskInfo, projectScoped, context, contextId, onTurnComplete } = options ?? {};
const {
replaceUrl = true,
storage,
resourceChatDir,
taskInfo,
projectScoped,
context,
contextId,
onTurnComplete,
} = options ?? {};
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [streamingText, setStreamingText] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
@@ -77,9 +86,10 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
function commitStreaming() {
if (!streamingRef.current) return;
setMessages((prev) => [...prev, { role: 'assistant', id: crypto.randomUUID(), text: streamingRef.current }]);
const text = streamingRef.current;
streamingRef.current = '';
setStreamingText('');
setMessages((prev) => [...prev, { role: 'assistant', id: crypto.randomUUID(), text }]);
}
function handleMessage(data: unknown) {
@@ -286,7 +296,10 @@ export function usePiChat(initialSessionId?: string, initialModel?: string | nul
setHasStarted(true);
}
setMessages((prev) => [...prev, { role: 'user', text: displayText ?? text, ...(images?.length ? { images } : {}) }]);
setMessages((prev) => [
...prev,
{ role: 'user', text: displayText ?? text, ...(images?.length ? { images } : {}) },
]);
setIsGenerating(true);
streamingRef.current = '';
setStreamingText('');
+8 -3
View File
@@ -30,7 +30,11 @@ export function usePiModels() {
queryKey: ['PI_MODELS'],
enabled: isAuthenticated,
queryFn: async () => {
const data = await client.get<{ models: ModelOption[]; providerNames?: Record<string, string>; hostHome?: string }>('/pi/models');
const data = await client.get<{
models: ModelOption[];
providerNames?: Record<string, string>;
hostHome?: string;
}>('/pi/models');
if (data.providerNames) {
globalProviderNames = data.providerNames;
@@ -48,13 +52,14 @@ export function usePiModels() {
return models;
}
/** Filter models by system-wide access policy. New providers pass through. */
/** Filter models by system-wide access policy. Super Admin sees all. New providers pass through. */
export function useVisiblePiModels() {
const models = usePiModels();
const { user } = useAuth();
const { policy } = useAccessPolicy();
const allowed = policy.allowedModels;
if (allowed.length === 0) return models;
if (user?.role === 'Super Admin' || allowed.length === 0) return models;
const allowedSet = new Set(allowed);
const allowedProviderSet = new Set(allowed.map((key) => key.split(':')[0]));