Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 274f043681 fix(settings): truncate long Agent CLI paths in AI cards
The settings content column is a row flex item, so a long CLI path
set its min-content width and pushed the pane past the window. Let
that column shrink, and ellipsize the path on each managed agent card
while keeping the version visible.

Fixes #3923

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-25 06:29:30 +00:00
6 changed files with 158 additions and 25 deletions

No files matched your search

+2 -1
View File
@@ -523,7 +523,8 @@ const SettingsPageContent: React.FC<{ settings: SettingsState; appLock?: AppLock
</TabsList>
</div>
<div className="flex-1 h-full flex flex-col min-h-0 bg-muted/10">
{/* min-w-0: row flex item must shrink so long Agent paths truncate inside the pane. */}
<div className="flex-1 h-full flex flex-col min-h-0 min-w-0 bg-muted/10">
{mountedTabs.has("application") && (
<SettingsLazyTab value="application">
<LazySettingsApplicationTab
@@ -77,13 +77,13 @@ export const ClaudeCodeCard: React.FC<{
</div>
{found && (
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">{t('ai.claude.path')}</span>
<span className="font-mono text-foreground truncate">{pathInfo.path}</span>
<div className="flex items-center gap-2 text-xs min-w-0">
<span className="text-muted-foreground shrink-0">{t('ai.claude.path')}</span>
<span className="font-mono text-foreground min-w-0 flex-1 truncate" title={pathInfo.path ?? undefined}>{pathInfo.path}</span>
{pathInfo.version && (
<>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{pathInfo.version}</span>
<span className="text-muted-foreground shrink-0">|</span>
<span className="text-muted-foreground shrink-0">{pathInfo.version}</span>
</>
)}
</div>
@@ -102,7 +102,7 @@ export const ClaudeCodeCard: React.FC<{
value={customPath}
onChange={(e) => onCustomPathChange(e.target.value)}
placeholder={t('ai.claude.customPathPlaceholder')}
className="flex-1 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="flex-1 min-w-0 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={!customPath.trim()}>
<RefreshCw size={14} className="mr-1.5" />
@@ -117,13 +117,13 @@ export const CodebuddyCard: React.FC<{
</div>
{found && (
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">{t('ai.codebuddy.path')}</span>
<span className="font-mono text-foreground truncate">{pathInfo.path}</span>
<div className="flex items-center gap-2 text-xs min-w-0">
<span className="text-muted-foreground shrink-0">{t('ai.codebuddy.path')}</span>
<span className="font-mono text-foreground min-w-0 flex-1 truncate" title={pathInfo.path ?? undefined}>{pathInfo.path}</span>
{pathInfo.version && (
<>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{pathInfo.version}</span>
<span className="text-muted-foreground shrink-0">|</span>
<span className="text-muted-foreground shrink-0">{pathInfo.version}</span>
</>
)}
</div>
@@ -142,7 +142,7 @@ export const CodebuddyCard: React.FC<{
value={customPath}
onChange={(e) => onCustomPathChange(e.target.value)}
placeholder={t('ai.codebuddy.customPathPlaceholder')}
className="flex-1 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="flex-1 min-w-0 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={!customPath.trim()}>
<RefreshCw size={14} className="mr-1.5" />
@@ -108,13 +108,13 @@ export const CodexConnectionCard: React.FC<{
</div>
{found && (
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">{t('ai.codex.path')}</span>
<span className="font-mono text-foreground truncate">{pathInfo.path}</span>
<div className="flex items-center gap-2 text-xs min-w-0">
<span className="text-muted-foreground shrink-0">{t('ai.codex.path')}</span>
<span className="font-mono text-foreground min-w-0 flex-1 truncate" title={pathInfo.path ?? undefined}>{pathInfo.path}</span>
{pathInfo.version && (
<>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{pathInfo.version}</span>
<span className="text-muted-foreground shrink-0">|</span>
<span className="text-muted-foreground shrink-0">{pathInfo.version}</span>
</>
)}
</div>
@@ -133,7 +133,7 @@ export const CodexConnectionCard: React.FC<{
value={customPath}
onChange={(e) => onCustomPathChange(e.target.value)}
placeholder={t('ai.codex.customPathPlaceholder')}
className="flex-1 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="flex-1 min-w-0 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={!customPath.trim()}>
<RefreshCw size={14} className="mr-1.5" />
@@ -61,13 +61,13 @@ export const CopilotCliCard: React.FC<{
</div>
{found && (
<div className="flex items-center gap-2 text-xs">
<span className="text-muted-foreground">{t(`${i18nPrefix}.path`)}</span>
<span className="font-mono text-foreground truncate">{pathInfo.path}</span>
<div className="flex items-center gap-2 text-xs min-w-0">
<span className="text-muted-foreground shrink-0">{t(`${i18nPrefix}.path`)}</span>
<span className="font-mono text-foreground min-w-0 flex-1 truncate" title={pathInfo.path ?? undefined}>{pathInfo.path}</span>
{pathInfo.version && (
<>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{pathInfo.version}</span>
<span className="text-muted-foreground shrink-0">|</span>
<span className="text-muted-foreground shrink-0">{pathInfo.version}</span>
</>
)}
</div>
@@ -87,7 +87,7 @@ export const CopilotCliCard: React.FC<{
value={customPath}
onChange={(e) => onCustomPathChange(e.target.value)}
placeholder={t(`${i18nPrefix}.customPathPlaceholder`)}
className="flex-1 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
className="flex-1 min-w-0 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
)}
<Button variant="outline" size="sm" onClick={onRecheckPath} disabled={!allowEmptyCheck && !customPath.trim()}>
@@ -0,0 +1,132 @@
import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { ClaudeCodeCard } from "./ClaudeCodeCard";
import { CodebuddyCard } from "./CodebuddyCard";
import { CodexConnectionCard } from "./CodexConnectionCard";
import { CopilotCliCard } from "./CopilotCliCard";
const LONG_PATH = "C:\\Users\\70541\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Anthropic.ClaudeCode_Microsoft.Winget.Source_8wekyb3d8bbwe\\claude.exe";
const VERSION = "9.9.9-test";
function classList(tag: string): Set<string> {
const match = tag.match(/class="([^"]*)"/);
assert.ok(match, `missing class on ${tag}`);
return new Set(match[1].split(/\s+/));
}
function assertClasses(tag: string, expected: string[]) {
const have = classList(tag);
for (const name of expected) {
assert.ok(have.has(name), `expected ${name} on ${tag}`);
}
}
function openingTagContaining(markup: string, text: string): string {
const idx = markup.indexOf(`>${text}<`);
assert.ok(idx >= 0, `missing text node ${text}`);
const start = markup.lastIndexOf("<", idx);
return markup.slice(start, idx + 1);
}
function pathRowClass(markup: string): string {
const idx = markup.indexOf(LONG_PATH);
assert.ok(idx >= 0, "missing path text");
const matches = [...markup.slice(0, idx).matchAll(/<div\b[^>]*class="([^"]*)"/g)];
assert.ok(matches.length > 0, "missing path row");
return matches[matches.length - 1][1];
}
function assertLongPathTruncates(markup: string) {
const pathTag = openingTagContaining(markup, LONG_PATH);
assertClasses(pathTag, ["truncate", "min-w-0", "flex-1"]);
assert.match(pathTag, new RegExp(`title="${LONG_PATH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`));
assert.ok(pathRowClass(markup).split(/\s+/).includes("min-w-0"), "path row missing min-w-0");
const versionTag = openingTagContaining(markup, VERSION);
assertClasses(versionTag, ["shrink-0"]);
const input = markup.match(/<input\b[^>]*>/);
assert.ok(input, "missing custom path input");
assertClasses(input[0], ["flex-1", "min-w-0"]);
assert.match(input[0], new RegExp(`value="${LONG_PATH.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`));
}
test("Claude Code card truncates a long CLI path inside the card", () => {
const markup = renderToStaticMarkup(
<ClaudeCodeCard
pathInfo={{ path: LONG_PATH, version: VERSION, available: true }}
isResolvingPath={false}
customPath={LONG_PATH}
onCustomPathChange={() => {}}
onRecheckPath={() => {}}
onResetPath={() => {}}
configDir=""
onConfigDirChange={() => {}}
settingsPath=""
onSettingsPathChange={() => {}}
envText=""
onEnvTextChange={() => {}}
/>,
);
assertLongPathTruncates(markup);
});
test("Codex card truncates a long CLI path inside the card", () => {
const markup = renderToStaticMarkup(
<CodexConnectionCard
pathInfo={{ path: LONG_PATH, version: VERSION, available: true }}
isResolvingPath={false}
customPath={LONG_PATH}
onCustomPathChange={() => {}}
onRecheckPath={() => {}}
onResetPath={() => {}}
integration={null}
loginSession={null}
isLoading={false}
error={null}
onRefresh={() => {}}
onConnect={() => {}}
onCancel={() => {}}
onOpenUrl={() => {}}
onLogout={() => {}}
appServerRuntime="sdk"
appServerStatus={null}
onAppServerRuntimeChange={() => {}}
/>,
);
assertLongPathTruncates(markup);
});
test("Codebuddy card truncates a long CLI path inside the card", () => {
const markup = renderToStaticMarkup(
<CodebuddyCard
pathInfo={{ path: LONG_PATH, version: VERSION, available: true }}
isResolvingPath={false}
customPath={LONG_PATH}
onCustomPathChange={() => {}}
onRecheckPath={() => {}}
onResetPath={() => {}}
internetEnv=""
onInternetEnvChange={() => {}}
envText=""
onEnvTextChange={() => {}}
/>,
);
assertLongPathTruncates(markup);
});
test("Copilot CLI card truncates a long CLI path inside the card", () => {
const markup = renderToStaticMarkup(
<CopilotCliCard
pathInfo={{ path: LONG_PATH, version: VERSION, available: true }}
isResolvingPath={false}
customPath={LONG_PATH}
onCustomPathChange={() => {}}
onRecheckPath={() => {}}
onResetPath={() => {}}
/>,
);
assertLongPathTruncates(markup);
});