Skip to content

Commit 22651a6

Browse files
anandgupta42claude
andauthored
feat: show update-available indicator in TUI footer (#175)
* feat: show update-available indicator in TUI footer Add a persistent, subtle upgrade indicator in both the home page and session footers. When a newer version of Altimate Code is available, the footer shows `current → latest · altimate upgrade` in muted/accent colors instead of just the version number. - Add `UpgradeIndicator` component and `getAvailableVersion` utility - Store available version in KV on `UpdateAvailable` event for persistence - Show indicator in both home and session footers - Fix toast message to say `altimate upgrade` instead of `opencode upgrade` - Add 24 tests covering version comparison, KV integration, and event flow Closes #173 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address bot review findings for upgrade indicator - F1: Clear KV on `Updated` event — set `update_available_version` to current `VERSION` after autoupgrade so indicator hides immediately - F2: Add semver comparison via `isNewer()` — prevents downgrade arrow when user upgrades externally past the stored version - F3: Reject empty string as invalid version in `getAvailableVersion()` - Update tests to cover all three fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address code review findings for upgrade indicator - Replace custom `isNewer` with `semver.gt()` — `semver` is already a dependency; handles prerelease tags and rejects corrupted KV values - Use `UPGRADE_KV_KEY` constant in `app.tsx` instead of magic string - Add `fallback` prop to `UpgradeIndicator` — eliminates duplicate `getAvailableVersion` call in `home.tsx` (Gemini design suggestion) - Remove unused `UPGRADE_KV_KEY` re-export from `upgrade-indicator.tsx` - Add conditional check before `kv.set` on `Updated` event to avoid unnecessary file writes - Fix tautological test, add tests for corrupted/prerelease versions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add e2e tests for upgrade indicator lifecycle Add 21 end-to-end tests covering the full upgrade indicator flow: - Full lifecycle: fresh install → UpdateAvailable → indicator shown → Updated → indicator hidden - F1 regression: stale indicator after autoupgrade is cleared - F2 regression: downgrade arrow prevention with `semver.gt()` - F3 regression: empty/corrupted/non-string KV values rejected - Semver integration: prerelease, build metadata, v-prefix handling - Edge cases: rapid events, Updated without UpdateAvailable, same version available as current Uses mock KV store pattern consistent with codebase test conventions (algorithm extraction, no Solid.js context required). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clarify upgrade indicator wording Change "available" to "update available" so users clearly understand a new version upgrade is pending, not just that a version exists. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: make upgrade indicator responsive for small screens On narrow terminals (<100 cols), hide "update available ·" text and show only the essential: ↑ 0.6.0 altimate upgrade Wide (100+): ↑ 0.6.0 update available · altimate upgrade Narrow (<100): ↑ 0.6.0 altimate upgrade Uses useTerminalDimensions() for reactive width detection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent df24e73 commit 22651a6

File tree

8 files changed

+614
-1
lines changed

8 files changed

+614
-1
lines changed

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { RouteProvider, useRoute } from "@tui/context/route"
66
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
77
import { win32DisableProcessedInput, win32FlushInputBuffer, win32InstallCtrlCGuard } from "./win32"
88
import { Installation } from "@/installation"
9+
import { UPGRADE_KV_KEY } from "./component/upgrade-indicator-utils"
910
import { Flag } from "@/flag/flag"
1011
import { Log } from "@/util/log"
1112
import { DialogProvider, useDialog } from "@tui/ui/dialog"
@@ -842,13 +843,20 @@ function App() {
842843

843844
// altimate_change start — branding: altimate upgrade
844845
sdk.event.on(Installation.Event.UpdateAvailable.type, (evt) => {
846+
kv.set(UPGRADE_KV_KEY, evt.properties.version)
845847
toast.show({
846848
variant: "info",
847849
title: "Update Available",
848850
message: `Altimate Code v${evt.properties.version} is available. Run 'altimate upgrade' to update manually.`,
849851
duration: 10000,
850852
})
851853
})
854+
855+
sdk.event.on(Installation.Event.Updated.type, () => {
856+
if (kv.get(UPGRADE_KV_KEY) !== Installation.VERSION) {
857+
kv.set(UPGRADE_KV_KEY, Installation.VERSION)
858+
}
859+
})
852860
// altimate_change end
853861

854862
return (
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import semver from "semver"
2+
import { Installation } from "@/installation"
3+
4+
export const UPGRADE_KV_KEY = "update_available_version"
5+
6+
function isNewer(candidate: string, current: string): boolean {
7+
// Dev mode: show indicator for any valid semver candidate
8+
if (current === "local") {
9+
return semver.valid(candidate) !== null
10+
}
11+
if (!semver.valid(candidate) || !semver.valid(current)) {
12+
return false
13+
}
14+
return semver.gt(candidate, current)
15+
}
16+
17+
export function getAvailableVersion(kvValue: unknown): string | undefined {
18+
if (typeof kvValue !== "string" || !kvValue) return undefined
19+
if (kvValue === Installation.VERSION) return undefined
20+
if (!isNewer(kvValue, Installation.VERSION)) return undefined
21+
return kvValue
22+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { createMemo, Show, type JSX } from "solid-js"
2+
import { useTerminalDimensions } from "@opentui/solid"
3+
import { useTheme } from "@tui/context/theme"
4+
import { useKV } from "../context/kv"
5+
import { UPGRADE_KV_KEY, getAvailableVersion } from "./upgrade-indicator-utils"
6+
7+
export function UpgradeIndicator(props: { fallback?: JSX.Element }) {
8+
const { theme } = useTheme()
9+
const kv = useKV()
10+
const dimensions = useTerminalDimensions()
11+
12+
const latestVersion = createMemo(() => getAvailableVersion(kv.get(UPGRADE_KV_KEY)))
13+
const isCompact = createMemo(() => dimensions().width < 100)
14+
15+
return (
16+
<Show when={latestVersion()} fallback={props.fallback}>
17+
{(version) => (
18+
<box flexDirection="row" gap={1} flexShrink={0}>
19+
<text fg={theme.success}></text>
20+
<text fg={theme.accent}>{version()}</text>
21+
<Show when={!isCompact()}>
22+
<text fg={theme.textMuted}>update available ·</text>
23+
</Show>
24+
<text fg={theme.textMuted}>altimate upgrade</text>
25+
</box>
26+
)}
27+
</Show>
28+
)
29+
}

packages/opencode/src/cli/cmd/tui/routes/home.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Installation } from "@/installation"
1515
import { useKV } from "../context/kv"
1616
import { useCommandDialog } from "../component/dialog-command"
1717
import { useLocal } from "../context/local"
18+
import { UpgradeIndicator } from "../component/upgrade-indicator"
1819

1920
// TODO: what is the best way to do this?
2021
let once = false
@@ -152,7 +153,7 @@ export function Home() {
152153
</box>
153154
<box flexGrow={1} />
154155
<box flexShrink={0}>
155-
<text fg={theme.textMuted}>{Installation.VERSION}</text>
156+
<UpgradeIndicator fallback={<text fg={theme.textMuted}>{Installation.VERSION}</text>} />
156157
</box>
157158
</box>
158159
</>

packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useRoute } from "../../context/route"
88
// altimate_change start - yolo mode visual indicator
99
import { Flag } from "@/flag/flag"
1010
// altimate_change end
11+
import { UpgradeIndicator } from "../../component/upgrade-indicator"
1112

1213
export function Footer() {
1314
const { theme } = useTheme()
@@ -95,6 +96,7 @@ export function Footer() {
9596
<text fg={theme.textMuted}>/status</text>
9697
</Match>
9798
</Switch>
99+
<UpgradeIndicator />
98100
</box>
99101
</box>
100102
)

0 commit comments

Comments
 (0)