fx
A coding agent session in a terminal.
Customize
import {Scene, Video} from "odori";
import {Fx} from "../components/fx/fx";
<Video>
<Scene id="fx" duration="9s">
<Fx
query="add a regression test for the chunk cache"
version="0.0.3"
mode="auto"
model="glm-5.2"
charactersPerSecond={22}
/>
</Scene>
</Video>Installation
pnpm odori add component fxnpx odori add component fxbunx odori add component fxEdit the source in videos/components/fx/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/fx/fx.tsx
import {Easing, Fill, interpolate, useBrand, useDesignScale, useFrame, useTyping, typingFrames} from "odori";
export type FxProps = {
/** The instruction typed at the prompt. */
query: string;
/** The version printed in the welcome line. */
version?: string;
/** The permission mode, which the status line brightens. */
mode?: string;
/** The model, after the mode on the status line. */
model?: string;
/** Lines printed back once the instruction is sent. */
response?: string[];
charactersPerSecond?: number;
};
/**
* The tool's own palette, read from its source rather than guessed.
*
* fx is monochrome by design: what other terminal tools colour, it renders in
* greys, down to the styles literally named `green_style` and `red_style`
* being the same 252 as everything else. The only colour anywhere is the plus
* and minus in a diff, so those are the only two values here that are not a
* grey. The numbers are xterm-256 indexes, which is how the tool ships them.
*/
const HINT = "#EEEEEE"; // 255, and bold where the tool uses tag/subtitle
const NOTICE = "#BCBCBC"; // 250
const BRIGHT = "#D0D0D0"; // 252, the "auto" step above the status line
const STATUS = "#8A8A8A"; // 245, status line and dim
const DIVIDER = "#585858"; // 240
/**
* A coding agent that behaves like a shell command.
*
* The restraint is the design, and it goes further than it first looks: there
* is no box, no banner, no panel, and no colour either. The tool prints a
* welcome line, takes an instruction after a `❯`, and writes its answer into
* the same scrollback. Against a bordered welcome card or a status-barred TUI
* this is the third thing a terminal agent can look like, and the contrast is
* the reason to keep all three.
*/
export const Fx = ({
query,
version = "0.0.3",
mode = "auto",
model = "glm-5.2",
response = [],
charactersPerSecond = 22,
}: FxProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const px = (value: number) => value * scale;
const from = 22;
const typed = useTyping(query, {from, charactersPerSecond});
const sentAt = from + typingFrames(query, {charactersPerSecond}) + 12;
const enter = interpolate(frame, [0, 12], [0, 1], {easing: Easing.standard});
const sent = frame >= sentAt;
return (
<Fill style={{background: brand.colors.background, padding: `${px(90)}px ${px(110)}px`}}>
<div
style={{
fontFamily: brand.typography.mono,
fontSize: px(26),
lineHeight: 1.75,
opacity: enter,
width: "100%",
}}
>
{/* The welcome line, as the tool prints it: the wordmark bold and
bright, the rest a step down. */}
<div style={{color: STATUS}}>
<span style={{color: HINT, fontStyle: "italic", fontWeight: 700}}>𝒇x</span>
{` v${version} · Run /help for commands`}
</div>
<div style={{color: NOTICE, marginTop: px(26), whiteSpace: "pre-wrap"}}>
<span style={{color: STATUS}}>❯ </span>
<span style={{color: HINT}}>{typed.text}</span>
{sent ? null : (
<span
style={{
background: typed.caret ? HINT : "transparent",
display: "inline-block",
height: px(28),
transform: `translateY(${px(6)}px)`,
width: px(3),
}}
/>
)}
</div>
{response.length > 0 ? (
<div style={{marginTop: px(22)}}>
{response.map((line, index) => {
// Printed into the same scrollback, one line at a time, the way
// a command reports rather than the way a panel repaints.
const at = interpolate(frame, [sentAt + 8 + index * 13, sentAt + 18 + index * 13], [0, 1], {
easing: Easing.standard,
});
return (
<div key={line} style={{color: NOTICE, display: "flex", gap: px(14), opacity: at}}>
<span style={{color: DIVIDER}}>│</span>
<span>{line}</span>
</div>
);
})}
</div>
) : null}
{/* The status line: the permission mode one step brighter than the
rest, which is the only emphasis the tool allows itself. */}
<div style={{color: STATUS, fontSize: px(23), marginTop: px(30)}}>
<span style={{color: BRIGHT}}>{mode}</span>
{` · ${model}`}
{sent ? " · working" : ""}
</div>
</div>
</Fill>
);
};
videos/components/fx/fx.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {Fx} from "./fx";
export default defineComponentPreview({
title: "fx",
category: "Agents",
description: "A coding agent session in a terminal.",
component: Fx,
canvas: {width: 1920, height: 1080, duration: "9s"},
controls: {
query: {type: "text", defaultValue: "add a regression test for the chunk cache", maxLength: 90},
version: {type: "text", defaultValue: "0.0.3", maxLength: 12},
mode: {type: "select", defaultValue: "auto", options: ["auto", "ask", "YOLO"]},
model: {type: "text", defaultValue: "glm-5.2", maxLength: 28},
charactersPerSecond: {type: "number", defaultValue: 22, min: 8, max: 60, step: 1},
},
examples: [
{name: "Default", props: {}},
{
name: "Working",
props: {
query: "why did the export change size?",
response: ["read packages/odori-cli/src/render.ts", "compared both manifests", "the toolchain pin changed the encoder defaults"],
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Fx} from "../components/fx/fx";
<Video>
<Scene id="fx" duration="9s">
<Fx
query="add a regression test for the chunk cache"
version="0.0.3"
mode="auto"
model="glm-5.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
- 12 frames
- Exit
- 12 frames
- Reduced motion
- query shown whole, caret still
- Requires
- mono font
- Content limits
- query ≤ 90, model ≤ 28
Props
| Prop | Type | Default | Required |
|---|---|---|---|
queryThe instruction typed at the prompt. | string | — | Yes |
versionThe version printed in the welcome line. | string | "0.0.3" | — |
modeThe permission mode, which the status line brightens. | string | "auto" | — |
modelThe model, after the mode on the status line. | string | "glm-5.2" | — |
responseLines printed back once the instruction 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.OpenCodeAn open coding agent's terminal session, with the query typed under its wordmark.v0A v0 composer with an animated prompt.Absolute stageFull-frame surface with stable clipping and positioning.