Skip to content
Merged
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
28 changes: 28 additions & 0 deletions extensions/shared/spinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* One braille spinner for every running-state indicator in the package:
* transcripts, takeover and dashboard headers, and the below-editor strips all
* advance on the same cadence so concurrent views animate in step.
*/

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

/** Frame cadence, shared with the dashboard and takeover headers. */
export const SPINNER_INTERVAL_MS = 120;

export function spinnerFrame(now: number) {
const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
return SPINNER_FRAMES[
(frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
];
}
4 changes: 3 additions & 1 deletion extensions/subagents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,10 @@ export default function (pi: ExtensionAPI) {
const meta = [details.harness, details.model]
.filter(Boolean)
.join(" \u00b7 ");
// A spawn is a beginning, not a success: keep the marker quiet and let
// the strip's spinner carry the running state from here on.
return new Text(
`${theme.fg("success", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
`${theme.fg("dim", "\u25cf")} ${theme.bold(details.title ?? details.id)} ${theme.fg("dim", meta)}`,
0,
0,
);
Expand Down
13 changes: 9 additions & 4 deletions extensions/subagents/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
unreadActivityCounts,
type ActivityCounts,
} from "../shared/activity-status.ts";
import { spinnerFrame } from "../shared/spinner.ts";
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
import { formatElapsed, type SubagentSnapshot } from "./src/domain.ts";
import { contextPercent } from "./src/format.ts";
Expand Down Expand Up @@ -59,9 +60,13 @@ function statusColor(status: SubagentSnapshot["status"]) {
return "error" as const;
}

/** One status glyph per run state; doubles as the focus marker when selected. */
function statusGlyph(snapshot: SubagentSnapshot, theme: Theme) {
if (snapshot.status === "running") return theme.fg("warning", "●");
/**
* One status indicator per run state; doubles as the focus marker when
* selected. Running spins, in step with the dashboard and takeover headers.
*/
function statusGlyph(snapshot: SubagentSnapshot, theme: Theme, now: number) {
if (snapshot.status === "running")
return theme.fg("warning", spinnerFrame(now));
if (snapshot.status === "done") return theme.fg("success", "✓");
return theme.fg("error", "x");
}
Expand Down Expand Up @@ -100,7 +105,7 @@ export class SubagentStripWidget {
const { snapshot, counts } = entry;
const glyph = this.strip.focused
? this.theme.fg("accent", "❯")
: statusGlyph(snapshot, this.theme);
: statusGlyph(snapshot, this.theme, Date.now());
const titleText = normalizeSubagentTitle(snapshot.title, snapshot.id);
const title = this.strip.focused
? this.theme.bold(this.theme.fg("accent", titleText))
Expand Down
30 changes: 8 additions & 22 deletions extensions/subagents/src/ui/transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,14 @@ import type { SubagentSnapshot, TranscriptItem } from "../domain.ts";

const MAX_CACHED_WIDTHS_PER_ITEM = 2;

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

/** Frame cadence, shared with the dashboard and takeover headers. */
export const SPINNER_INTERVAL_MS = 120;

export function spinnerFrame(now: number) {
const frame = Math.floor(now / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length;
return SPINNER_FRAMES[
(frame + SPINNER_FRAMES.length) % SPINNER_FRAMES.length
];
}
// The spinner lives in shared/ so strips outside this extension animate in
// step; the re-export keeps this module's historical import surface intact.
import { spinnerFrame } from "../../../shared/spinner.ts";
export {
SPINNER_FRAMES,
SPINNER_INTERVAL_MS,
spinnerFrame,
} from "../../../shared/spinner.ts";

/**
* Strip raw ANSI codes, expand tabs, and drop control chars. Terminal-expanded
Expand Down
12 changes: 8 additions & 4 deletions extensions/workflows/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
fitNavigationSides,
renderNavigationMetrics,
} from "../shared/below-editor-navigation.ts";
import { spinnerFrame } from "../shared/spinner.ts";
import { sanitizeTerminalText } from "../shared/terminal-text.ts";
import {
aggregateUsage,
Expand Down Expand Up @@ -34,10 +35,13 @@ function cleanLine(value: string) {
return sanitizeTerminalText(value).replace(/\s+/g, " ").trim();
}

/** One status glyph per run state; doubles as the focus marker when selected. */
function statusGlyph(status: WorkflowStatus, theme: Theme) {
/**
* One status indicator per run state; doubles as the focus marker when
* selected. Running spins, in step with the dashboard and takeover headers.
*/
function statusGlyph(status: WorkflowStatus, theme: Theme, now: number) {
if (status === "completed") return theme.fg("success", "✓");
if (status === "running") return theme.fg("warning", "●");
if (status === "running") return theme.fg("warning", spinnerFrame(now));
return theme.fg("error", "x");
}

Expand Down Expand Up @@ -79,7 +83,7 @@ export class WorkflowStripWidget {
const tokenCount = usage.input + usage.output;
const glyph = this.strip.focused
? this.theme.fg("accent", "❯")
: statusGlyph(details.status, this.theme);
: statusGlyph(details.status, this.theme, Date.now());
const displayName = cleanLine(details.name ?? entry.runId) || entry.runId;
const name = this.strip.focused
? this.theme.bold(this.theme.fg("accent", displayName))
Expand Down
Loading