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
5 changes: 5 additions & 0 deletions .changeset/2fead794.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mynameistito/opencode-usage-limits": patch
---

Use OpenCode 2.1.3 theme tokens for usage colors
5 changes: 5 additions & 0 deletions .changeset/616f2eea.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mynameistito/opencode-force-input": patch
---

Use OpenCode 2.1.3 theme tokens in the prompt hint
26 changes: 11 additions & 15 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"@types/node": "26.6.5",
"@effect/platform-bun": "4.0.0",
"@effect/platform-node": "4.0.0",
"@opencode/plugin": "2.1.2",
"@opencode/plugin": "2.1.3",
"@opentui/core": "0.6.6",
"@opentui/solid": "0.6.6",
"@typescript/native": "npm:typescript@^7.0.2",
Expand Down
27 changes: 19 additions & 8 deletions packages/opencode-force-input/__tests__/hint.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ type SessionExecutionEvent = (typeof SESSION_EXECUTION_EVENTS)[number];
type FiredEvent = SessionExecutionEvent | "session.retry.scheduled";

const v2Theme = {
text: {
action: { primary: { base: RGBA.fromInts(154, 176, 255) } },
base: RGBA.fromInts(208, 208, 208),
muted: RGBA.fromInts(128, 128, 128),
},
} as const;

const previousV2Theme = {
text: {
action: { primary: { default: RGBA.fromInts(154, 176, 255) } },
default: RGBA.fromInts(208, 208, 208),
Expand Down Expand Up @@ -133,11 +141,9 @@ describe("hint options and colors", () => {
expect(hintEnabled({ hint: false })).toBeFalsy();
});

it("resolves structured and legacy theme colors", () => {
it("resolves current, previous, and legacy theme colors", () => {
const structured = resolveHintColors(v2Theme);
expect(structured.key).toBe(v2Theme.text.default);
expect(structured.label).toBe(v2Theme.text.subdued);
expect(structured.force).toBe(v2Theme.text.action.primary.default);
const previous = resolveHintColors(previousV2Theme);

const legacy = {
text: RGBA.fromInts(1, 2, 3),
Expand All @@ -149,11 +155,16 @@ describe("hint options and colors", () => {

const bareColor = RGBA.fromInts(9, 9, 9);
const bare = resolveHintColors({ text: bareColor });
expect([structured, flat, bare]).toStrictEqual([
expect([structured, previous, flat, bare]).toStrictEqual([
{
force: v2Theme.text.action.primary.base,
key: v2Theme.text.base,
label: v2Theme.text.muted,
},
{
force: v2Theme.text.action.primary.default,
key: v2Theme.text.default,
label: v2Theme.text.subdued,
force: previousV2Theme.text.action.primary.default,
key: previousV2Theme.text.default,
label: previousV2Theme.text.subdued,
},
{
force: legacy.warning,
Expand Down
34 changes: 27 additions & 7 deletions packages/opencode-force-input/src/hint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ export interface ForceHintData {
};
}

interface V2Theme {
interface CurrentV2Theme {
readonly text: {
readonly base: RGBA;
readonly muted: RGBA;
readonly action: { readonly primary: { readonly base: RGBA } };
};
}

interface PreviousV2Theme {
readonly text: {
readonly default: RGBA;
readonly subdued: RGBA;
Expand All @@ -45,10 +53,10 @@ interface LegacyTheme {
}

/**
* Theme shapes seen across OpenCode v2 builds: the structured v2 theme and the
* older flat one. Both are accepted so the hint renders on either host.
* Theme token shapes seen across OpenCode v2 builds, plus the older flat
* theme shape. Both are accepted so the hint renders across host versions.
*/
export type ForceHintTheme = V2Theme | LegacyTheme;
export type ForceHintTheme = CurrentV2Theme | PreviousV2Theme | LegacyTheme;

export interface HintColors {
/** Accent color for the force-submit action. */
Expand All @@ -59,8 +67,12 @@ export interface HintColors {
readonly label: RGBA;
}

/** Distinguishes the structured v2 theme from the older flat one. */
const isV2Theme = (theme: ForceHintTheme): theme is V2Theme =>
/** Identifies the current structured theme tokens. */
const isCurrentV2Theme = (theme: ForceHintTheme): theme is CurrentV2Theme =>
"base" in theme.text;

/** Identifies the earlier structured theme tokens. */
const isPreviousV2Theme = (theme: ForceHintTheme): theme is PreviousV2Theme =>
"default" in theme.text;

/**
Expand All @@ -70,7 +82,15 @@ const isV2Theme = (theme: ForceHintTheme): theme is V2Theme =>
* @returns Colors for the force action, key label, and explanatory copy.
*/
export const resolveHintColors = (theme: ForceHintTheme): HintColors => {
if (isV2Theme(theme)) {
if (isCurrentV2Theme(theme)) {
return {
force: theme.text.action.primary.base,
key: theme.text.base,
label: theme.text.muted,
};
}

if (isPreviousV2Theme(theme)) {
return {
force: theme.text.action.primary.default,
key: theme.text.default,
Expand Down
80 changes: 75 additions & 5 deletions packages/opencode-usage-limits/__tests__/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ import {
} from "@/usage.ts";

const color = RGBA.fromValues(1, 2, 3, 255);
const errorColor = RGBA.fromValues(220, 40, 40, 255);
const warningColor = RGBA.fromValues(220, 160, 40, 255);
const successColor = RGBA.fromValues(40, 180, 80, 255);

const theme = {
text: {
default: color,
base: color,
feedback: {
error: { default: color },
success: { default: color },
warning: { default: color },
error: { base: errorColor },
success: { base: successColor },
warning: { base: warningColor },
},
subdued: color,
muted: color,
},
};

Expand Down Expand Up @@ -466,6 +469,35 @@ describe(UsageLimitsPanel, () => {
}
});

it("renders footer usage with previous structured theme tokens", async () => {
const previousTheme = {
text: {
default: color,
feedback: {
error: { default: color },
success: { default: color },
warning: { default: color },
},
subdued: color,
},
};

const setup = await testRender(
() => (
<BottomUsage showBar theme={previousTheme} window={usageWindow()} />
),
{ height: 4, width: 80 }
);

try {
await setup.flush();
expect(setup.captureCharFrame()).toContain("5h");
expect(setup.captureCharFrame()).toContain("42%");
} finally {
setup.renderer.destroy();
}
});

it("renders footer balances without a percentage or progress bar", async () => {
const setup = await testRender(
() => (
Expand All @@ -492,4 +524,42 @@ describe(UsageLimitsPanel, () => {
setup.renderer.destroy();
}
});

it.each([
{ expectedColor: errorColor, usedPercent: 90 },
{ expectedColor: warningColor, usedPercent: 70 },
{ expectedColor: successColor, usedPercent: 42 },
])(
"uses the matching theme color at $usedPercent%",
async ({ usedPercent, expectedColor }) => {
const setup = await testRender(
() => (
<BottomUsage
showBar
theme={theme}
window={usageWindow({
quota: percentageQuota(
Result.getOrThrow(parseUsagePercentage(usedPercent))
),
})}
/>
),
{ height: 2, width: 80 }
);

try {
await setup.flush();

const bar = setup
.captureSpans()
.lines.flatMap((line) => line.spans)
.find((span) => span.text.startsWith("["));

expect(bar).toBeDefined();
expect(bar?.fg.equals(expectedColor)).toBeTruthy();
} finally {
setup.renderer.destroy();
}
}
);
});
Loading