Split flat docs/ into user-docs/ (guides, config, troubleshooting) and dev/ (ADRs, architecture, extension guides, proposals). Updated docs/README.md index to reflect new paths.
4.3 KiB
4.3 KiB
Real-World Patterns from Examples
Pattern: Selection Dialog with Borders
From preset.ts and tools.ts:
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
const container = new Container();
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
container.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
const selectList = new SelectList(items, maxVisible, {
selectedPrefix: (t) => theme.fg("accent", t),
selectedText: (t) => theme.fg("accent", t),
description: (t) => theme.fg("muted", t),
scrollInfo: (t) => theme.fg("dim", t),
noMatch: (t) => theme.fg("warning", t),
});
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
container.addChild(selectList);
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
return {
render: (w) => container.render(w),
invalidate: () => container.invalidate(),
handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
};
});
Pattern: Game with Timer Loop
From snake.ts:
class SnakeComponent {
private interval: ReturnType<typeof setInterval> | null = null;
constructor(tui: { requestRender: () => void }, done: () => void) {
this.interval = setInterval(() => {
this.tick(); // Update game state
this.version++; // Bump render version
tui.requestRender(); // Request re-render
}, 100);
}
dispose(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
// Call dispose() before calling done() to stop the timer
handleInput(data: string): void {
if (matchesKey(data, Key.escape)) {
this.dispose();
this.onClose();
}
}
}
Pattern: Async Operation with Spinner
From qna.ts:
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
const loader = new BorderedLoader(tui, theme, "Processing...");
loader.onAbort = () => done(null);
doAsyncWork(loader.signal)
.then(data => done(data))
.catch(() => done(null));
return loader;
});
Pattern: Persistent Widget with Live Updates
From plan-mode/index.ts:
function updateUI(ctx: ExtensionContext): void {
if (todoItems.length > 0) {
const lines = todoItems.map(item => {
if (item.completed) {
return ctx.ui.theme.fg("success", "☑ ") +
ctx.ui.theme.fg("muted", ctx.ui.theme.strikethrough(item.text));
}
return ctx.ui.theme.fg("muted", "☐ ") + item.text;
});
ctx.ui.setWidget("plan-todos", lines);
ctx.ui.setStatus("plan-mode", ctx.ui.theme.fg("accent", `📋 ${completed}/${total}`));
} else {
ctx.ui.setWidget("plan-todos", undefined);
ctx.ui.setStatus("plan-mode", undefined);
}
}
Pattern: Multi-Tab Questionnaire
From questionnaire.ts:
// State: currentTab, optionIndex, inputMode, answers map
// Tab navigation with shift+tab / tab
// Option selection with up/down + enter
// "Type something" option that switches to embedded Editor
// Submit tab that shows summary of all answers
// Full renderCall and renderResult for LLM context display
Pattern: Custom Footer with Reactive Data
From custom-footer.ts:
ctx.ui.setFooter((tui, theme, footerData) => ({
invalidate() {},
render(width: number): string[] {
let input = 0, output = 0, cost = 0;
for (const e of ctx.sessionManager.getBranch()) {
if (e.type === "message" && e.message.role === "assistant") {
const m = e.message as AssistantMessage;
input += m.usage.input;
output += m.usage.output;
cost += m.usage.cost.total;
}
}
const branch = footerData.getGitBranch();
const left = theme.fg("dim", `↑${fmt(input)} ↓${fmt(output)} $${cost.toFixed(3)}`);
const right = theme.fg("dim", `${ctx.model?.id}${branch ? ` (${branch})` : ""}`);
const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
return [truncateToWidth(left + pad + right, width)];
},
dispose: footerData.onBranchChange(() => tui.requestRender()),
}));