mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
Add tests and implement functionality for staged cascade search and LSP expansion
- Introduced a new JSON file for verbose output of the Codex Lens search results. - Added unit tests for binary search functionality in `test_stage1_binary_search_uses_chunk_lines.py`. - Implemented regression tests for staged cascade Stage 2 expansion depth in `test_staged_cascade_lsp_depth.py`. - Created unit tests for staged cascade Stage 2 realtime LSP graph expansion in `test_staged_cascade_realtime_lsp.py`. - Enhanced the ChainSearchEngine to respect configuration settings for staged LSP depth and improve search accuracy.
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
// ========================================
|
||||
// Workspace Selector Component
|
||||
// ========================================
|
||||
// Dropdown for selecting recent workspaces with folder browser and manual path input
|
||||
// Dropdown for selecting recent workspaces with native folder picker and manual path input
|
||||
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { ChevronDown, X, FolderOpen, Check } from 'lucide-react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { selectFolder } from '@/lib/nativeDialog';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import {
|
||||
@@ -69,7 +70,7 @@ function truncatePath(path: string, maxChars: number = 40): string {
|
||||
* Workspace selector component
|
||||
*
|
||||
* Provides a dropdown menu for selecting from recent workspace paths,
|
||||
* a manual path input dialog for entering custom paths, and delete buttons
|
||||
* a native OS folder picker, a manual path input dialog, and delete buttons
|
||||
* for removing paths from recent history.
|
||||
*
|
||||
* @example
|
||||
@@ -86,15 +87,9 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
|
||||
// UI state
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [isBrowseOpen, setIsBrowseOpen] = useState(false);
|
||||
const [isManualOpen, setIsManualOpen] = useState(false);
|
||||
const [manualPath, setManualPath] = useState('');
|
||||
|
||||
// Hidden file input for folder selection
|
||||
const folderInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
/**
|
||||
* Handle path selection from dropdown
|
||||
*/
|
||||
const handleSelectPath = useCallback(
|
||||
async (path: string) => {
|
||||
await switchWorkspace(path);
|
||||
@@ -103,77 +98,30 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
[switchWorkspace]
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle remove path from recent history
|
||||
*/
|
||||
const handleRemovePath = useCallback(
|
||||
async (e: React.MouseEvent, path: string) => {
|
||||
e.stopPropagation(); // Prevent triggering selection
|
||||
e.stopPropagation();
|
||||
await removeRecentPath(path);
|
||||
},
|
||||
[removeRecentPath]
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle open folder browser - trigger hidden file input click
|
||||
*/
|
||||
const handleBrowseFolder = useCallback(() => {
|
||||
const handleBrowseFolder = useCallback(async () => {
|
||||
setIsDropdownOpen(false);
|
||||
// Trigger the hidden file input click
|
||||
folderInputRef.current?.click();
|
||||
}, []);
|
||||
const selected = await selectFolder(projectPath || undefined);
|
||||
if (selected) {
|
||||
await switchWorkspace(selected);
|
||||
}
|
||||
}, [projectPath, switchWorkspace]);
|
||||
|
||||
/**
|
||||
* Handle folder selection from file input
|
||||
*/
|
||||
const handleFolderSelect = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
// Get the path from the first file
|
||||
const firstFile = files[0];
|
||||
// The webkitRelativePath contains the full path relative to the selected folder
|
||||
// We need to get the parent directory path
|
||||
const relativePath = firstFile.webkitRelativePath;
|
||||
const folderPath = relativePath.substring(0, relativePath.indexOf('/'));
|
||||
|
||||
// In browser environment, we can't get the full absolute path
|
||||
// We need to ask the user to confirm or use the folder name
|
||||
// For now, open the manual dialog with the folder name as hint
|
||||
setManualPath(folderPath);
|
||||
setIsBrowseOpen(true);
|
||||
}
|
||||
// Reset input value to allow selecting the same folder again
|
||||
e.target.value = '';
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle manual path submission
|
||||
*/
|
||||
const handleManualPathSubmit = useCallback(async () => {
|
||||
const trimmedPath = manualPath.trim();
|
||||
if (!trimmedPath) {
|
||||
return; // TODO: Show validation error
|
||||
}
|
||||
|
||||
if (!trimmedPath) return;
|
||||
await switchWorkspace(trimmedPath);
|
||||
setIsBrowseOpen(false);
|
||||
setIsManualOpen(false);
|
||||
setManualPath('');
|
||||
}, [manualPath, switchWorkspace]);
|
||||
|
||||
/**
|
||||
* Handle dialog cancel
|
||||
*/
|
||||
const handleDialogCancel = useCallback(() => {
|
||||
setIsBrowseOpen(false);
|
||||
setManualPath('');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle keyboard events in dialog input
|
||||
*/
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -259,7 +207,7 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
|
||||
{recentPaths.length > 0 && <DropdownMenuSeparator />}
|
||||
|
||||
{/* Browse button to open folder selector */}
|
||||
{/* Browse button to open native folder selector */}
|
||||
<DropdownMenuItem
|
||||
onClick={handleBrowseFolder}
|
||||
className="cursor-pointer gap-2"
|
||||
@@ -279,7 +227,7 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(false);
|
||||
setIsBrowseOpen(true);
|
||||
setIsManualOpen(true);
|
||||
}}
|
||||
className="cursor-pointer gap-2"
|
||||
>
|
||||
@@ -290,20 +238,8 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Hidden file input for folder selection */}
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
<input
|
||||
ref={folderInputRef}
|
||||
type="file"
|
||||
{...({ webkitdirectory: '', directory: '' } as any)}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFolderSelect}
|
||||
aria-hidden="true"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
||||
{/* Manual path input dialog */}
|
||||
<Dialog open={isBrowseOpen} onOpenChange={setIsBrowseOpen}>
|
||||
<Dialog open={isManualOpen} onOpenChange={setIsManualOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
@@ -324,7 +260,10 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleDialogCancel}
|
||||
onClick={() => {
|
||||
setIsManualOpen(false);
|
||||
setManualPath('');
|
||||
}}
|
||||
>
|
||||
{formatMessage({ id: 'common.actions.cancel' })}
|
||||
</Button>
|
||||
|
||||
36
ccw/frontend/src/lib/nativeDialog.ts
Normal file
36
ccw/frontend/src/lib/nativeDialog.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Native OS dialog helpers
|
||||
* Calls server-side endpoints that open system-native file/folder picker dialogs.
|
||||
*/
|
||||
|
||||
export async function selectFolder(initialDir?: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch('/api/dialog/select-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ initialDir }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (data.cancelled) return null;
|
||||
return data.path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectFile(initialDir?: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch('/api/dialog/select-file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ initialDir }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (data.cancelled) return null;
|
||||
return data.path || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function searchUnsplash(
|
||||
export async function uploadBackgroundImage(file: File): Promise<{ url: string; filename: string }> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': file.type,
|
||||
'X-Filename': file.name,
|
||||
'X-Filename': encodeURIComponent(file.name),
|
||||
};
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
"dialog": {
|
||||
"title": "Select Project Folder",
|
||||
"placeholder": "Enter project path...",
|
||||
"help": "The path to your project directory"
|
||||
"help": "The path to your project directory",
|
||||
"selectCurrent": "Select This Folder",
|
||||
"parentDir": "Parent Directory",
|
||||
"loading": "Loading...",
|
||||
"emptyDir": "Empty directory",
|
||||
"accessDenied": "Cannot access this directory"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
"dialog": {
|
||||
"title": "选择项目文件夹",
|
||||
"placeholder": "输入项目路径...",
|
||||
"help": "您的项目目录路径"
|
||||
"help": "您的项目目录路径",
|
||||
"selectCurrent": "选择此文件夹",
|
||||
"parentDir": "上级目录",
|
||||
"loading": "加载中...",
|
||||
"emptyDir": "空目录",
|
||||
"accessDenied": "无法访问此目录"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
|
||||
@@ -35,13 +35,6 @@ import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/Dialog';
|
||||
import { ThemeSelector } from '@/components/shared/ThemeSelector';
|
||||
import { useTheme } from '@/hooks';
|
||||
import { toast } from 'sonner';
|
||||
@@ -63,146 +56,43 @@ import {
|
||||
useUpgradeCcwInstallation,
|
||||
} from '@/hooks/useSystemSettings';
|
||||
|
||||
// ========== File Path Input with Browse Dialog ==========
|
||||
|
||||
interface BrowseItem {
|
||||
name: string;
|
||||
path: string;
|
||||
isDirectory: boolean;
|
||||
isFile: boolean;
|
||||
}
|
||||
// ========== File Path Input with Native File Picker ==========
|
||||
|
||||
interface FilePathInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder: string;
|
||||
showHidden?: boolean;
|
||||
}
|
||||
|
||||
function FilePathInput({ value, onChange, placeholder, showHidden = true }: FilePathInputProps) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [browseItems, setBrowseItems] = useState<BrowseItem[]>([]);
|
||||
const [currentBrowsePath, setCurrentBrowsePath] = useState('');
|
||||
const [parentPath, setParentPath] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const browseDirectory = async (dirPath?: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/dialog/browse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: dirPath || '~', showHidden }),
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
setBrowseItems(data.items || []);
|
||||
setCurrentBrowsePath(data.currentPath || '');
|
||||
setParentPath(data.parentPath || '');
|
||||
} catch {
|
||||
// silently fail
|
||||
} finally {
|
||||
setLoading(false);
|
||||
function FilePathInput({ value, onChange, placeholder }: FilePathInputProps) {
|
||||
const handleBrowse = async () => {
|
||||
const { selectFile } = await import('@/lib/nativeDialog');
|
||||
const initialDir = value ? value.replace(/[/\\][^/\\]*$/, '') : undefined;
|
||||
const selected = await selectFile(initialDir);
|
||||
if (selected) {
|
||||
onChange(selected);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setDialogOpen(true);
|
||||
// If value is set, browse its parent directory; otherwise browse home
|
||||
const startPath = value ? value.replace(/[/\\][^/\\]*$/, '') : undefined;
|
||||
browseDirectory(startPath);
|
||||
};
|
||||
|
||||
const handleSelectFile = (filePath: string) => {
|
||||
onChange(filePath);
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 h-9"
|
||||
onClick={handleOpen}
|
||||
title="Browse"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
Browse Files
|
||||
</DialogTitle>
|
||||
<DialogDescription className="font-mono text-xs truncate" title={currentBrowsePath}>
|
||||
{currentBrowsePath}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<RefreshCw className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-auto max-h-[350px]">
|
||||
{/* Parent directory */}
|
||||
{parentPath && parentPath !== currentBrowsePath && (
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted/50 transition-colors text-left border-b border-border"
|
||||
onClick={() => browseDirectory(parentPath)}
|
||||
>
|
||||
<Folder className="w-4 h-4 text-primary shrink-0" />
|
||||
<span className="text-muted-foreground font-medium">..</span>
|
||||
</button>
|
||||
)}
|
||||
{browseItems.map((item) => (
|
||||
<button
|
||||
key={item.path}
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-muted/50 transition-colors text-left"
|
||||
onClick={() => {
|
||||
if (item.isDirectory) {
|
||||
browseDirectory(item.path);
|
||||
} else {
|
||||
handleSelectFile(item.path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.isDirectory ? (
|
||||
<Folder className="w-4 h-4 text-primary shrink-0" />
|
||||
) : (
|
||||
<File className="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
)}
|
||||
<span className={cn('truncate', item.isFile && 'text-foreground font-medium')}>
|
||||
{item.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{browseItems.length === 0 && (
|
||||
<div className="px-3 py-8 text-sm text-muted-foreground text-center">
|
||||
Empty directory
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="shrink-0 h-9"
|
||||
onClick={handleBrowse}
|
||||
title="Browse"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -439,6 +439,119 @@ export async function handleSystemRoutes(ctx: SystemRouteContext): Promise<boole
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Native OS folder selection dialog
|
||||
if (pathname === '/api/dialog/select-folder' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { initialDir } = body as { initialDir?: string };
|
||||
const os = await import('os');
|
||||
const { execFile } = await import('child_process');
|
||||
const startDir = initialDir || os.homedir();
|
||||
|
||||
return new Promise<Record<string, unknown>>((resolve) => {
|
||||
if (process.platform === 'win32') {
|
||||
const script = `Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.SelectedPath = '${startDir.replace(/'/g, "''")}'; $d.ShowNewFolderButton = $true; if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath }`;
|
||||
execFile('powershell', ['-NoProfile', '-Command', script],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
resolve({ cancelled: true });
|
||||
} else {
|
||||
resolve({ path: stdout.trim() });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (process.platform === 'darwin') {
|
||||
const escapedDir = startDir.replace(/"/g, '\\"');
|
||||
const script = `POSIX path of (choose folder with prompt "Select Project Folder" default location POSIX file "${escapedDir}")`;
|
||||
execFile('osascript', ['-e', script],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
resolve({ cancelled: true });
|
||||
} else {
|
||||
resolve({ path: stdout.trim().replace(/\/$/, '') });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Linux: try zenity, fallback to kdialog
|
||||
execFile('zenity', ['--file-selection', '--directory', '--title=Select Project Folder', `--filename=${startDir}/`],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
execFile('kdialog', ['--getexistingdirectory', startDir, '--title', 'Select Project Folder'],
|
||||
{ timeout: 120000 },
|
||||
(err2, stdout2) => {
|
||||
resolve(err2 || !stdout2.trim() ? { cancelled: true } : { path: stdout2.trim() });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve({ path: stdout.trim() });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: Native OS file selection dialog
|
||||
if (pathname === '/api/dialog/select-file' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { initialDir } = body as { initialDir?: string };
|
||||
const os = await import('os');
|
||||
const { execFile } = await import('child_process');
|
||||
const startDir = initialDir || os.homedir();
|
||||
|
||||
return new Promise<Record<string, unknown>>((resolve) => {
|
||||
if (process.platform === 'win32') {
|
||||
const script = `Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.OpenFileDialog; $d.InitialDirectory = '${startDir.replace(/'/g, "''")}'; if ($d.ShowDialog() -eq 'OK') { $d.FileName }`;
|
||||
execFile('powershell', ['-NoProfile', '-Command', script],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
resolve({ cancelled: true });
|
||||
} else {
|
||||
resolve({ path: stdout.trim() });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else if (process.platform === 'darwin') {
|
||||
const escapedDir = startDir.replace(/"/g, '\\"');
|
||||
const script = `POSIX path of (choose file with prompt "Select File" default location POSIX file "${escapedDir}")`;
|
||||
execFile('osascript', ['-e', script],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
resolve({ cancelled: true });
|
||||
} else {
|
||||
resolve({ path: stdout.trim() });
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
execFile('zenity', ['--file-selection', '--title=Select File', `--filename=${startDir}/`],
|
||||
{ timeout: 120000 },
|
||||
(err, stdout) => {
|
||||
if (err || !stdout.trim()) {
|
||||
execFile('kdialog', ['--getopenfilename', startDir, '--title', 'Select File'],
|
||||
{ timeout: 120000 },
|
||||
(err2, stdout2) => {
|
||||
resolve(err2 || !stdout2.trim() ? { cancelled: true } : { path: stdout2.trim() });
|
||||
}
|
||||
);
|
||||
} else {
|
||||
resolve({ path: stdout.trim() });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// API: File dialog - list directory contents for file browser
|
||||
if (pathname === '/api/dialog/browse' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
|
||||
@@ -551,15 +551,18 @@ export function handleMultiAnswer(compositeId: string, answers: QuestionAnswer[]
|
||||
* Automatically stops when the questionId is no longer in pendingQuestions (timeout cleanup).
|
||||
*/
|
||||
function startAnswerPolling(questionId: string, isComposite: boolean = false): void {
|
||||
const path = `/api/a2ui/answer?questionId=${encodeURIComponent(questionId)}&composite=${isComposite}`;
|
||||
const pollPath = `/api/a2ui/answer?questionId=${encodeURIComponent(questionId)}&composite=${isComposite}`;
|
||||
|
||||
console.error(`[A2UI-Poll] Starting polling for questionId=${questionId}, composite=${isComposite}, port=${DASHBOARD_PORT}`);
|
||||
|
||||
const poll = () => {
|
||||
// Stop if the question was already resolved or timed out
|
||||
if (!pendingQuestions.has(questionId)) {
|
||||
console.error(`[A2UI-Poll] Stopping: questionId=${questionId} no longer pending`);
|
||||
return;
|
||||
}
|
||||
|
||||
const req = http.get({ hostname: 'localhost', port: DASHBOARD_PORT, path }, (res) => {
|
||||
const req = http.get({ hostname: '127.0.0.1', port: DASHBOARD_PORT, path: pollPath }, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
||||
res.on('end', () => {
|
||||
@@ -571,23 +574,27 @@ function startAnswerPolling(questionId: string, isComposite: boolean = false): v
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`[A2UI-Poll] Answer received for questionId=${questionId}:`, JSON.stringify(parsed).slice(0, 200));
|
||||
|
||||
if (isComposite && Array.isArray(parsed.answers)) {
|
||||
handleMultiAnswer(questionId, parsed.answers as QuestionAnswer[]);
|
||||
const ok = handleMultiAnswer(questionId, parsed.answers as QuestionAnswer[]);
|
||||
console.error(`[A2UI-Poll] handleMultiAnswer result: ${ok}`);
|
||||
} else if (!isComposite && parsed.answer) {
|
||||
handleAnswer(parsed.answer as QuestionAnswer);
|
||||
const ok = handleAnswer(parsed.answer as QuestionAnswer);
|
||||
console.error(`[A2UI-Poll] handleAnswer result: ${ok}`);
|
||||
} else {
|
||||
// Unexpected shape, keep polling
|
||||
console.error(`[A2UI-Poll] Unexpected response shape, keep polling`);
|
||||
setTimeout(poll, POLL_INTERVAL_MS);
|
||||
}
|
||||
} catch {
|
||||
// Parse error, keep polling
|
||||
} catch (e) {
|
||||
console.error(`[A2UI-Poll] Parse error:`, e);
|
||||
setTimeout(poll, POLL_INTERVAL_MS);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', () => {
|
||||
// Network error (Dashboard not reachable), keep trying
|
||||
req.on('error', (err) => {
|
||||
console.error(`[A2UI-Poll] Network error: ${err.message}`);
|
||||
if (pendingQuestions.has(questionId)) {
|
||||
setTimeout(poll, POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user