Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/extensions/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
formatMs,
formatTokens,
formatTurns,
getAgentSpinnerFrames,
getDisplayName,
SPINNER,
type Theme,
Expand Down Expand Up @@ -163,6 +164,7 @@ interface GetSubagentResultDetails {
agentId: string
displayName: string
description: string
subagentType?: string
status: string
visibility?: AgentVisibility
abortReason?: AgentAbortReason
Expand All @@ -186,11 +188,13 @@ function formatAgentBodyForDisplay(raw: string): string {
return cleaned.replace(/\n{3,}/g, "\n\n").trimEnd()
}

function getSubagentResultIcon(status: string, theme: Theme): string {
function getSubagentResultIcon(status: string, theme: Theme, subagentType?: string): string {
switch (status) {
case "running":
case "queued":
return theme.fg("accent", SPINNER[0])
case "queued": {
const frames = subagentType ? getAgentSpinnerFrames(subagentType) : SPINNER
return theme.fg("accent", frames[0])
}
case "error":
case "aborted":
return theme.fg("error", "✗")
Expand Down Expand Up @@ -1320,7 +1324,8 @@ ${AGENT_TOOL_GUIDELINES}`,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️🔧 Maintainability

getAgentSpinnerFrames(details.subagentType) is called with a value that may be undefined, because GetSubagentResultDetails.subagentType is declared as optional (subagentType?: string) while getAgentSpinnerFrames requires a non-optional string. Although the function happens to fall back to SPINNER when the type does not match, relying on this is brittle and may break under stricter TypeScript settings or future refactors. It is also inconsistent with getSubagentResultIcon, which explicitly guards the call with subagentType ? getAgentSpinnerFrames(subagentType) : SPINNER.

💡 Suggestion: Change getAgentSpinnerFrames in src/extensions/agents/ui/agent-widget.ts to accept type: string | undefined (returning SPINNER for undefined), or guard the call here: const frames = details.subagentType ? getAgentSpinnerFrames(details.subagentType) : SPINNER.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment makes sense, it would be better to handle this possible null case in getAgentSpinnerFrames itself.


if (isPartial || details.status === "running") {
const frame = SPINNER[details.spinnerFrame ?? 0]
const frames = getAgentSpinnerFrames(details.subagentType)
const frame = frames[(details.spinnerFrame ?? 0) % frames.length]
const s = stats(details)
let line = theme.fg("accent", frame) + (s ? ` ${s}` : "")
line += `\n${theme.fg("dim", ` ⎿ ${details.activity ?? "thinking..."}`)} ${theme.fg("muted", "(ctrl+b to run in background)")}`
Expand Down Expand Up @@ -1625,7 +1630,7 @@ ${AGENT_TOOL_GUIDELINES}`,
durationMs: Date.now() - startedAt,
status: "running",
activity: describeActivity(fgState.activeTools, fgState.responseText),
spinnerFrame: spinnerFrame % SPINNER.length,
spinnerFrame: spinnerFrame % getAgentSpinnerFrames(subagentType).length,
}
onUpdate?.({
content: [{ type: "text", text: `${fgState.toolUses} tool uses...` }],
Expand Down Expand Up @@ -1927,7 +1932,7 @@ ${AGENT_TOOL_GUIDELINES}`,
return parts.map((p) => theme.fg("dim", p)).join(` ${theme.fg("dim", "·")} `)
}

const icon = getSubagentResultIcon(details.status, theme)
const icon = getSubagentResultIcon(details.status, theme, details.subagentType)

const headerName = theme.fg("toolTitle", theme.bold("Get Agent Result"))
const headerDesc = details.description ? ` ${theme.fg("muted", details.description)}` : ""
Expand Down Expand Up @@ -2022,6 +2027,7 @@ ${AGENT_TOOL_GUIDELINES}`,
agentId: record.id,
displayName,
description: record.description,
subagentType: record.type,
status: record.status,
visibility: record.visibility,
abortReason: record.abortReason,
Expand Down
21 changes: 20 additions & 1 deletion src/extensions/agents/ui/agent-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ const MAX_WIDGET_LINES = 12

export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

/**
* Cloud Agent animation — a cloud glyph (☁) with a loading arc (◜◝◞◟)
* tracing around it, completing in a full circle (◯) before restarting.
* Used instead of SPINNER for the Remote-Runner (Cloud Agent) subagent type.
*/
export const CLOUD = ["☁◜", "☁◝", "☁◞", "☁◟", "☁◯", "☁◯"]

/** Type identifier for the Cloud Agent (remote sandbox runner via ACP). */
const CLOUD_AGENT_TYPE = "Remote-Runner"

/**
* Returns the animation frames for a given agent type.
* Cloud Agent (Remote-Runner) gets the cloud animation; everything else gets the braille spinner.
*/
export function getAgentSpinnerFrames(type: string): string[] {
return type === CLOUD_AGENT_TYPE ? CLOUD : SPINNER
}

export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"])

const TOOL_DISPLAY: Record<string, string> = {
Expand Down Expand Up @@ -262,7 +280,6 @@ export class AgentWidget {
const truncate = (line: string) => truncateToWidth(line, width)
const headingColor = hasActive ? "accent" : "dim"
const headingIcon = hasActive ? "●" : "○"
const frame = SPINNER[this.widgetFrame % SPINNER.length]

const finishedLines: string[] = []
for (const a of finished) {
Expand All @@ -275,6 +292,8 @@ export class AgentWidget {
for (const a of running) {
const name = getDisplayName(a.type)
const elapsed = formatMs(Date.now() - a.startedAt)
const frames = getAgentSpinnerFrames(a.type)
const frame = frames[this.widgetFrame % frames.length]

const bg = this.agentActivity.get(a.id)
const toolUses = bg?.toolUses ?? a.toolUses
Expand Down
Loading