mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-24 19:35:44 +08:00
Compare commits
2 Commits
feat/pendi
...
fix/shell-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ce5b8313 | ||
|
|
9a33426eed |
@@ -171,6 +171,62 @@ function buildShellCommand(
|
||||
return command;
|
||||
}
|
||||
|
||||
function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
|
||||
const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase());
|
||||
return resolvedKey ? env[resolvedKey] : undefined;
|
||||
}
|
||||
|
||||
function getPathEnvKey(env: NodeJS.ProcessEnv): string {
|
||||
return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
||||
}
|
||||
|
||||
function prioritizeUserNpmGlobalBin(env: NodeJS.ProcessEnv): { key: string; value: string | undefined } {
|
||||
const pathKey = getPathEnvKey(env);
|
||||
const currentPath = env[pathKey];
|
||||
if (!currentPath) {
|
||||
return { key: pathKey, value: currentPath };
|
||||
}
|
||||
|
||||
const delimiter = path.delimiter;
|
||||
const pathEntries = currentPath.split(delimiter).filter(Boolean);
|
||||
const npmPrefix = readEnvValue(env, 'npm_config_prefix');
|
||||
const appData = readEnvValue(env, 'APPDATA');
|
||||
const candidates = [
|
||||
npmPrefix || '',
|
||||
npmPrefix ? path.join(npmPrefix, 'bin') : '',
|
||||
appData ? path.join(appData, 'npm') : '',
|
||||
path.join(os.homedir(), 'AppData', 'Roaming', 'npm'),
|
||||
path.join(os.homedir(), '.npm-global', 'bin'),
|
||||
].filter(Boolean);
|
||||
|
||||
const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry);
|
||||
const preferredEntries = candidates.filter((candidate, index) => {
|
||||
const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate;
|
||||
return (
|
||||
candidates.indexOf(candidate) === index &&
|
||||
normalizedPathEntries.includes(normalizedCandidate)
|
||||
);
|
||||
});
|
||||
|
||||
if (preferredEntries.length === 0) {
|
||||
return { key: pathKey, value: currentPath };
|
||||
}
|
||||
|
||||
const normalizedPreferredEntries = preferredEntries.map((entry) =>
|
||||
os.platform() === 'win32' ? entry.toLowerCase() : entry
|
||||
);
|
||||
|
||||
const value = [
|
||||
...preferredEntries,
|
||||
...pathEntries.filter((entry) => {
|
||||
const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry;
|
||||
return !normalizedPreferredEntries.includes(normalizedEntry);
|
||||
}),
|
||||
].join(delimiter);
|
||||
|
||||
return { key: pathKey, value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles websocket connections used by the standalone shell terminal UI.
|
||||
*/
|
||||
@@ -284,6 +340,7 @@ export function handleShellConnection(
|
||||
os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
||||
const termCols = readNumber(data.cols, 80);
|
||||
const termRows = readNumber(data.rows, 24);
|
||||
const prioritizedPath = prioritizeUserNpmGlobalBin(process.env);
|
||||
|
||||
shellProcess = pty.spawn(shell, shellArgs, {
|
||||
name: 'xterm-256color',
|
||||
@@ -292,6 +349,7 @@ export function handleShellConnection(
|
||||
cwd: resolvedProjectPath,
|
||||
env: {
|
||||
...process.env,
|
||||
[prioritizedPath.key]: prioritizedPath.value,
|
||||
TERM: 'xterm-256color',
|
||||
COLORTERM: 'truecolor',
|
||||
FORCE_COLOR: '3',
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
|
||||
import type { ServerEvent } from '../../../contexts/WebSocketContext';
|
||||
import { showCompletionTitleIndicator } from '../../../utils/pageTitleNotification';
|
||||
import { playChatCompletionSound, playNotificationSound } from '../../../utils/notificationSound';
|
||||
import { playChatCompletionSound } from '../../../utils/notificationSound';
|
||||
import type { MarkSessionIdle, MarkSessionProcessing } from '../../../hooks/useSessionProtection';
|
||||
import type { PendingPermissionRequest } from '../types/types';
|
||||
import type { ProjectSession, LLMProvider } from '../../../types/app';
|
||||
import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore';
|
||||
|
||||
const isActionablePermissionRequest = (request: { toolName?: unknown } | null | undefined): boolean => {
|
||||
return request?.toolName !== 'ExitPlanMode' && request?.toolName !== 'exit_plan_mode';
|
||||
};
|
||||
|
||||
const hasActionablePermissionRequests = (requests: Array<{ toolName?: unknown }> | null | undefined): boolean => {
|
||||
return Array.isArray(requests) && requests.some((request) => isActionablePermissionRequest(request));
|
||||
};
|
||||
|
||||
interface UseChatRealtimeHandlersArgs {
|
||||
subscribe: (listener: (event: ServerEvent) => void) => () => void;
|
||||
provider: LLMProvider;
|
||||
selectedSession: ProjectSession | null;
|
||||
currentSessionId: string | null;
|
||||
setTokenBudget: (budget: Record<string, unknown> | null) => void;
|
||||
pendingPermissionRequests: PendingPermissionRequest[];
|
||||
setPendingPermissionRequests: Dispatch<SetStateAction<PendingPermissionRequest[]>>;
|
||||
streamTimerRef: MutableRefObject<number | null>;
|
||||
accumulatedStreamRef: MutableRefObject<string>;
|
||||
@@ -61,7 +52,6 @@ export function useChatRealtimeHandlers({
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setTokenBudget,
|
||||
pendingPermissionRequests,
|
||||
setPendingPermissionRequests,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
@@ -72,15 +62,6 @@ export function useChatRealtimeHandlers({
|
||||
onWebSocketReconnect,
|
||||
sessionStore,
|
||||
}: UseChatRealtimeHandlersArgs) {
|
||||
// Keep the latest pending-permission snapshot available to the websocket
|
||||
// listener so back-to-back permission events can dedupe and re-arm the
|
||||
// notification sound before React finishes a rerender.
|
||||
const pendingPermissionRequestsRef = useRef(pendingPermissionRequests);
|
||||
|
||||
useEffect(() => {
|
||||
pendingPermissionRequestsRef.current = pendingPermissionRequests;
|
||||
}, [pendingPermissionRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEvent = (msg: ServerEvent) => {
|
||||
if (!msg.kind) {
|
||||
@@ -120,16 +101,7 @@ export function useChatRealtimeHandlers({
|
||||
|
||||
const isViewedSession = sid === activeViewSessionId;
|
||||
if (isViewedSession && Array.isArray(msg.pendingPermissions)) {
|
||||
const nextPendingPermissionRequests = msg.pendingPermissions as PendingPermissionRequest[];
|
||||
const hadActionablePermissionRequests = hasActionablePermissionRequests(pendingPermissionRequestsRef.current);
|
||||
const hasPendingActionablePermissionRequests = hasActionablePermissionRequests(nextPendingPermissionRequests);
|
||||
|
||||
pendingPermissionRequestsRef.current = nextPendingPermissionRequests;
|
||||
setPendingPermissionRequests(nextPendingPermissionRequests);
|
||||
|
||||
if (hasPendingActionablePermissionRequests && !hadActionablePermissionRequests) {
|
||||
void playNotificationSound();
|
||||
}
|
||||
setPendingPermissionRequests(msg.pendingPermissions as PendingPermissionRequest[]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -231,7 +203,6 @@ export function useChatRealtimeHandlers({
|
||||
// hides it immediately and atomically.
|
||||
onSessionIdle?.(sid);
|
||||
if (sid === activeViewSessionId) {
|
||||
pendingPermissionRequestsRef.current = [];
|
||||
setPendingPermissionRequests([]);
|
||||
}
|
||||
|
||||
@@ -264,9 +235,9 @@ export function useChatRealtimeHandlers({
|
||||
case 'permission_request': {
|
||||
if (!msg.requestId) break;
|
||||
if (sid === activeViewSessionId) {
|
||||
const previousPendingPermissionRequests = pendingPermissionRequestsRef.current;
|
||||
if (!previousPendingPermissionRequests.some((request) => request.requestId === msg.requestId)) {
|
||||
const nextPendingPermissionRequests = [...previousPendingPermissionRequests, {
|
||||
setPendingPermissionRequests((prev) => {
|
||||
if (prev.some((r: PendingPermissionRequest) => r.requestId === msg.requestId)) return prev;
|
||||
return [...prev, {
|
||||
requestId: msg.requestId as string,
|
||||
toolName: (msg.toolName as string) || 'UnknownTool',
|
||||
input: msg.input,
|
||||
@@ -274,17 +245,7 @@ export function useChatRealtimeHandlers({
|
||||
sessionId: sid || null,
|
||||
receivedAt: new Date(),
|
||||
}];
|
||||
|
||||
pendingPermissionRequestsRef.current = nextPendingPermissionRequests;
|
||||
setPendingPermissionRequests(nextPendingPermissionRequests);
|
||||
|
||||
if (
|
||||
isActionablePermissionRequest({ toolName: msg.toolName })
|
||||
&& !hasActionablePermissionRequests(previousPendingPermissionRequests)
|
||||
) {
|
||||
void playNotificationSound();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (sid) {
|
||||
onSessionProcessing?.(sid);
|
||||
@@ -294,12 +255,7 @@ export function useChatRealtimeHandlers({
|
||||
|
||||
case 'permission_cancelled': {
|
||||
if (msg.requestId && sid === activeViewSessionId) {
|
||||
const nextPendingPermissionRequests = pendingPermissionRequestsRef.current.filter(
|
||||
(request: PendingPermissionRequest) => request.requestId !== msg.requestId,
|
||||
);
|
||||
|
||||
pendingPermissionRequestsRef.current = nextPendingPermissionRequests;
|
||||
setPendingPermissionRequests(nextPendingPermissionRequests);
|
||||
setPendingPermissionRequests((prev) => prev.filter((r: PendingPermissionRequest) => r.requestId !== msg.requestId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -330,7 +286,6 @@ export function useChatRealtimeHandlers({
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setTokenBudget,
|
||||
pendingPermissionRequests,
|
||||
setPendingPermissionRequests,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
|
||||
@@ -239,7 +239,6 @@ function ChatInterface({
|
||||
selectedSession,
|
||||
currentSessionId,
|
||||
setTokenBudget,
|
||||
pendingPermissionRequests,
|
||||
setPendingPermissionRequests,
|
||||
streamTimerRef,
|
||||
accumulatedStreamRef,
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"sound": {
|
||||
"title": "Sound",
|
||||
"description": "Play a short tone when a chat run finishes or needs tool approval.",
|
||||
"description": "Play a short tone when a chat run finishes.",
|
||||
"enabled": "Enabled",
|
||||
"test": "Test sound"
|
||||
},
|
||||
|
||||
@@ -58,7 +58,7 @@ const playTone = (
|
||||
oscillator.stop(startsAt + duration + 0.02);
|
||||
};
|
||||
|
||||
export const playNotificationSound = async ({ force = false } = {}): Promise<void> => {
|
||||
export const playChatCompletionSound = async ({ force = false } = {}): Promise<void> => {
|
||||
if (!force && !isNotificationSoundEnabled()) {
|
||||
return;
|
||||
}
|
||||
@@ -81,5 +81,3 @@ export const playNotificationSound = async ({ force = false } = {}): Promise<voi
|
||||
console.warn('Unable to play notification sound:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const playChatCompletionSound = (options = {}): Promise<void> => playNotificationSound(options);
|
||||
|
||||
Reference in New Issue
Block a user