mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Make provider diagnostics easier to share (#1611)
* Show PATH matches in provider diagnostics * Add copyable provider command probes * Fix provider diagnostic path reporting * Clean up command probe diagnostics
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { AlertTriangle, FileText, Plus, RotateCw, Trash2 } from "lucide-react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { AlertTriangle, Copy, FileText, Plus, RotateCw, Trash2 } from "lucide-react-native";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,6 +21,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { CODE_SURFACE_DATASET } from "@/styles/code-surface";
|
||||
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
||||
@@ -248,6 +250,7 @@ function DiagnosticSubSheet({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const [diagnostic, setDiagnostic] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -288,31 +291,62 @@ function DiagnosticSubSheet({
|
||||
void fetchDiagnostic();
|
||||
}, [fetchDiagnostic]);
|
||||
|
||||
const copyButtonStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
sheetStyles.iconButton,
|
||||
(Boolean(hovered) || pressed) && Boolean(diagnostic) && sheetStyles.iconButtonHovered,
|
||||
diagnostic ? null : sheetStyles.disabled,
|
||||
],
|
||||
[diagnostic],
|
||||
);
|
||||
|
||||
const handleCopyPress = useCallback(() => {
|
||||
if (!diagnostic) return;
|
||||
void Clipboard.setStringAsync(diagnostic)
|
||||
.then(() => toast.copied(t("settings.providers.diagnostic.copyLabel")))
|
||||
.catch(() => toast.error(t("settings.providers.diagnostic.copyFailed")));
|
||||
}, [diagnostic, t, toast]);
|
||||
|
||||
const header = useMemo<SheetHeader>(
|
||||
() => ({
|
||||
title: t("settings.providers.diagnostic.title"),
|
||||
actions: (
|
||||
<Pressable
|
||||
onPress={handleRefreshPress}
|
||||
disabled={loading}
|
||||
hitSlop={8}
|
||||
style={refreshButtonStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
loading
|
||||
? t("settings.providers.diagnostic.refreshingAccessibility")
|
||||
: t("settings.providers.diagnostic.refreshAccessibility")
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
</Pressable>
|
||||
<View style={sheetStyles.headerActions}>
|
||||
<Pressable
|
||||
onPress={handleCopyPress}
|
||||
disabled={!diagnostic}
|
||||
hitSlop={8}
|
||||
style={copyButtonStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("settings.providers.diagnostic.copyAccessibility")}
|
||||
>
|
||||
<Copy size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleRefreshPress}
|
||||
disabled={loading}
|
||||
hitSlop={8}
|
||||
style={refreshButtonStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
loading
|
||||
? t("settings.providers.diagnostic.refreshingAccessibility")
|
||||
: t("settings.providers.diagnostic.refreshAccessibility")
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
) : (
|
||||
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
}),
|
||||
[
|
||||
copyButtonStyle,
|
||||
diagnostic,
|
||||
handleCopyPress,
|
||||
handleRefreshPress,
|
||||
loading,
|
||||
refreshButtonStyle,
|
||||
@@ -733,6 +767,11 @@ const sheetStyles = StyleSheet.create((theme) => ({
|
||||
iconButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
|
||||
@@ -1778,6 +1778,9 @@ export const ar: TranslationResources = {
|
||||
button: "التشخيص",
|
||||
refresh: "ينعش",
|
||||
refreshing: "منعش...",
|
||||
copyLabel: "التشخيص",
|
||||
copyAccessibility: "نسخ التشخيص",
|
||||
copyFailed: "فشل نسخ التشخيص",
|
||||
refreshAccessibility: "تحديث التشخيص",
|
||||
refreshingAccessibility: "تحديث التشخيص",
|
||||
running: "تشغيل التشخيص...",
|
||||
|
||||
@@ -1786,6 +1786,9 @@ export const en = {
|
||||
button: "Diagnostic",
|
||||
refresh: "Refresh",
|
||||
refreshing: "Refreshing...",
|
||||
copyLabel: "diagnostic",
|
||||
copyAccessibility: "Copy diagnostic",
|
||||
copyFailed: "Failed to copy diagnostic",
|
||||
refreshAccessibility: "Refresh diagnostic",
|
||||
refreshingAccessibility: "Refreshing diagnostic",
|
||||
running: "Running diagnostic...",
|
||||
|
||||
@@ -1819,6 +1819,9 @@ export const es: TranslationResources = {
|
||||
button: "Diagnóstico",
|
||||
refresh: "Refrescar",
|
||||
refreshing: "Refrescante...",
|
||||
copyLabel: "diagnóstico",
|
||||
copyAccessibility: "Copiar diagnóstico",
|
||||
copyFailed: "No se pudo copiar el diagnóstico",
|
||||
refreshAccessibility: "Actualizar diagnóstico",
|
||||
refreshingAccessibility: "Diagnóstico refrescante",
|
||||
running: "Ejecutando diagnóstico...",
|
||||
|
||||
@@ -1824,6 +1824,9 @@ export const fr: TranslationResources = {
|
||||
button: "Diagnostique",
|
||||
refresh: "Rafraîchir",
|
||||
refreshing: "Rafraîchissant...",
|
||||
copyLabel: "diagnostic",
|
||||
copyAccessibility: "Copier le diagnostic",
|
||||
copyFailed: "Échec de la copie du diagnostic",
|
||||
refreshAccessibility: "Actualiser le diagnostic",
|
||||
refreshingAccessibility: "Diagnostic rafraîchissant",
|
||||
running: "Exécution du diagnostic...",
|
||||
|
||||
@@ -1810,6 +1810,9 @@ export const ru: TranslationResources = {
|
||||
button: "Диагностика",
|
||||
refresh: "Обновить",
|
||||
refreshing: "Освежающий...",
|
||||
copyLabel: "диагностика",
|
||||
copyAccessibility: "Скопировать диагностику",
|
||||
copyFailed: "Не удалось скопировать диагностику",
|
||||
refreshAccessibility: "Обновить диагностику",
|
||||
refreshingAccessibility: "Обновление диагностики",
|
||||
running: "Запускаю диагностику...",
|
||||
|
||||
@@ -1755,6 +1755,9 @@ export const zhCN: TranslationResources = {
|
||||
button: "诊断",
|
||||
refresh: "刷新",
|
||||
refreshing: "正在刷新...",
|
||||
copyLabel: "诊断",
|
||||
copyAccessibility: "复制诊断",
|
||||
copyFailed: "复制诊断失败",
|
||||
refreshAccessibility: "刷新诊断",
|
||||
refreshingAccessibility: "正在刷新诊断",
|
||||
running: "正在运行诊断...",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { ClaudeSidechainTracker } from "./sidechain-tracker.js";
|
||||
import { buildClaudeFeatures, claudeModelSupportsFastMode } from "./feature-definitions.js";
|
||||
import {
|
||||
buildBinaryDiagnosticRows,
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
formatDiagnosticStatus,
|
||||
formatProviderDiagnostic,
|
||||
formatProviderDiagnosticError,
|
||||
@@ -1494,6 +1495,9 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
|
||||
return {
|
||||
diagnostic: formatProviderDiagnostic("Claude Code", [
|
||||
...(await buildCommandResolutionDiagnosticRows(launch, {
|
||||
knownBinaryNames: ["claude"],
|
||||
})),
|
||||
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||
...(auth ? [{ label: "Auth", value: auth }] : []),
|
||||
{ label: "Models", value: modelsValue },
|
||||
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
formatProviderDiagnostic,
|
||||
formatProviderDiagnosticError,
|
||||
buildBinaryDiagnosticRows,
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
resolveBinaryVersion,
|
||||
toDiagnosticErrorMessage,
|
||||
} from "./diagnostic-utils.js";
|
||||
@@ -5606,6 +5607,9 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const availability = await checkCodexLaunchAvailable(launch);
|
||||
const available = availability.available;
|
||||
const entries: Array<{ label: string; value: string }> = [
|
||||
...(await buildCommandResolutionDiagnosticRows(launch, {
|
||||
knownBinaryNames: ["codex"],
|
||||
})),
|
||||
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||
];
|
||||
let status = formatDiagnosticStatus(available);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
formatProviderDiagnostic,
|
||||
formatProviderDiagnosticError,
|
||||
buildBinaryDiagnosticRows,
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
toDiagnosticErrorMessage,
|
||||
} from "./diagnostic-utils.js";
|
||||
|
||||
@@ -127,6 +128,9 @@ export class CopilotACPAgentClient extends ACPAgentClient {
|
||||
|
||||
return {
|
||||
diagnostic: formatProviderDiagnostic("Copilot", [
|
||||
...(await buildCommandResolutionDiagnosticRows(launch, {
|
||||
knownBinaryNames: ["copilot"],
|
||||
})),
|
||||
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||
{ label: "Models", value: modelsValue },
|
||||
{ label: "Status", value: status },
|
||||
|
||||
@@ -1,6 +1,131 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import { toDiagnosticErrorMessage } from "./diagnostic-utils.js";
|
||||
import {
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
toDiagnosticErrorMessage,
|
||||
} from "./diagnostic-utils.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (dir) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function makeTempDir(): string {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "paseo-diagnostic-path-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writeExecutable(dir: string, name: string): string {
|
||||
const filePath = path.join(dir, name);
|
||||
writeFileSync(filePath, "#!/bin/sh\nexit 0\n");
|
||||
if (process.platform !== "win32") {
|
||||
chmodSync(filePath, 0o755);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
describe("buildCommandResolutionDiagnosticRows", () => {
|
||||
test("reports daemon PATH matches for known binary names", async () => {
|
||||
const binDir = makeTempDir();
|
||||
const binaryPath = writeExecutable(binDir, "claude");
|
||||
|
||||
const rows = await buildCommandResolutionDiagnosticRows(
|
||||
{ command: "claude", args: [], source: "default" },
|
||||
{
|
||||
knownBinaryNames: ["claude"],
|
||||
pathValue: binDir,
|
||||
},
|
||||
);
|
||||
|
||||
expect(rows).toContainEqual({ label: "Command source", value: "default" });
|
||||
expect(rows).toContainEqual({ label: "Configured command", value: "claude" });
|
||||
expect(rows).toContainEqual({
|
||||
label: "Daemon PATH",
|
||||
value: expect.stringContaining(binDir),
|
||||
});
|
||||
expect(rows).toContainEqual({
|
||||
label: "PATH matches",
|
||||
value: expect.stringContaining(binaryPath),
|
||||
});
|
||||
if (process.platform === "win32") {
|
||||
expect(rows.some((row) => row.label === "where.exe claude")).toBe(true);
|
||||
expect(rows.some((row) => row.label === "powershell Get-Command -All claude")).toBe(true);
|
||||
} else {
|
||||
expect(rows.some((row) => row.label === "which -a claude")).toBe(true);
|
||||
expect(rows.some((row) => row.label.endsWith(" -lc type -a claude"))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("reports none when the daemon PATH has no matching executable", async () => {
|
||||
const binDir = makeTempDir();
|
||||
|
||||
const rows = await buildCommandResolutionDiagnosticRows(
|
||||
{ command: "claude", args: [], source: "default" },
|
||||
{ knownBinaryNames: ["claude"], includeCommandProbes: false, pathValue: binDir },
|
||||
);
|
||||
|
||||
expect(rows).toContainEqual({ label: "PATH matches", value: "none" });
|
||||
});
|
||||
|
||||
test("truncates very long daemon PATH values", async () => {
|
||||
const rows = await buildCommandResolutionDiagnosticRows(
|
||||
{ command: "claude", args: [], source: "default" },
|
||||
{
|
||||
knownBinaryNames: ["claude"],
|
||||
includeCommandProbes: false,
|
||||
pathValue: "x".repeat(5000),
|
||||
},
|
||||
);
|
||||
|
||||
expect(rows).toContainEqual({
|
||||
label: "Daemon PATH",
|
||||
value: expect.stringContaining("(truncated)"),
|
||||
});
|
||||
});
|
||||
|
||||
test("matches Windows PATHEXT executable names", async () => {
|
||||
const binDir = makeTempDir();
|
||||
const binaryPath = writeExecutable(binDir, "claude.EXE");
|
||||
|
||||
const rows = await buildCommandResolutionDiagnosticRows(
|
||||
{ command: "claude", args: [], source: "default" },
|
||||
{
|
||||
knownBinaryNames: ["claude"],
|
||||
includeCommandProbes: false,
|
||||
pathext: ".EXE;.CMD",
|
||||
pathValue: binDir,
|
||||
platform: "win32",
|
||||
},
|
||||
);
|
||||
|
||||
expect(rows).toContainEqual({
|
||||
label: "PATH matches",
|
||||
value: expect.stringContaining(binaryPath),
|
||||
});
|
||||
});
|
||||
|
||||
test("does not treat absolute configured commands as PATH binary names", async () => {
|
||||
const rows = await buildCommandResolutionDiagnosticRows(
|
||||
{ command: "/Users/mn/.local/bin/claude", args: [], source: "override" },
|
||||
{
|
||||
knownBinaryNames: ["/Users/mn/.local/bin/claude"],
|
||||
includeCommandProbes: false,
|
||||
},
|
||||
);
|
||||
|
||||
expect(rows).toContainEqual({ label: "PATH matches", value: "not checked" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("toDiagnosticErrorMessage", () => {
|
||||
test("returns message for plain Error", () => {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { constants } from "node:fs";
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
createProviderEnvSpec,
|
||||
type ProviderLaunchAvailability,
|
||||
@@ -154,6 +158,232 @@ export interface BinaryDiagnosticRowsOptions {
|
||||
versionCommand?: BinaryDiagnosticVersionCommand;
|
||||
}
|
||||
|
||||
export interface CommandResolutionDiagnosticRowsOptions {
|
||||
knownBinaryNames: readonly string[];
|
||||
includeCommandProbes?: boolean;
|
||||
pathValue?: string;
|
||||
pathext?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
shell?: string;
|
||||
}
|
||||
|
||||
const COMMAND_PROBE_TIMEOUT_MS = 3_000;
|
||||
const COMMAND_PROBE_MAX_BUFFER = 32 * 1024;
|
||||
|
||||
function resolvePlatform(options?: CommandResolutionDiagnosticRowsOptions): NodeJS.Platform {
|
||||
return options?.platform ?? process.platform;
|
||||
}
|
||||
|
||||
function resolvePathValue(options?: CommandResolutionDiagnosticRowsOptions): string {
|
||||
return options?.pathValue ?? process.env["PATH"] ?? process.env["Path"] ?? "";
|
||||
}
|
||||
|
||||
function resolveShellValue(options?: CommandResolutionDiagnosticRowsOptions): string {
|
||||
if (options?.shell) {
|
||||
return options.shell;
|
||||
}
|
||||
if (resolvePlatform(options) === "win32") {
|
||||
return process.env["ComSpec"] ?? "cmd.exe";
|
||||
}
|
||||
return process.env["SHELL"] ?? "/bin/sh";
|
||||
}
|
||||
|
||||
async function isExecutableFile(filePath: string, platform: NodeJS.Platform): Promise<boolean> {
|
||||
try {
|
||||
const candidate = await stat(filePath);
|
||||
if (!candidate.isFile()) {
|
||||
return false;
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return true;
|
||||
}
|
||||
await access(filePath, constants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSearchableNames(binaryNames: readonly string[]): string[] {
|
||||
return binaryNames.filter(
|
||||
(binaryName) =>
|
||||
binaryName.trim().length > 0 && !binaryName.includes("/") && !binaryName.includes("\\"),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveWindowsPathExt(options: CommandResolutionDiagnosticRowsOptions): string[] {
|
||||
const value = options.pathext ?? process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD";
|
||||
return value
|
||||
.split(";")
|
||||
.map((extension) => extension.trim())
|
||||
.filter(Boolean)
|
||||
.map((extension) => (extension.startsWith(".") ? extension : `.${extension}`));
|
||||
}
|
||||
|
||||
function resolveBinaryCandidateNames(
|
||||
binaryName: string,
|
||||
options: CommandResolutionDiagnosticRowsOptions,
|
||||
): string[] {
|
||||
if (resolvePlatform(options) !== "win32" || path.win32.extname(binaryName)) {
|
||||
return [binaryName];
|
||||
}
|
||||
return [binaryName, ...resolveWindowsPathExt(options).map((extension) => binaryName + extension)];
|
||||
}
|
||||
|
||||
async function formatPathMatches(options: CommandResolutionDiagnosticRowsOptions): Promise<string> {
|
||||
const binaryNames = options.knownBinaryNames;
|
||||
const searchableNames = resolveSearchableNames(binaryNames);
|
||||
|
||||
if (searchableNames.length === 0) {
|
||||
return "not checked";
|
||||
}
|
||||
|
||||
const pathDelimiter = resolvePlatform(options) === "win32" ? ";" : path.delimiter;
|
||||
const pathEntries = resolvePathValue(options).split(pathDelimiter).filter(Boolean);
|
||||
const matches: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const platform = resolvePlatform(options);
|
||||
|
||||
for (const directory of pathEntries) {
|
||||
for (const binaryName of searchableNames) {
|
||||
for (const candidateName of resolveBinaryCandidateNames(binaryName, options)) {
|
||||
const candidate = path.join(directory, candidateName);
|
||||
if (seen.has(candidate)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(candidate);
|
||||
if (await isExecutableFile(candidate, platform)) {
|
||||
matches.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches.length > 0 ? matches.join("\n ") : "none";
|
||||
}
|
||||
|
||||
function shellToken(value: string): string {
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function formatCommandProbeOutput(stdout: string, stderr: string): string {
|
||||
const sections: string[] = [];
|
||||
const trimmedStdout = truncateForDiagnostic(stdout);
|
||||
const trimmedStderr = truncateForDiagnostic(stderr);
|
||||
if (trimmedStdout.length > 0) {
|
||||
sections.push(trimmedStdout);
|
||||
}
|
||||
if (trimmedStderr.length > 0) {
|
||||
sections.push(`stderr: ${trimmedStderr}`);
|
||||
}
|
||||
return sections.length > 0 ? sections.join("\n") : "(no output)";
|
||||
}
|
||||
|
||||
function formatCommandProbeError(error: unknown): string {
|
||||
return toDiagnosticErrorMessage(error);
|
||||
}
|
||||
|
||||
async function runCommandProbe(command: string, args: string[]): Promise<string> {
|
||||
try {
|
||||
const { stdout, stderr } = await execCommand(command, args, {
|
||||
timeout: COMMAND_PROBE_TIMEOUT_MS,
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: COMMAND_PROBE_MAX_BUFFER,
|
||||
});
|
||||
return formatCommandProbeOutput(stdout, stderr);
|
||||
} catch (error) {
|
||||
return formatCommandProbeError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildPosixCommandProbeRows(binaryName: string): Promise<DiagnosticEntry[]> {
|
||||
const shell = resolveShellValue();
|
||||
const typeCommand = `type -a ${shellToken(binaryName)}`;
|
||||
return [
|
||||
{
|
||||
label: `which -a ${binaryName}`,
|
||||
value: await runCommandProbe("/usr/bin/which", ["-a", binaryName]),
|
||||
},
|
||||
{
|
||||
label: `${path.basename(shell)} -lc type -a ${binaryName}`,
|
||||
value: await runCommandProbe(shell, ["-lc", typeCommand]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function buildWindowsCommandProbeRows(binaryName: string): Promise<DiagnosticEntry[]> {
|
||||
const powershellCommand = [
|
||||
"$ErrorActionPreference = 'Continue';",
|
||||
`Get-Command -All ${JSON.stringify(binaryName)} |`,
|
||||
"Select-Object CommandType,Source,Name,Definition |",
|
||||
"Format-List",
|
||||
].join(" ");
|
||||
|
||||
return [
|
||||
{
|
||||
label: `where.exe ${binaryName}`,
|
||||
value: await runCommandProbe("where.exe", [binaryName]),
|
||||
},
|
||||
{
|
||||
label: `powershell Get-Command -All ${binaryName}`,
|
||||
value: await runCommandProbe("powershell.exe", [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
powershellCommand,
|
||||
]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function buildCommandProbeRows(binaryNames: readonly string[]): Promise<DiagnosticEntry[]> {
|
||||
const searchableNames = resolveSearchableNames(binaryNames);
|
||||
if (searchableNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: DiagnosticEntry[] = [];
|
||||
for (const binaryName of searchableNames) {
|
||||
rows.push(
|
||||
...(process.platform === "win32"
|
||||
? await buildWindowsCommandProbeRows(binaryName)
|
||||
: await buildPosixCommandProbeRows(binaryName)),
|
||||
);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export async function buildCommandResolutionDiagnosticRows(
|
||||
launch: ResolvedProviderLaunch,
|
||||
options: CommandResolutionDiagnosticRowsOptions,
|
||||
): Promise<DiagnosticEntry[]> {
|
||||
const includeCommandProbes = options.includeCommandProbes ?? true;
|
||||
return [
|
||||
{
|
||||
label: "Command source",
|
||||
value: launch.source,
|
||||
},
|
||||
{
|
||||
label: "Configured command",
|
||||
value: [launch.command, ...launch.args].join(" "),
|
||||
},
|
||||
{
|
||||
label: "Daemon PATH",
|
||||
value: truncateForDiagnostic(resolvePathValue(options)) || "(empty)",
|
||||
},
|
||||
{
|
||||
label: "Daemon shell",
|
||||
value: resolveShellValue(options),
|
||||
},
|
||||
{
|
||||
label: "PATH matches",
|
||||
value: await formatPathMatches(options),
|
||||
},
|
||||
...(includeCommandProbes ? await buildCommandProbeRows(options.knownBinaryNames) : []),
|
||||
];
|
||||
}
|
||||
|
||||
async function resolveCommandVersion(invocation: BinaryDiagnosticVersionCommand): Promise<string> {
|
||||
try {
|
||||
const { stdout, stderr } = await execCommand(invocation.command, invocation.args, {
|
||||
|
||||
@@ -71,6 +71,7 @@ import {
|
||||
formatProviderDiagnostic,
|
||||
formatProviderDiagnosticError,
|
||||
buildBinaryDiagnosticRows,
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
toDiagnosticErrorMessage,
|
||||
} from "./diagnostic-utils.js";
|
||||
import { runProviderTurn } from "./provider-runner.js";
|
||||
@@ -1609,6 +1610,9 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
|
||||
return {
|
||||
diagnostic: formatProviderDiagnostic("OpenCode", [
|
||||
...(await buildCommandResolutionDiagnosticRows(launch, {
|
||||
knownBinaryNames: ["opencode"],
|
||||
})),
|
||||
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||
{ label: "Server", value: serverStatus },
|
||||
{ label: "Auth", value: authValue },
|
||||
|
||||
@@ -45,6 +45,7 @@ import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
||||
import { composeSystemPromptParts } from "../../system-prompt.js";
|
||||
import {
|
||||
buildBinaryDiagnosticRows,
|
||||
buildCommandResolutionDiagnosticRows,
|
||||
formatDiagnosticStatus,
|
||||
formatProviderDiagnostic,
|
||||
formatProviderDiagnosticError,
|
||||
@@ -2062,6 +2063,9 @@ export class PiRpcAgentClient implements AgentClient {
|
||||
|
||||
return {
|
||||
diagnostic: formatProviderDiagnostic("Pi", [
|
||||
...(await buildCommandResolutionDiagnosticRows(launch, {
|
||||
knownBinaryNames: [launch.command],
|
||||
})),
|
||||
...(await buildBinaryDiagnosticRows(launch, availability)),
|
||||
{ label: "Configured providers", value: configuredProvidersValue },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user