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
15 changes: 8 additions & 7 deletions packages/coding-agent/docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ vim ~/.diffex/agent/themes/my-theme.json
"thinkingText": "secondary",
"startupIntroText": "#CDD6F4",
"startupIntroAccent": "#89B4FA",
"autocompleteSelected": "#89B4FA",
"selectionAccent": "#89B4FA",
"selectedBg": "#2d2d30",
"scrollbarThumb": "#555566",
"userMessageBg": "#2d2d30",
Expand Down Expand Up @@ -145,21 +145,21 @@ vim ~/.diffex/agent/themes/my-theme.json
- `name` is required, must be unique, and must not contain `/`.
- `vars` is optional. Define reusable colors here, then reference them in `colors`.
- `colors` must define all 51 required tokens.
`thinkingMax`, `scrollbarThumb`, `startupIntroText`, `startupIntroAccent`, and `autocompleteSelected` are optional compatibility tokens with fallbacks.
`thinkingMax`, `scrollbarThumb`, `startupIntroText`, `startupIntroAccent`, `selectionAccent`, and `autocompleteSelected` are optional compatibility tokens with fallbacks.

The `$schema` field enables editor auto-completion and validation.

## Color Tokens

Every theme must define all 51 required color tokens.
`thinkingMax`, `scrollbarThumb`, `startupIntroText`, `startupIntroAccent`, and `autocompleteSelected` are optional for compatibility with existing themes.
When omitted, they use `thinkingXhigh`, `selectedBg`, `text`, `mdLink`, and `startupIntroAccent`, respectively.
`thinkingMax`, `scrollbarThumb`, `startupIntroText`, `startupIntroAccent`, `selectionAccent`, and `autocompleteSelected` are optional for compatibility with existing themes.
When omitted, they use `thinkingXhigh`, `selectedBg`, `text`, `mdLink`, `autocompleteSelected` or `startupIntroAccent`, and `selectionAccent`, respectively.

### Core UI (11 colors)

| Token | Purpose |
|-------|---------|
| `accent` | Primary accent (logo, selected items, cursor) |
| `accent` | Primary accent for titles, active controls, and status elements |
| `border` | Normal borders |
| `borderAccent` | Highlighted borders |
| `borderMuted` | Subtle borders (editor) |
Expand All @@ -171,13 +171,14 @@ When omitted, they use `thinkingXhigh`, `selectedBg`, `text`, `mdLink`, and `sta
| `text` | Default text (usually `""`) |
| `thinkingText` | Thinking block text |

### Startup Intro And Autocomplete (3 optional)
### Startup Intro And Selection (4 optional)

| Token | Purpose |
|-------|---------|
| `startupIntroText` | Startup logo art, version, directory, and changelog preview text; optional, falls back to `text` |
| `startupIntroAccent` | Startup title and boundary box; optional, falls back to `mdLink` |
| `autocompleteSelected` | Editor autocomplete selected row; optional, falls back to `startupIntroAccent` |
| `selectionAccent` | Selected row prefix and primary text; optional, falls back to `autocompleteSelected`, then `startupIntroAccent` |
| `autocompleteSelected` | Legacy autocomplete selection color; optional, falls back to `selectionAccent` |

### Backgrounds & Content (11 required, 1 optional)

Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/src/core/tools/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): st
if (args?.offset === undefined && args?.limit === undefined) return "";
const startLine = args.offset ?? 1;
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
return theme.fg("accent", `:${startLine}${endLine ? `-${endLine}` : ""}`);
}

function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme, cwd: string): string {
Expand Down
57 changes: 32 additions & 25 deletions packages/coding-agent/src/core/tools/update-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,28 @@ const updatePlanItemSchema = Type.Object(
{ additionalProperties: false },
);

const updatePlanSchema = Type.Union([
Type.Object(
{
plan: Type.Array(updatePlanItemSchema, {
minItems: 1,
description: "Full ordered plan snapshot.",
}),
explanation: Type.Optional(Type.String({ description: "Reason for a material plan change." })),
},
{ additionalProperties: false },
),
Type.Object(
{
clear: Type.Literal(true, { description: "Explicitly abandon and clear the current plan." }),
explanation: Type.Optional(Type.String({ description: "Reason for clearing the plan." })),
},
{ additionalProperties: false },
),
]);
const updatePlanSchema = Type.Unsafe<UpdatePlanToolInput>({
type: "object",
anyOf: [
Type.Object(
{
plan: Type.Array(updatePlanItemSchema, {
minItems: 1,
description: "Full ordered plan snapshot.",
}),
explanation: Type.Optional(Type.String({ description: "Reason for a material plan change." })),
},
{ additionalProperties: false },
),
Type.Object(
{
clear: Type.Literal(true, { description: "Explicitly abandon and clear the current plan." }),
explanation: Type.Optional(Type.String({ description: "Reason for clearing the plan." })),
},
{ additionalProperties: false },
),
],
});

export const updatePlanToolSystemPromptContribution = {
snippet: "track progress with an ordered task checklist",
Expand Down Expand Up @@ -183,9 +186,9 @@ function formatPlanSummary(plan: readonly UpdatePlanItem[]): string {
function formatPlanMarker(status: unknown, theme: Theme): string {
switch (status) {
case "completed":
return theme.fg("success", "[x]");
return theme.fg("toolOutput", "[x]");
case "in_progress":
return theme.fg("warning", "[>]");
return theme.fg("toolOutput", "[>]");
case "pending":
return theme.fg("toolOutput", "[ ]");
default:
Expand All @@ -200,13 +203,13 @@ function formatPlanStep(step: unknown, theme: Theme): string {
return theme.fg("toolOutput", step);
}

function formatPlanCall(args: unknown, theme: Theme): string {
function formatPlanCall(args: unknown, theme: Theme, showInvalidArgs: boolean): string {
let text = theme.fg("toolTitle", theme.bold("update_plan"));
let details: UpdatePlanToolDetails;
try {
details = parseUpdatePlanOperation(args);
} catch {
return `${text}\n${invalidArgText(theme)}`;
return showInvalidArgs ? `${text}\n${invalidArgText(theme)}` : text;
}

if (details.explanation !== undefined) {
Expand Down Expand Up @@ -244,8 +247,12 @@ export function createUpdatePlanToolDefinition(): ToolDefinition<typeof updatePl
details,
};
},
renderCall(args, theme) {
return new Text(formatPlanCall(args, theme), 0, 0);
renderCall(args, theme, context) {
return new Text(
formatPlanCall(args, theme, context.argsComplete || context.executionStarted || context.isError),
0,
0,
);
},
renderResult(result, _options, theme, context) {
if (!context.isError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
getKeybindings,
Input,
matchesKey,
SELECTED_LIST_PREFIX,
Spacer,
truncateToWidth,
UNSELECTED_LIST_PREFIX,
visibleWidth,
} from "@earendil-works/diffex-tui";
import { CONFIG_DIR_NAME } from "../../../config.ts";
Expand Down Expand Up @@ -426,10 +428,14 @@ class ResourceList implements Component, Focusable {
} else {
// Resource item (cursor only on items)
const item = entry.item;
const cursor = isSelected ? "> " : " ";
const cursor = isSelected ? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) : UNSELECTED_LIST_PREFIX;
const dimmed = this.isDimmedItem(item);
const nameText = isSelected && !dimmed ? theme.bold(item.displayName) : item.displayName;
const name = dimmed ? theme.fg("dim", nameText) : nameText;
const name = dimmed
? theme.fg("dim", nameText)
: isSelected
? theme.fg("selectionAccent", nameText)
: nameText;
lines.push(
truncateToWidth(
`${cursor} ${this.renderCheckbox(item)} ${name}${this.getItemSuffix(item)}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class DiffexWelcomeHeader implements Component {
private tick = 0;
private interval: ReturnType<typeof setInterval> | undefined;
private readonly details = new Text("", 1, 0);
private detailsText: string | undefined;

constructor(ui: TUI, getCollapsedText: () => string, getExpandedText: () => string, expanded = false) {
this.ui = ui;
Expand Down Expand Up @@ -115,6 +116,7 @@ export class DiffexWelcomeHeader implements Component {
}

render(width: number): string[] {
this.updateDetails();
if (width < 4) {
return [...renderDiffexWelcomeFrame(this.tick, width).map(diffexIntroText), ...this.details.render(width)];
}
Expand Down Expand Up @@ -149,6 +151,11 @@ export class DiffexWelcomeHeader implements Component {
}

private updateDetails(): void {
this.details.setText(this.expanded ? this.getExpandedText() : this.getCollapsedText());
const detailsText = this.expanded ? this.getExpandedText() : this.getCollapsedText();
if (detailsText === this.detailsText) {
return;
}
this.detailsText = detailsText;
this.details.setText(detailsText);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
* Displays a list of string options with keyboard navigation.
*/

import { Container, getKeybindings, Spacer, Text, type TUI } from "@earendil-works/diffex-tui";
import {
Container,
getKeybindings,
SELECTED_LIST_PREFIX,
Spacer,
Text,
type TUI,
UNSELECTED_LIST_PREFIX,
} from "@earendil-works/diffex-tui";
import { theme } from "../theme/theme.ts";
import { CountdownTimer } from "./countdown-timer.ts";
import { DynamicBorder } from "./dynamic-border.ts";
Expand Down Expand Up @@ -82,8 +90,8 @@ export class ExtensionSelectorComponent extends Container {
for (let i = 0; i < this.options.length; i++) {
const isSelected = i === this.selectedIndex;
const text = isSelected
? theme.fg("accent", "→ ") + theme.fg("accent", this.options[i])
: ` ${theme.fg("text", this.options[i])}`;
? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) + theme.fg("selectionAccent", this.options[i])
: `${UNSELECTED_LIST_PREFIX}${theme.fg("text", this.options[i])}`;
this.listContainer.addChild(new Text(text, 1, 0));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Container, getKeybindings, Spacer, Text } from "@earendil-works/diffex-tui";
import {
Container,
getKeybindings,
SELECTED_LIST_PREFIX,
Spacer,
Text,
UNSELECTED_LIST_PREFIX,
} from "@earendil-works/diffex-tui";
import { type TerminalTheme, theme } from "../theme/theme.ts";
import { DIFFEX_LOGO_ART } from "./diffex-welcome.ts";
import { DynamicBorder } from "./dynamic-border.ts";
Expand Down Expand Up @@ -99,8 +106,8 @@ export class FirstTimeSetupComponent extends Container {
private addOptionList(labels: string[], selectedIndex: number): void {
for (let i = 0; i < labels.length; i++) {
const isSelected = i === selectedIndex;
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
const label = isSelected ? theme.fg("accent", labels[i]) : theme.fg("text", labels[i]);
const prefix = isSelected ? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) : UNSELECTED_LIST_PREFIX;
const label = isSelected ? theme.fg("selectionAccent", labels[i]) : theme.fg("text", labels[i]);
this.addChild(new Text(`${prefix}${label}`, 1, 0));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
fuzzyFilter,
getKeybindings,
Input,
SELECTED_LIST_PREFIX,
Spacer,
Text,
type TUI,
UNSELECTED_LIST_PREFIX,
} from "@earendil-works/diffex-tui";
import type { ModelRuntime } from "../../../core/model-runtime.ts";
import type { SettingsManager } from "../../../core/settings-manager.ts";
Expand Down Expand Up @@ -274,13 +276,13 @@ export class ModelSelectorComponent extends Container implements Focusable {

let line = "";
if (isSelected) {
const prefix = theme.fg("accent", "→ ");
const prefix = theme.fg("selectionAccent", SELECTED_LIST_PREFIX);
const modelText = `${item.id}`;
const providerBadge = theme.fg("muted", `[${item.provider}]`);
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
line = `${prefix + theme.fg("accent", modelText)} ${providerBadge}${checkmark}`;
line = `${prefix + theme.fg("selectionAccent", modelText)} ${providerBadge}${checkmark}`;
} else {
const modelText = ` ${item.id}`;
const modelText = `${UNSELECTED_LIST_PREFIX}${item.id}`;
const providerBadge = theme.fg("muted", `[${item.provider}]`);
const checkmark = isCurrent ? theme.fg("success", " ✓") : "";
line = `${modelText} ${providerBadge}${checkmark}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
fuzzyFilter,
getKeybindings,
Input,
SELECTED_LIST_PREFIX,
Spacer,
TruncatedText,
UNSELECTED_LIST_PREFIX,
} from "@earendil-works/diffex-tui";
import { theme } from "../theme/theme.ts";
import { DynamicBorder } from "./dynamic-border.ts";
Expand Down Expand Up @@ -133,11 +135,11 @@ export class OAuthSelectorComponent extends Container implements Focusable {
: "";
let line = "";
if (isSelected) {
const prefix = theme.fg("accent", "→ ");
const text = theme.fg("accent", provider.name);
const prefix = theme.fg("selectionAccent", SELECTED_LIST_PREFIX);
const text = theme.fg("selectionAccent", provider.name);
line = prefix + text + authTypeLabel + statusIndicator;
} else {
const text = ` ${theme.fg("text", provider.name)}`;
const text = `${UNSELECTED_LIST_PREFIX}${theme.fg("text", provider.name)}`;
line = text + authTypeLabel + statusIndicator;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
Input,
Key,
matchesKey,
SELECTED_LIST_PREFIX,
Spacer,
Text,
UNSELECTED_LIST_PREFIX,
} from "@earendil-works/diffex-tui";
import { getModelSearchText } from "../model-search.ts";
import { theme } from "../theme/theme.ts";
Expand Down Expand Up @@ -245,9 +247,9 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
for (let i = startIndex; i < endIndex; i++) {
const item = this.filteredItems[i]!;
const isSelected = i === this.selectedIndex;
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
const prefix = isSelected ? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) : UNSELECTED_LIST_PREFIX;
const id = item.model?.id ?? item.fullId;
const modelText = isSelected ? theme.fg("accent", id) : id;
const modelText = isSelected ? theme.fg("selectionAccent", id) : id;
const providerBadge = theme.fg("muted", item.model ? ` [${item.model.provider}]` : " [unavailable]");
const status = item.model
? allEnabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
type Focusable,
getKeybindings,
Input,
SELECTED_LIST_PREFIX,
Spacer,
Text,
truncateToWidth,
UNSELECTED_LIST_PREFIX,
visibleWidth,
} from "@earendil-works/diffex-tui";
import { KeybindingsManager } from "../../../core/keybindings.ts";
Expand Down Expand Up @@ -473,7 +475,7 @@ class SessionList implements Component, Focusable {
}

// Cursor
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
const cursor = isSelected ? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) : UNSELECTED_LIST_PREFIX;

// Calculate available width for message
const prefixWidth = visibleWidth(prefix);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
getKeybindings,
Input,
type Keybinding,
SELECTED_LIST_PREFIX,
Spacer,
sliceByColumn,
Text,
truncateToWidth,
UNSELECTED_LIST_PREFIX,
visibleWidth,
wrapTextWithAnsi,
} from "@earendil-works/diffex-tui";
Expand Down Expand Up @@ -686,7 +688,7 @@ class TreeList implements Component {
const isSelected = i === this.selectedIndex;

// Build line: cursor + prefix + path marker + label + content
const cursor = isSelected ? theme.fg("accent", "› ") : " ";
const cursor = isSelected ? theme.fg("selectionAccent", SELECTED_LIST_PREFIX) : UNSELECTED_LIST_PREFIX;

// If multiple roots, shift display (roots at 0, not 1)
const displayIndent = this.multipleRoots ? Math.max(0, flatNode.indent - 1) : flatNode.indent;
Expand Down
Loading