From 9bf2c8ef496f0457f8e677b4db799fe51fd30e0c Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 15 Jan 2026 06:18:38 +0000 Subject: [PATCH 1/2] Prettify retry duration display in TUI Co-authored-by: rekram1-node --- .../cli/cmd/tui/component/prompt/index.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 9ad85d08f0e..88f37b456d9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1032,12 +1032,34 @@ export function Prompt(props: PromptProps) { } } + const formatDuration = (secs: number) => { + if (secs <= 0) return "" + if (secs < 60) return `${secs}s` + if (secs < 3600) { + const mins = Math.floor(secs / 60) + const remainingSecs = secs % 60 + return remainingSecs > 0 ? `${mins}m ${remainingSecs}s` : `${mins}m` + } + if (secs < 86400) { + const hours = Math.floor(secs / 3600) + const remainingMins = Math.floor((secs % 3600) / 60) + return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h` + } + if (secs < 604800) { + const days = Math.floor(secs / 86400) + return days === 1 ? "~1 day" : `~${days} days` + } + const weeks = Math.floor(secs / 604800) + return weeks === 1 ? "~1 week" : `~${weeks} weeks` + } + const retryText = () => { const r = retry() if (!r) return "" const baseMessage = message() const truncatedHint = isTruncated() ? " (click to expand)" : "" - const retryInfo = ` [retrying ${seconds() > 0 ? `in ${seconds()}s ` : ""}attempt #${r.attempt}]` + const duration = formatDuration(seconds()) + const retryInfo = ` [retrying ${duration ? `in ${duration} ` : ""}attempt #${r.attempt}]` return baseMessage + truncatedHint + retryInfo } From 2c5e78279d7ae165822dfb92b41aeaeddc7ceab9 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 15 Jan 2026 17:13:53 +0000 Subject: [PATCH 2/2] Extract formatDuration to util, add tests Co-authored-by: rekram1-node --- .../cli/cmd/tui/component/prompt/index.tsx | 22 +------ packages/opencode/src/util/format.ts | 20 +++++++ packages/opencode/test/util/format.test.ts | 59 +++++++++++++++++++ 3 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 packages/opencode/src/util/format.ts create mode 100644 packages/opencode/test/util/format.test.ts diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 88f37b456d9..96b9e8ffd57 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -23,6 +23,7 @@ import type { FilePart } from "@opencode-ai/sdk/v2" import { TuiEvent } from "../../event" import { iife } from "@/util/iife" import { Locale } from "@/util/locale" +import { formatDuration } from "@/util/format" import { createColors, createFrames } from "../../ui/spinner.ts" import { useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" @@ -1032,27 +1033,6 @@ export function Prompt(props: PromptProps) { } } - const formatDuration = (secs: number) => { - if (secs <= 0) return "" - if (secs < 60) return `${secs}s` - if (secs < 3600) { - const mins = Math.floor(secs / 60) - const remainingSecs = secs % 60 - return remainingSecs > 0 ? `${mins}m ${remainingSecs}s` : `${mins}m` - } - if (secs < 86400) { - const hours = Math.floor(secs / 3600) - const remainingMins = Math.floor((secs % 3600) / 60) - return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h` - } - if (secs < 604800) { - const days = Math.floor(secs / 86400) - return days === 1 ? "~1 day" : `~${days} days` - } - const weeks = Math.floor(secs / 604800) - return weeks === 1 ? "~1 week" : `~${weeks} weeks` - } - const retryText = () => { const r = retry() if (!r) return "" diff --git a/packages/opencode/src/util/format.ts b/packages/opencode/src/util/format.ts new file mode 100644 index 00000000000..4ae62eac450 --- /dev/null +++ b/packages/opencode/src/util/format.ts @@ -0,0 +1,20 @@ +export function formatDuration(secs: number) { + if (secs <= 0) return "" + if (secs < 60) return `${secs}s` + if (secs < 3600) { + const mins = Math.floor(secs / 60) + const remaining = secs % 60 + return remaining > 0 ? `${mins}m ${remaining}s` : `${mins}m` + } + if (secs < 86400) { + const hours = Math.floor(secs / 3600) + const remaining = Math.floor((secs % 3600) / 60) + return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h` + } + if (secs < 604800) { + const days = Math.floor(secs / 86400) + return days === 1 ? "~1 day" : `~${days} days` + } + const weeks = Math.floor(secs / 604800) + return weeks === 1 ? "~1 week" : `~${weeks} weeks` +} diff --git a/packages/opencode/test/util/format.test.ts b/packages/opencode/test/util/format.test.ts new file mode 100644 index 00000000000..5b346e7f6bc --- /dev/null +++ b/packages/opencode/test/util/format.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test" +import { formatDuration } from "../../src/util/format" + +describe("util.format", () => { + describe("formatDuration", () => { + test("returns empty string for zero or negative values", () => { + expect(formatDuration(0)).toBe("") + expect(formatDuration(-1)).toBe("") + expect(formatDuration(-100)).toBe("") + }) + + test("formats seconds under a minute", () => { + expect(formatDuration(1)).toBe("1s") + expect(formatDuration(30)).toBe("30s") + expect(formatDuration(59)).toBe("59s") + }) + + test("formats minutes under an hour", () => { + expect(formatDuration(60)).toBe("1m") + expect(formatDuration(61)).toBe("1m 1s") + expect(formatDuration(90)).toBe("1m 30s") + expect(formatDuration(120)).toBe("2m") + expect(formatDuration(330)).toBe("5m 30s") + expect(formatDuration(3599)).toBe("59m 59s") + }) + + test("formats hours under a day", () => { + expect(formatDuration(3600)).toBe("1h") + expect(formatDuration(3660)).toBe("1h 1m") + expect(formatDuration(7200)).toBe("2h") + expect(formatDuration(8100)).toBe("2h 15m") + expect(formatDuration(86399)).toBe("23h 59m") + }) + + test("formats days under a week", () => { + expect(formatDuration(86400)).toBe("~1 day") + expect(formatDuration(172800)).toBe("~2 days") + expect(formatDuration(259200)).toBe("~3 days") + expect(formatDuration(604799)).toBe("~6 days") + }) + + test("formats weeks", () => { + expect(formatDuration(604800)).toBe("~1 week") + expect(formatDuration(1209600)).toBe("~2 weeks") + expect(formatDuration(1609200)).toBe("~2 weeks") + }) + + test("handles boundary values correctly", () => { + expect(formatDuration(59)).toBe("59s") + expect(formatDuration(60)).toBe("1m") + expect(formatDuration(3599)).toBe("59m 59s") + expect(formatDuration(3600)).toBe("1h") + expect(formatDuration(86399)).toBe("23h 59m") + expect(formatDuration(86400)).toBe("~1 day") + expect(formatDuration(604799)).toBe("~6 days") + expect(formatDuration(604800)).toBe("~1 week") + }) + }) +})