OpenCode
An open coding agent's terminal session, with the query typed under its wordmark.
Customize
import {Scene, Video} from "odori";
import {Opencode} from "../components/opencode/opencode";
<Video>
<Scene id="opencode" duration="9s">
<Opencode
query="find every place we still shell out to system ffmpeg"
project="odori"
model="claude-opus-4-5"
version="v0.4.2"
charactersPerSecond={22}
/>
</Scene>
</Video>Installation
pnpm odori add component opencodenpx odori add component opencodebunx odori add component opencodeEdit the source in videos/components/opencode/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/opencode/opencode.tsx
import {Easing, Fill, interpolate, useDesignScale, useFrame, useTyping, typingFrames} from "odori";
export type OpencodeProps = {
/** What gets typed into the input under the wordmark. */
query: string;
/** The project the session is attached to, shown in the status bar. */
project?: string;
/** The model, shown in the status bar. */
model?: string;
/** The version beside the wordmark. */
version?: string;
/** Lines printed back once the query is sent. */
response?: string[];
charactersPerSecond?: number;
};
/**
* opencode's own theme, taken from the palette the TUI ships with: the
* primary is a warm peach rather than the blue a terminal usually reaches
* for, and the surfaces climb #0a0a0a → #141414 → #1e1e1e.
*/
const PAGE = "#0A0A0A";
const PANEL = "#141414";
const ELEMENT = "#1E1E1E";
const INK = "#EEEEEE";
const FAINT = "#808080";
const EDGE = "#484848";
const EDGE_SUBTLE = "#3C3C3C";
const ACCENT = "#FAB283";
const SECONDARY = "#5C9CF5";
const SUCCESS = "#7FD88F";
/**
* An open coding agent's terminal session.
*
* Where a hosted tool leads with a welcome card, a TUI leads with its own
* chrome: the wordmark at the top left, the session's facts pinned to a status
* bar at the bottom, and the whole width given to the input between them. The
* status bar is the detail worth keeping, because it is what makes a terminal
* app feel like a place rather than a prompt.
*/
/** A terminal sets its own face; this is the stack a TUI lands in. */
const FONT = '"SF Mono", "JetBrains Mono", "Fira Code", ui-monospace, Menlo, Consolas, monospace';
export const Opencode = ({
query,
project = "odori",
model = "claude-opus-4-5",
version = "v0.4.2",
response = [],
charactersPerSecond = 22,
}: OpencodeProps) => {
const frame = useFrame();
const scale = useDesignScale();
const px = (value: number) => value * scale;
const from = 24;
const typed = useTyping(query, {from, charactersPerSecond});
const sentAt = from + typingFrames(query, {charactersPerSecond}) + 12;
const enter = interpolate(frame, [0, 14], [0, 1], {easing: Easing.standard});
const sent = frame >= sentAt;
return (
<Fill style={{background: PAGE, padding: px(90)}}>
<div
style={{
background: PANEL,
border: `${px(1)}px solid ${EDGE_SUBTLE}`,
borderRadius: px(12),
display: "flex",
flexDirection: "column",
fontFamily: FONT,
height: "100%",
opacity: enter,
overflow: "hidden",
transform: `translateY(${(1 - enter) * px(12)}px)`,
width: "100%",
}}
>
<div style={{flex: 1, padding: `${px(34)}px ${px(36)}px`}}>
<div style={{alignItems: "baseline", display: "flex", gap: px(16)}}>
<span style={{color: INK, fontSize: px(34), fontWeight: 700, letterSpacing: "-0.02em"}}>
open<span style={{color: ACCENT}}>code</span>
</span>
<span style={{color: FAINT, fontSize: px(20)}}>{version}</span>
</div>
<div
style={{
alignItems: "center",
background: ELEMENT,
border: `${px(1)}px solid ${sent ? EDGE_SUBTLE : EDGE}`,
borderRadius: px(8),
display: "flex",
gap: px(14),
marginTop: px(30),
padding: `${px(18)}px ${px(20)}px`,
}}
>
<span style={{color: ACCENT, fontSize: px(24)}}>❯</span>
<span style={{color: INK, fontSize: px(24), whiteSpace: "pre"}}>
{typed.text}
{sent ? null : (
<span
style={{
background: typed.caret ? ACCENT : "transparent",
display: "inline-block",
height: px(26),
transform: `translateY(${px(5)}px)`,
width: px(11),
}}
/>
)}
</span>
</div>
{response.length > 0 ? (
<div style={{display: "grid", gap: px(12), marginTop: px(28)}}>
{response.map((line, index) => {
const at = interpolate(frame, [sentAt + 8 + index * 13, sentAt + 20 + index * 13], [0, 1], {
easing: Easing.standard,
});
return (
<div key={line} style={{alignItems: "baseline", display: "flex", gap: px(12), opacity: at}}>
<span style={{color: EDGE_SUBTLE, fontSize: px(20)}}>│</span>
<span style={{color: INK, fontSize: px(21)}}>{line}</span>
</div>
);
})}
</div>
) : null}
</div>
{/* The status bar: what a TUI keeps in front of you the whole time. */}
<div
style={{
alignItems: "center",
borderTop: `${px(1)}px solid ${EDGE_SUBTLE}`,
color: FAINT,
display: "flex",
fontSize: px(19),
gap: px(20),
padding: `${px(14)}px ${px(24)}px`,
}}
>
<span style={{color: ACCENT, fontWeight: 700}}>{project}</span>
<span style={{color: SECONDARY}}>{model}</span>
<span style={{color: sent ? SUCCESS : FAINT, marginLeft: "auto"}}>{sent ? "working" : "ctrl+c to quit"}</span>
</div>
</div>
</Fill>
);
};
videos/components/opencode/opencode.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {Opencode} from "./opencode";
export default defineComponentPreview({
title: "OpenCode",
category: "Agents",
description: "An open coding agent's terminal session, with the query typed under its wordmark.",
component: Opencode,
canvas: {width: 1920, height: 1080, duration: "9s"},
controls: {
query: {type: "text", defaultValue: "find every place we still shell out to system ffmpeg", maxLength: 90},
project: {type: "text", defaultValue: "odori", maxLength: 28},
model: {type: "text", defaultValue: "claude-opus-4-5", maxLength: 28},
version: {type: "text", defaultValue: "v0.4.2", maxLength: 12},
charactersPerSecond: {type: "number", defaultValue: 22, min: 8, max: 60, step: 1},
},
examples: [
{name: "Default", props: {}},
{
name: "Working",
props: {
query: "run the render tests",
response: ["packages/odori-cli/src/render.ts", "9 passed in 41s", "No system binaries were used."],
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Opencode} from "../components/opencode/opencode";
<Video>
<Scene id="opencode" duration="9s">
<Opencode
query="find every place we still shell out to system ffmpeg"
project="odori"
model="claude-opus-4-5"
version="v0.4.2"
charactersPerSecond={22}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Agents
- Recommended duration
- 270 frames · 9s
- Minimum duration
- 120 frames · 4s
- Entrance
- 14 frames
- Exit
- 12 frames
- Reduced motion
- query shown whole, caret still
- Requires
- mono font
- Content limits
- query ≤ 90, project ≤ 28, model ≤ 28, version ≤ 12
Props
| Prop | Type | Default | Required |
|---|---|---|---|
queryWhat gets typed into the input under the wordmark. | string | — | Yes |
projectThe project the session is attached to, shown in the status bar. | string | "odori" | — |
modelThe model, shown in the status bar. | string | "claude-opus-4-5" | — |
versionThe version beside the wordmark. | string | "v0.4.2" | — |
responseLines printed back once the query is sent. | string[] | [] | — |
charactersPerSecond | number | 22 | — |
Related components
All componentsChatGPTA ChatGPT composer with an animated prompt.ClaudeA Claude composer with an animated prompt.Claude CodeA Claude Code session from prompt to response.fxA coding agent session in a terminal.v0A v0 composer with an animated prompt.Absolute stageFull-frame surface with stable clipping and positioning.