Skip to content
6 changes: 5 additions & 1 deletion ui/goose2/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,11 @@ export function AgentsView() {
>
<AlertDialogContent className="max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>{t("view.deleteTitle")}</AlertDialogTitle>
<AlertDialogTitle>
{t("view.deleteTitle", {
name: deletingPersona?.displayName ?? "",
})}
</AlertDialogTitle>
<AlertDialogDescription>
{t("view.deleteDescription", {
name: deletingPersona?.displayName ?? "",
Expand Down
45 changes: 45 additions & 0 deletions ui/goose2/src/features/chat/lib/chatInputPlaceholder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import {
getChatInputAgentLabel,
getChatInputPlaceholder,
} from "./chatInputPlaceholder";

const t = (key: string, options?: { agent: string }) =>
options?.agent ? `${key}:${options.agent}` : key;

describe("getChatInputAgentLabel", () => {
it("uses the active persona display name when present", () => {
expect(getChatInputAgentLabel("Reviewer", "Goose")).toBe("Reviewer");
});

it("falls back to the provider display name", () => {
expect(getChatInputAgentLabel(undefined, "Goose")).toBe("Goose");
});

it("preserves explicit persona names with the default suffix", () => {
expect(getChatInputAgentLabel("Ops (Default)", "Goose (Default)")).toBe(
"Ops (Default)",
);
});

it("removes the default suffix from provider fallback labels", () => {
expect(getChatInputAgentLabel(undefined, "Goose (Default)")).toBe("Goose");
});
});

describe("getChatInputPlaceholder", () => {
it("uses the agent label in the default placeholder", () => {
expect(getChatInputPlaceholder(t, "Goose", false, false)).toBe(
"input.placeholder:Goose",
);
});

it("uses voice status placeholders while recording or transcribing", () => {
expect(getChatInputPlaceholder(t, "Goose", true, false)).toBe(
"toolbar.voiceInputRecording",
);
expect(getChatInputPlaceholder(t, "Goose", false, true)).toBe(
"toolbar.voiceInputTranscribing",
);
});
});
15 changes: 15 additions & 0 deletions ui/goose2/src/features/chat/lib/chatInputPlaceholder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
const DEFAULT_LABEL_SUFFIX = " (Default)";

export function getChatInputAgentLabel(
personaDisplayName: string | undefined,
providerDisplayName: string,
): string {
if (personaDisplayName) {
return personaDisplayName;
}

return providerDisplayName.endsWith(DEFAULT_LABEL_SUFFIX)
? providerDisplayName.slice(0, -DEFAULT_LABEL_SUFFIX.length)
: providerDisplayName;
}

export function getChatInputPlaceholder(
t: (key: string, options?: { agent: string }) => string,
agent: string,
Expand Down
12 changes: 9 additions & 3 deletions ui/goose2/src/features/chat/lib/newChat.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import type { Message } from "@/shared/types/messages";
import { findExistingDraft } from "./newChat";
import type { ChatSession } from "../stores/chatSessionStore";

Expand All @@ -8,7 +9,7 @@ function makeSession(
): ChatSession {
return {
id,
title: "New Chat",
title: "New chat",
createdAt: "2026-04-01T00:00:00.000Z",
updatedAt: "2026-04-01T00:00:00.000Z",
messageCount: 0,
Expand All @@ -30,7 +31,7 @@ describe("findExistingDraft", () => {
draftsBySession: { "alpha-draft": "alpha draft" },
messagesBySession: {},
request: {
title: "New Chat",
title: "New chat",
projectId: "alpha",
},
}),
Expand Down Expand Up @@ -111,7 +112,12 @@ describe("findExistingDraft", () => {
draftsBySession: {},
messagesBySession: {
"alpha-session": [
{ id: "msg-1", role: "user", content: "hello" } as any,
{
id: "msg-1",
role: "user",
created: 1,
content: [{ type: "text", text: "hello" }],
} satisfies Message,
],
},
request: {
Expand Down
6 changes: 6 additions & 0 deletions ui/goose2/src/features/chat/lib/sessionTitle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getDisplaySessionTitle,
getEditableSessionTitle,
getSessionTitleFromDraft,
isDefaultChatTitle,
isSessionTitleUnchanged,
} from "./sessionTitle";

Expand All @@ -17,6 +18,11 @@ describe("sessionTitle", () => {
);
});

it("treats the ACP title-case default title as the default title", () => {
expect(isDefaultChatTitle("New Chat")).toBe(true);
expect(getDisplaySessionTitle("New Chat", "Nuevo chat")).toBe("Nuevo chat");
});

it("treats the localized default title as unchanged while the sentinel is still internal", () => {
expect(
isSessionTitleUnchanged("Nuevo chat", DEFAULT_CHAT_TITLE, "Nuevo chat"),
Expand Down
13 changes: 11 additions & 2 deletions ui/goose2/src/features/chat/lib/sessionTitle.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { ChatAttachmentDraft } from "@/shared/types/messages";

export const DEFAULT_CHAT_TITLE = "New Chat";
export const DEFAULT_CHAT_TITLE = "New chat";
const ACP_DEFAULT_CHAT_TITLE = "New Chat";

export function isDefaultChatTitle(title: string): boolean {
return title === DEFAULT_CHAT_TITLE;
return title === DEFAULT_CHAT_TITLE || title === ACP_DEFAULT_CHAT_TITLE;
}

function attachmentKindLabel(kind: ChatAttachmentDraft["kind"], count: number) {
Expand All @@ -17,6 +18,14 @@ function attachmentKindLabel(kind: ChatAttachmentDraft["kind"], count: number) {
}
}

// The goose ACP backend uses "New Chat" (title case) as its default — normalize to ours.
export function normalizeAcpTitle(
title: string | null | undefined,
): string | undefined {
if (!title) return undefined;
return title === ACP_DEFAULT_CHAT_TITLE ? DEFAULT_CHAT_TITLE : title;
}

export function getSessionTitleFromDraft(
text: string,
attachments?: ChatAttachmentDraft[],
Expand Down
7 changes: 5 additions & 2 deletions ui/goose2/src/features/chat/stores/chatSessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
type AcpSessionInfo,
} from "@/shared/api/acp";
import type { Session } from "@/shared/types/chat";
import { DEFAULT_CHAT_TITLE } from "@/features/chat/lib/sessionTitle";
import {
DEFAULT_CHAT_TITLE,
normalizeAcpTitle,
} from "@/features/chat/lib/sessionTitle";
import {
archiveSession as acpArchiveSession,
unarchiveSession as acpUnarchiveSession,
Expand Down Expand Up @@ -97,7 +100,7 @@ function acpSessionToChatSession(session: AcpSessionInfo): ChatSession {
return {
id: session.sessionId,
acpSessionId: session.sessionId,
title: session.title ?? "Untitled",
title: normalizeAcpTitle(session.title) ?? "Untitled",
projectId: session.projectId ?? undefined,
providerId: session.providerId ?? undefined,
personaId: session.personaId ?? undefined,
Expand Down
10 changes: 8 additions & 2 deletions ui/goose2/src/features/chat/ui/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
attachmentSnapshotsMatch,
skillDraftSnapshotsMatch,
} from "../lib/chatInputSnapshots";
import { getChatInputPlaceholder } from "../lib/chatInputPlaceholder";
import {
getChatInputAgentLabel,
getChatInputPlaceholder,
} from "../lib/chatInputPlaceholder";
import { cn } from "@/shared/lib/cn";
import { Badge } from "@/shared/ui/badge";
import { Popover, PopoverAnchor } from "@/shared/ui/popover";
Expand Down Expand Up @@ -330,7 +333,10 @@ export function ChatInput({
const providerDisplayName =
providers.find((provider) => provider.id === selectedProvider)?.label ??
formatProviderLabel(selectedProvider);
const agentDisplayName = activePersona?.displayName ?? providerDisplayName;
const agentDisplayName = getChatInputAgentLabel(
activePersona?.displayName,
providerDisplayName,
);
const resolvedCurrentModel = useMemo(() => {
if (currentModel) {
return currentModel;
Expand Down
4 changes: 1 addition & 3 deletions ui/goose2/src/features/chat/ui/__tests__/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ describe("ChatInput", () => {
it("renders with default placeholder", () => {
render(<ChatInput onSend={vi.fn()} />);
expect(
screen.getByPlaceholderText(
"Message Goose, @ to mention agents or skills",
),
screen.getByPlaceholderText("Chat with Goose or @ mention an agent"),
).toBeInTheDocument();
});

Expand Down
4 changes: 1 addition & 3 deletions ui/goose2/src/features/chat/ui/__tests__/FilesList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ describe("FilesList", () => {
render(<FilesList />);

expect(
screen.getByText(
"Project files are unavailable until a project with working directories is assigned.",
),
screen.getByText("Files will show here after you assign a project."),
).toBeInTheDocument();
});

Expand Down
4 changes: 1 addition & 3 deletions ui/goose2/src/features/home/ui/HomeScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,7 @@ describe("HomeScreen", () => {
it("renders the chat input placeholder with default agent name when no persona selected", () => {
renderHome();
expect(
screen.getByPlaceholderText(
"Message Goose, @ to mention agents or skills",
),
screen.getByPlaceholderText("Chat with Goose or @ mention an agent"),
).toBeInTheDocument();
});

Expand Down
6 changes: 5 additions & 1 deletion ui/goose2/src/features/projects/ui/ProjectsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,11 @@ export function ProjectsView({ onStartChat }: ProjectsViewProps) {
>
<AlertDialogContent className="max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>{t("view.deleteTitle")}</AlertDialogTitle>
<AlertDialogTitle>
{t("view.deleteTitle", {
name: deletingProject?.name ?? "",
})}
</AlertDialogTitle>
<AlertDialogDescription>
{t("view.deleteDescription", {
name: deletingProject?.name ?? "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ describe("CreateProjectDialog", () => {
/>,
);

expect(screen.getByText("Edit Project")).toBeInTheDocument();
expect(screen.getByText("Edit project")).toBeInTheDocument();
});

it("shows New Project title without editingProject", () => {
render(<CreateProjectDialog {...defaultProps} isOpen={true} />);

expect(screen.getByText("New Project")).toBeInTheDocument();
expect(screen.getByText("New project")).toBeInTheDocument();
});

it("populates the prompt editor with working dirs and prompt text", () => {
Expand Down
3 changes: 0 additions & 3 deletions ui/goose2/src/features/settings/ui/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,6 @@ export function AppearanceSettings() {
<h3 className="text-lg font-semibold font-display tracking-tight">
{t("appearance.title")}
</h3>
<p className="mt-1 text-sm text-muted-foreground">
{t("appearance.description")}
</p>

<Separator className="my-4" />

Expand Down
14 changes: 6 additions & 8 deletions ui/goose2/src/features/settings/ui/DoctorCheckRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,19 @@ export function DoctorCheckRow({ check, onFixed }: DoctorCheckRowProps) {
<AlertDialogContent className="max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>{t("settings:doctor.runFix")}</AlertDialogTitle>
<AlertDialogDescription className="break-all font-mono">
{check.fixCommand}
<AlertDialogDescription>
{t("settings:doctor.runFixDescription")}
</AlertDialogDescription>
<code className="block break-all rounded bg-muted px-3 py-2 font-mono text-xs">
{check.fixCommand}
</code>
</AlertDialogHeader>
{fixError && <p className="text-xs text-destructive">{fixError}</p>}
<AlertDialogFooter>
<AlertDialogCancel disabled={fixing}>
{t("common:actions.cancel")}
</AlertDialogCancel>
<Button
variant="outline"
size="sm"
disabled={fixing}
onClick={confirmFix}
>
<Button disabled={fixing} onClick={confirmFix}>
{fixing && <Loader2 className="h-3 w-3 animate-spin" />}
{fixing
? t("common:actions.running")
Expand Down
3 changes: 0 additions & 3 deletions ui/goose2/src/features/settings/ui/DoctorSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@ export function DoctorSettings() {
<h3 className="text-lg font-semibold font-display tracking-tight">
{t("doctor.title")}
</h3>
<p className="mt-1 text-sm text-muted-foreground">
{t("doctor.description")}
</p>
</div>

<div className="flex flex-shrink-0 items-center gap-2">
Expand Down
1 change: 1 addition & 0 deletions ui/goose2/src/features/settings/ui/ModelProviderRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ export function ModelProviderRow({
const fieldSetupDescription = getFieldSetupDescription(
provider.setupMethod,
t,
provider.fields,
);

if (loadingConfig && hasFields) {
Expand Down
3 changes: 0 additions & 3 deletions ui/goose2/src/features/settings/ui/ProvidersSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,6 @@ export function ProvidersSettings() {
<h3 className="text-lg font-semibold font-display tracking-tight">
{t("providers.title")}
</h3>
<p className="mt-1 text-sm text-muted-foreground">
{t("providers.description")}
</p>

<Separator className="my-4" />

Expand Down
6 changes: 5 additions & 1 deletion ui/goose2/src/features/settings/ui/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,11 @@ export function SettingsModal({
>
<AlertDialogContent className="max-w-sm">
<AlertDialogHeader>
<AlertDialogTitle>{t("deleteProject.title")}</AlertDialogTitle>
<AlertDialogTitle>
{t("deleteProject.title", {
name: deletingProject?.name ?? "",
})}
</AlertDialogTitle>
<AlertDialogDescription>
{t("deleteProject.description", {
name: deletingProject?.name ?? "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { getFieldSetupDescription } from "../modelProviderHelpers";

const t = (key: string) => key;

describe("getFieldSetupDescription", () => {
it("uses single API key copy for config fields with one required secret API key", () => {
expect(
getFieldSetupDescription("config_fields", t, [
{
key: "OPENAI_API_KEY",
label: "API Key",
secret: true,
required: true,
placeholder: "Paste your API key",
},
]),
).toBe("providers.models.setup.fieldDescription.singleApiKey");
});

it("uses generic config fields copy for config fields without an API key field", () => {
expect(
getFieldSetupDescription("config_fields", t, [
{
key: "OLLAMA_HOST",
label: "Host",
secret: false,
required: true,
placeholder: "localhost or http://localhost:11434",
},
]),
).toBe("providers.models.setup.fieldDescription.configFields");
});
});
Loading
Loading