Clean up types (#9057)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-05-07 09:38:37 -04:00
committed by GitHub
parent 64f795f82c
commit bced4ea5ec
21 changed files with 123 additions and 262 deletions
@@ -141,11 +141,8 @@ describe("useChat attachments", () => {
{ type: "text", text: "" },
{
type: "image",
source: {
type: "base64",
mediaType: "image/png",
data: "abc123",
},
data: "abc123",
mimeType: "image/png",
},
]);
expect(mockAcpSendMessage).toHaveBeenCalledWith("session-1", " ", {
+3 -6
View File
@@ -94,7 +94,7 @@ function markMessageStopped(sessionId: string, messageId: string) {
completionStatus: "stopped",
},
content: message.content.map((block) =>
block.type === "toolRequest" && block.status === "executing"
block.type === "toolRequest" && block.status === "in_progress"
? { ...block, status: "stopped" }
: block,
),
@@ -215,11 +215,8 @@ export function useChat(
for (const img of images) {
userMessage.content.push({
type: "image",
source: {
type: "base64",
mediaType: img.mimeType,
data: img.base64,
},
data: img.base64,
mimeType: img.mimeType,
});
}
}
@@ -74,13 +74,13 @@ describe("getToolItemStatus", () => {
it("treats response.isError as error", () => {
expect(getToolItemStatus(pair("a", { name: "x" }, { isError: true }))).toBe(
"error",
"failed",
);
});
it("uses request status when no response yet", () => {
expect(getToolItemStatus(pair("a", { status: "executing" }))).toBe(
"executing",
expect(getToolItemStatus(pair("a", { status: "in_progress" }))).toBe(
"in_progress",
);
});
});
@@ -89,17 +89,17 @@ describe("getChainAggregateStatus", () => {
it("prefers error over pending and executing", () => {
expect(
getChainAggregateStatus([
pair("a", { status: "executing" }),
pair("a", { status: "in_progress" }),
pair("b", { name: "x" }, { isError: true }),
pair("c", { status: "pending" }),
]),
).toBe("error");
).toBe("failed");
});
it("prefers stopped over executing/pending when there is no error", () => {
expect(
getChainAggregateStatus([
pair("a", { status: "executing" }),
pair("a", { status: "in_progress" }),
pair("b", { status: "stopped" }),
]),
).toBe("stopped");
@@ -109,9 +109,9 @@ describe("getChainAggregateStatus", () => {
expect(
getChainAggregateStatus([
pair("a", { name: "x" }, {}),
pair("b", { status: "executing" }),
pair("b", { status: "in_progress" }),
]),
).toBe("executing");
).toBe("in_progress");
});
it("returns pending when only pending is present", () => {
@@ -23,7 +23,7 @@ export function getToolItemName(item: ToolChainItem): string {
export function getToolItemStatus(item: ToolChainItem): ToolCallStatus {
if (item.response) {
return item.response.isError ? "error" : "completed";
return item.response.isError ? "failed" : "completed";
}
return item.request?.status ?? "completed";
}
@@ -35,10 +35,10 @@ export function getToolItemStatus(item: ToolChainItem): ToolCallStatus {
export function getChainAggregateStatus(
items: ToolChainItem[],
): ToolCallStatus {
if (items.some((i) => getToolItemStatus(i) === "error")) return "error";
if (items.some((i) => getToolItemStatus(i) === "failed")) return "failed";
if (items.some((i) => getToolItemStatus(i) === "stopped")) return "stopped";
if (items.some((i) => getToolItemStatus(i) === "executing"))
return "executing";
if (items.some((i) => getToolItemStatus(i) === "in_progress"))
return "in_progress";
if (items.some((i) => getToolItemStatus(i) === "pending")) return "pending";
return "completed";
}
@@ -3,8 +3,8 @@ import type { ToolPart } from "@/shared/ui/ai-elements/tool";
export const toolStatusMap: Record<ToolCallStatus, ToolPart["state"]> = {
pending: "input-streaming",
executing: "input-available",
in_progress: "input-available",
completed: "output-available",
error: "output-error",
failed: "output-error",
stopped: "output-denied",
};
@@ -219,10 +219,7 @@ function renderContentBlock(
}
case "image": {
const ic = content as ImageContent;
const src =
ic.source.type === "base64"
? `data:${ic.source.mediaType};base64,${ic.source.data}`
: ic.source.url;
const src = ic.uri ?? `data:${ic.mimeType};base64,${ic.data}`;
return (
<ClickableImage
key={`image-${index}`}
@@ -46,7 +46,7 @@ function useElapsedTime(status: ToolCallStatus, startedAt?: number) {
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
if (status === "executing") {
if (status === "in_progress") {
const origin = startedAt ?? Date.now();
setElapsed(Math.floor((Date.now() - origin) / 1000));
const interval = setInterval(() => {
@@ -282,7 +282,7 @@ export function ToolCallAdapter({
[args, name],
);
const elapsedSeconds =
status === "executing" && elapsed >= 3 ? elapsed : undefined;
status === "in_progress" && elapsed >= 3 ? elapsed : undefined;
const { resolveMarkdownHref, openResolvedPath } = useArtifactPolicyContext();
@@ -17,25 +17,25 @@ import type { ToolCallStatus } from "@/shared/types/messages";
export type { ToolChainItem };
const STEP_BULLET_ICON: Record<
Exclude<ToolCallStatus, "error" | "stopped">,
Exclude<ToolCallStatus, "failed" | "stopped">,
LucideIcon
> = {
pending: CircleIcon,
executing: ClockIcon,
in_progress: ClockIcon,
completed: Check,
};
const STEP_BULLET_CLASS: Record<
Exclude<ToolCallStatus, "error" | "stopped">,
Exclude<ToolCallStatus, "failed" | "stopped">,
string
> = {
pending: "text-muted-foreground/70",
executing: "text-muted-foreground animate-pulse",
in_progress: "text-muted-foreground animate-pulse",
completed: "text-muted-foreground",
};
function ChainStepBullet({ status }: { status: ToolCallStatus }) {
if (status === "error") {
if (status === "failed") {
return (
<span aria-hidden className="size-1.5 shrink-0 rounded-full bg-red-600" />
);
@@ -172,7 +172,7 @@ export function ToolChainCards({ toolItems }: { toolItems: ToolChainItem[] }) {
const aggregateStatus = getChainAggregateStatus(toolItems);
const summary = summarizeToolChainSteps(primaryItems);
const isActiveChain =
aggregateStatus === "executing" || aggregateStatus === "pending";
aggregateStatus === "in_progress" || aggregateStatus === "pending";
// Chains that mount as already-complete (history replay) start collapsed;
// live chains mount mid-execution, stay open while running, and auto-collapse
// once they finish so the chat keeps moving forward.
@@ -349,7 +349,7 @@ describe("MessageBubble", () => {
id: "tool-1",
name: "readFile",
arguments: { path: "/tmp/demo.txt" },
status: "executing",
status: "in_progress",
},
{
type: "toolResponse",
@@ -374,7 +374,7 @@ describe("MessageBubble", () => {
id: "tool-1",
name: "readFile",
arguments: {},
status: "executing",
status: "in_progress",
},
{
type: "toolResponse",
@@ -410,7 +410,7 @@ describe("MessageBubble", () => {
id: "tool-1",
name: "readFile",
arguments: {},
status: "executing",
status: "in_progress",
},
{
type: "toolResponse",
@@ -79,7 +79,7 @@ describe("ToolChainCards", () => {
toolItems={[
pair("Shell · npm test", { completed: true }),
pair("Shell · npm build", {
status: "executing",
status: "in_progress",
completed: false,
}),
]}
@@ -97,7 +97,7 @@ describe("ToolChainCards", () => {
toolItems={[
pair("Edit · src/a.ts"),
pair("Edit · src/b.ts", {
status: "executing",
status: "in_progress",
completed: false,
}),
]}
@@ -129,7 +129,7 @@ describe("ToolChainCards", () => {
it("auto-collapses a live chain once every step has completed", () => {
const a = pair("Edit · src/a.ts");
const bRequest = pair("Edit · src/b.ts", {
status: "executing",
status: "in_progress",
completed: false,
});
const { rerender } = render(<ToolChainCards toolItems={[a, bRequest]} />);
@@ -171,7 +171,7 @@ describe("ToolChainCards", () => {
/>,
);
const wrapper = container.querySelector('[data-role="tool-chain-card"]');
expect(wrapper?.getAttribute("data-status")).toBe("error");
expect(wrapper?.getAttribute("data-status")).toBe("failed");
});
it("renders a step rail row for each child inside a chain", () => {
@@ -181,7 +181,7 @@ describe("ToolChainCards", () => {
pair("Edit · src/a.ts"),
pair("Edit · src/b.ts"),
pair("Edit · src/c.ts", {
status: "executing",
status: "in_progress",
completed: false,
}),
]}
@@ -317,7 +317,7 @@ describe("ToolChainCards", () => {
it("does not surface the chain summary while the chain is still active", () => {
const a = pair("Edit · src/a.ts");
const b = pair("Edit · src/b.ts", {
status: "executing",
status: "in_progress",
completed: false,
});
if (a.request) {
@@ -14,10 +14,7 @@ export async function getProviderConfig(
): Promise<ProviderFieldValue[]> {
const client = await getClient();
const response = await client.goose.GooseProvidersConfigRead({ providerId });
return response.fields.map((field) => ({
...field,
value: field.value ?? null,
}));
return response.fields;
}
export async function saveProviderConfig(
@@ -239,7 +239,7 @@ describe("acpNotificationHandler", () => {
type: "toolRequest",
id: "tool-b",
name: "grep",
status: "executing",
status: "in_progress",
});
expect(message.content[2]).toMatchObject({
type: "toolResponse",
@@ -65,7 +65,7 @@ describe("ACP tool call status handling", () => {
expect(assistant?.content[0]).toMatchObject({
type: "toolRequest",
id: "tool-1",
status: "error",
status: "failed",
});
expect(assistant?.content[1]).toMatchObject({
type: "toolResponse",
@@ -114,7 +114,7 @@ describe("ACP tool call status handling", () => {
expect(message.content[0]).toMatchObject({
type: "toolRequest",
id: "tool-1",
status: "error",
status: "failed",
});
expect(message.content[1]).toMatchObject({
type: "toolResponse",
@@ -7,7 +7,6 @@ import { useChatSessionStore } from "@/features/chat/stores/chatSessionStore";
import { getBufferedMessage } from "@/features/chat/hooks/replayBuffer";
import type {
ToolCallLocation,
ToolCallStatus,
ToolKind,
ToolRequestContent,
ToolResponseContent,
@@ -51,9 +50,6 @@ interface LivePerf {
}
const livePerf = new Map<string, LivePerf>();
const toolCallStatusFromUpdate = (status: string): ToolCallStatus =>
status === "failed" ? "error" : "completed";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -90,7 +86,7 @@ function locationsFromUpdate(
function toolCallUpdatePatch(
update: SessionUpdate,
): Partial<ToolRequestContent> {
): Pick<Partial<ToolRequestContent>, "toolKind" | "locations"> {
const toolKind = toolKindFromUpdate(update);
const locations = locationsFromUpdate(update);
@@ -226,7 +222,7 @@ function handleReplay(sessionId: string, update: SessionUpdate): void {
name: update.title,
...identity,
arguments: rawInputToArguments(update.rawInput),
status: "executing",
status: "in_progress",
...toolCallUpdatePatch(update),
startedAt: created ?? Date.now(),
...(chainSummary ? { chainSummary } : {}),
@@ -276,7 +272,6 @@ function handleReplay(sessionId: string, update: SessionUpdate): void {
}
}
if (update.status === "completed" || update.status === "failed") {
const toolCallStatus = toolCallStatusFromUpdate(update.status);
const tc = msg.content.find(
(c) => c.type === "toolRequest" && c.id === update.toolCallId,
);
@@ -287,7 +282,7 @@ function handleReplay(sessionId: string, update: SessionUpdate): void {
...tc,
...identity,
...toolCallUpdatePatch(update),
status: toolCallStatus,
status: update.status,
} as ToolRequestContent;
}
}
@@ -356,7 +351,7 @@ function handleLive(sessionId: string, update: SessionUpdate): void {
name: update.title,
...identity,
arguments: rawInputToArguments(update.rawInput),
status: "executing",
status: "in_progress",
...toolCallUpdatePatch(update),
startedAt: Date.now(),
...(chainSummary ? { chainSummary } : {}),
@@ -403,7 +398,7 @@ function handleLive(sessionId: string, update: SessionUpdate): void {
}
if (update.status === "completed" || update.status === "failed") {
const toolCallStatus = toolCallStatusFromUpdate(update.status);
const { status: resolvedStatus } = update;
const ownerMessage = store.messagesBySession[sessionId]?.find(
(m) => m.id === messageId,
);
@@ -425,7 +420,7 @@ function handleLive(sessionId: string, update: SessionUpdate): void {
...block,
...identity,
...toolCallUpdatePatch(update),
status: toolCallStatus,
status: resolvedStatus,
}
: block,
),
+2 -2
View File
@@ -59,7 +59,7 @@ export async function listDictationLocalModels(): Promise<
> {
const client = await getClient();
const response = await client.goose.GooseDictationModelsList({});
return response.models as unknown as WhisperModelStatus[];
return response.models;
}
export async function downloadDictationLocalModel(
@@ -76,7 +76,7 @@ export async function getDictationLocalModelDownloadProgress(
const response = await client.goose.GooseDictationModelsDownloadProgress({
modelId,
});
return (response.progress ?? null) as DictationDownloadProgress | null;
return response.progress ?? null;
}
export async function cancelDictationLocalModelDownload(
@@ -26,7 +26,9 @@ import type {
const textBlock: TextContent = { type: "text", text: "hello" };
const imageBlock: ImageContent = {
type: "image",
source: { type: "url", url: "https://img.png" },
data: "",
mimeType: "image/png",
uri: "https://img.png",
};
const toolRequestBlock: ToolRequestContent = {
type: "toolRequest",
+1 -19
View File
@@ -3,24 +3,6 @@
// a narrow union.
export type ProviderType = string;
export interface ProviderConfig {
type: ProviderType;
name: string;
description?: string;
models: ModelInfo[];
requiresApiKey: boolean;
apiKeyEnvVar?: string;
}
export interface ModelInfo {
id: string;
name: string;
contextWindow: number;
supportsTools: boolean;
supportsVision: boolean;
supportsThinking: boolean;
}
// Avatar type — either a remote URL or a local file in ~/.goose/avatars/
export type Avatar =
| { type: "url"; value: string }
@@ -86,4 +68,4 @@ export interface CreateAgentRequest {
acpEndpoint?: string;
}
// Session, TokenState, ChatState, and MessageEventType are defined in ./chat.ts
// Session, TokenState, and ChatState are defined in ./chat.ts
-61
View File
@@ -1,6 +1,3 @@
import type { Message } from "./messages";
import type { Agent } from "./agents";
// Chat state machine
export type ChatState =
| "idle"
@@ -67,61 +64,3 @@ export interface Session {
messageCount: number;
userSetName?: boolean;
}
// SSE event types (from goosed server)
export type MessageEventType =
| "message"
| "error"
| "finish"
| "modelChange"
| "notification"
| "updateConversation"
| "ping";
export interface MessageEvent {
type: "message";
message: Message;
tokenState: TokenState;
}
export interface ErrorEvent {
type: "error";
error: string;
}
export interface FinishEvent {
type: "finish";
reason: string;
tokenState: TokenState;
}
export interface ModelChangeEvent {
type: "modelChange";
model: string;
mode: string;
}
export type StreamEvent =
| MessageEvent
| ErrorEvent
| FinishEvent
| ModelChangeEvent;
// Chat request
export interface ChatRequest {
userMessage: Message;
sessionId: string;
recipeName?: string;
overrideConversation?: Message[];
}
// Active chat context
export interface ChatContext {
sessionId: string;
agent: Agent;
messages: Message[];
chatState: SessionChatRuntime["chatState"];
tokenState: SessionChatRuntime["tokenState"];
streamingMessageId: SessionChatRuntime["streamingMessageId"];
error: SessionChatRuntime["error"];
}
+8 -39
View File
@@ -1,47 +1,16 @@
export type {
DictationModelOption,
DictationProviderStatusEntry as DictationProviderStatus,
DictationTranscribeResponse,
DictationLocalModelStatus as WhisperModelStatus,
DictationDownloadProgress,
} from "@aaif/goose-sdk";
export type DictationProvider = "openai" | "groq" | "elevenlabs" | "local";
export interface DictationModelOption {
id: string;
label: string;
description: string;
}
export interface DictationProviderStatus {
configured: boolean;
host?: string | null;
description: string;
usesProviderConfig: boolean;
settingsPath?: string | null;
configKey?: string | null;
modelConfigKey?: string | null;
defaultModel?: string | null;
selectedModel?: string | null;
availableModels: DictationModelOption[];
}
export interface DictationTranscribeResponse {
text: string;
}
export type MicrophonePermissionStatus =
| "not_determined"
| "authorized"
| "denied"
| "restricted"
| "unsupported";
export interface WhisperModelStatus {
id: string;
sizeMb: number;
description: string;
downloaded: boolean;
downloadInProgress: boolean;
}
export interface DictationDownloadProgress {
bytesDownloaded: number;
totalBytes: number;
progressPercent: number;
status: string;
error?: string | null;
}
+61 -69
View File
@@ -2,6 +2,48 @@ import type {
GooseReadResourceResult,
GooseToolMetadata,
} from "@aaif/goose-sdk";
import type {
Annotations,
ImageContent as AcpImageContent,
Role,
TextContent as AcpTextContent,
ToolCallLocation as AcpToolCallLocation,
ToolCallStatus as AcpToolCallStatus,
ToolKind,
} from "@agentclientprotocol/sdk";
// ── Wire types (re-exported from ACP SDK) ─────────────────────────────
//
// These are the exact types that come off the ACP WebSocket. We re-export
// them so feature code imports everything from this module. Aliases exist
// only for readability — no reshaping, no field-dropping.
export type { Annotations, Role, ToolKind };
export type ToolCallLocation = AcpToolCallLocation;
/** ACP TextContent with discriminator. */
export type TextContent = AcpTextContent & { type: "text" };
/** ACP ImageContent with discriminator. */
export type ImageContent = AcpImageContent & { type: "image" };
/**
* Tool call execution status.
*
* The four ACP wire values plus `"stopped"`, a renderer-only extension
* for user-cancelled tool calls.
*/
export type ToolCallStatus = AcpToolCallStatus | "stopped";
// ── Message role ──────────────────────────────────────────────────────
/**
* ACP defines `Role = "user" | "assistant"`. The renderer adds `"system"`
* for locally-synthesized notification messages.
*/
export type MessageRole = Role | "system";
// ── Composer attachment drafts ────────────────────────────────────────
export type ChatAttachmentKind = "image" | "file" | "directory";
@@ -35,55 +77,11 @@ export type ChatAttachmentDraft =
| ChatFileAttachmentDraft
| ChatDirectoryAttachmentDraft;
// Message roles
export type MessageRole = "user" | "assistant" | "system";
/** ACP audience restriction — which roles may see a content block. */
export type Audience = ("user" | "assistant")[];
/** ACP content-block annotations (mirrors the SDK's Annotations shape). */
export interface ContentAnnotations {
audience?: Audience;
}
// Content block types
export interface TextContent {
type: "text";
text: string;
annotations?: ContentAnnotations;
}
export interface ImageContent {
type: "image";
source:
| { type: "base64"; mediaType: string; data: string }
| { type: "url"; url: string };
annotations?: ContentAnnotations;
}
export type ToolCallStatus =
| "pending"
| "executing"
| "completed"
| "error"
| "stopped";
export type ToolKind =
| "read"
| "edit"
| "delete"
| "move"
| "search"
| "execute"
| "think"
| "fetch"
| "switch_mode"
| "other";
export interface ToolCallLocation {
path: string;
line?: number | null;
}
// ── Renderer-only content block types ─────────────────────────────────
//
// These types have no ACP equivalent. They are synthesized by the
// notification handler from _meta payloads, tool call reductions, or
// local UI events.
export type MessageCompletionStatus =
| "inProgress"
@@ -92,9 +90,7 @@ export type MessageCompletionStatus =
| "stopped";
export interface ToolChainSummary {
/** Lowercase phrase covering the chain's tool calls (e.g. "applied dark mode polish"). */
summary: string;
/** Number of tool calls the summary covers. */
count: number;
}
@@ -107,15 +103,9 @@ export interface ToolRequestContent {
arguments: Record<string, unknown>;
status: ToolCallStatus;
toolKind?: ToolKind;
locations?: ToolCallLocation[];
/** Epoch ms when the tool call started executing (set on event receipt). */
locations?: AcpToolCallLocation[];
startedAt?: number;
annotations?: ContentAnnotations;
/**
* Server-generated summary of a multi-tool chain that starts at this tool
* call. Only set on the FIRST tool call of a chain (>= 2 tools); the rest of
* the chain has this field undefined.
*/
annotations?: Annotations;
chainSummary?: ToolChainSummary;
}
@@ -126,7 +116,7 @@ export interface ToolResponseContent {
result: string;
structuredContent?: unknown;
isError: boolean;
annotations?: ContentAnnotations;
annotations?: Annotations;
}
export interface McpAppPayload {
@@ -155,18 +145,18 @@ export interface McpAppContent {
export interface ThinkingContent {
type: "thinking";
text: string;
annotations?: ContentAnnotations;
annotations?: Annotations;
}
export interface RedactedThinkingContent {
type: "redactedThinking";
annotations?: ContentAnnotations;
annotations?: Annotations;
}
export interface ReasoningContent {
type: "reasoning";
text: string;
annotations?: ContentAnnotations;
annotations?: Annotations;
}
export interface ActionRequiredContent {
@@ -177,16 +167,18 @@ export interface ActionRequiredContent {
toolName?: string;
arguments?: Record<string, unknown>;
schema?: Record<string, unknown>;
annotations?: ContentAnnotations;
annotations?: Annotations;
}
export interface SystemNotificationContent {
type: "systemNotification";
notificationType: "compaction" | "info" | "warning" | "error";
text: string;
annotations?: ContentAnnotations;
annotations?: Annotations;
}
// ── Message ───────────────────────────────────────────────────────────
export type MessageContent =
| TextContent
| ImageContent
@@ -217,11 +209,9 @@ export interface MessageMetadata {
agentVisible?: boolean;
attachments?: MessageAttachment[];
chips?: MessageChip[];
/** Persona that generated this assistant message (set on send). */
personaId?: string;
personaName?: string;
providerId?: string;
/** Which persona this user message is addressed to. */
targetPersonaId?: string;
targetPersonaName?: string;
completionStatus?: MessageCompletionStatus;
@@ -235,7 +225,8 @@ export interface Message {
metadata?: MessageMetadata;
}
// Type guards for content blocks
// ── Type guards ───────────────────────────────────────────────────────
export function isTextContent(c: MessageContent): c is TextContent {
return c.type === "text";
}
@@ -265,7 +256,8 @@ export function isSystemNotification(
return c.type === "systemNotification";
}
// Helpers
// ── Helpers ───────────────────────────────────────────────────────────
export function getTextContent(message: Message): string {
return message.content
.filter(isTextContent)
+1 -7
View File
@@ -11,13 +11,7 @@ export type ProviderSetupMethod = ProviderSetupMethodDto;
export type ProviderGroup = ProviderSetupGroupDto;
export type ProviderField = ProviderSetupFieldDto;
export interface ProviderFieldValue {
key: string;
value: string | null;
isSet: boolean;
isSecret: boolean;
required: boolean;
}
export type { ProviderConfigFieldValueDto as ProviderFieldValue } from "@aaif/goose-sdk";
export type ProviderCatalogEntry = Omit<
ProviderSetupCatalogEntryDto,