Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4a14c28
TUI parity pass: streaming-perf foundation, reconnect, grid + lane ma…
arul28 May 29, 2026
f8851d4
Merge remote-tracking branch 'origin/main' into ade/tui-parity-pass-1…
arul28 May 29, 2026
eeac745
TUI live-feedback fixes: grid colors, backspace, footer green, new-ch…
arul28 May 29, 2026
fe7796b
Phase 3 (start): model-picker polish + Codex preset cycle fix
arul28 May 29, 2026
38752bd
Phase 3: footer fast/reasoning reachability (P0-D)
arul28 May 29, 2026
314e9f5
Phase 7 (start): animated shimmer "working" indicator
arul28 May 29, 2026
f79334b
TUI Phase 3/4/5 (Codex): model-picker unification, full mouse + Cmd-K…
arul28 May 29, 2026
1f29ec9
Review fixes for Codex Phase 3/4/5
arul28 May 29, 2026
7f382d1
UI cohesion pass (frontend-design): palette selection, awaiting signal
arul28 May 29, 2026
729aff3
TUI Phase 6 + 7: colorized scrollable diffs, PR mutations, visual del…
arul28 May 29, 2026
43b34b0
TUI: frontend-design cohesion pass on Phase 6 diff header
arul28 May 29, 2026
8aa0f46
roadmap: mark Phases 3–7 done (Phase 6 diffs/PR, Phase 7 motion)
arul28 May 29, 2026
829c459
TUI wordmark: solid logo-style letters with a 3D drop shadow
arul28 May 29, 2026
9475f0b
TUI: fix backspace + Ctrl+K toggle; layered 3D wordmark
arul28 May 29, 2026
5b63bd6
TUI visual pass: model picker redesign + chat-info cards
arul28 May 29, 2026
f9f7767
TUI: shared design kit + model picker → desktop icon-rail layout
arul28 May 29, 2026
4d79956
TUI cohesive pass: drawer, command palette, approval card on the desi…
arul28 May 29, 2026
e91de68
TUI feedback batch: cursor keys, picker polish, splash-only, lane layout
arul28 May 29, 2026
2a82a0b
TUI: picker keyboard nav, down→picker, adaptive slash palette, empty-…
arul28 May 29, 2026
2a815f1
TUI: restore per-lane boxes in the drawer
arul28 May 29, 2026
fa50c81
TUI: rewrite model picker — bounded grouped list, sticky settings, cl…
arul28 May 29, 2026
2785ffd
TUI: open both side panes on launch (always-splash, no crash)
arul28 May 29, 2026
040ea29
TUI Phase 8 (trimmed): picker hit-test fix, Claude PTY scrollback, /u…
arul28 May 30, 2026
264a355
TUI Phase 8 audit fixes: picker chip/Apply click rects, feedback time…
arul28 May 30, 2026
911dc8d
ship: merge origin/main into lane (resolve ModelPicker conflicts)
arul28 May 30, 2026
762e1f0
ship: iter1 — address review (picker geometry+wrap, feedback category…
arul28 May 30, 2026
99e30fb
ship: iter1 fixup — restore lane ModelPicker layout/types (keep rewri…
arul28 May 30, 2026
a883e93
ship: iter2 — make TerminalPane >500-chunk desync test deterministic …
arul28 May 30, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ package-lock.json
/apps/desktop/release-alpha
/apps/desktop/release-beta
apps/desktop/resources/runtime/ade-*

.claude/scheduled_tasks.lock
2 changes: 2 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/HeaderFooter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe("FooterControls", () => {
provider="codex"
modelDisplay="GPT-5.5"
permissionLabel="full-auto"
permissionDetail="never · danger-full-access"
fastMode
/>,
);
Expand All @@ -92,6 +93,7 @@ describe("FooterControls", () => {
expect(frame).toContain("GPT-5.5");
expect(frame).toContain("fast");
expect(frame).toContain("full-auto");
expect(frame).toContain("full-auto ·");
});

it("renders the resting hint strip with lanes/pane/chat-info/cmds/help", () => {
Expand Down
4 changes: 3 additions & 1 deletion apps/ade-cli/src/tuiClient/__tests__/Palettes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ describe("SlashPalette", () => {
).lastFrame() ?? "");
const lines = frame.split("\n").filter(Boolean);

expect(lines).toHaveLength(8);
// 6 command rows (MIN_VISIBLE_ROWS, no maxRows budget passed) + header +
// selected-summary + footer.
expect(lines).toHaveLength(9);
expect(lines.every((line) => /^[┌│└]/.test(line) && /[┐│┘]$/.test(line))).toBe(true);
expect(frame).toContain("/feedback");
expect(frame).toContain("Submit ADE feedback to GitHub issues");
Expand Down
172 changes: 158 additions & 14 deletions apps/ade-cli/src/tuiClient/__tests__/RightPane.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import React from "react";
import { describe, expect, it } from "vitest";
import { render } from "ink-testing-library";
import { LANE_DETAIL_ACTIONS, LANE_DETAIL_PR_ACTION_INDEX, laneDetailsInteractionLayout, RightPane } from "../components/RightPane";
import { LANE_DETAIL_ACTIONS, LANE_DETAIL_PR_ACTION_INDEX, feedbackStateFromContent, laneDetailsInteractionLayout, rightPaneScrollableRowCount, RightPane } from "../components/RightPane";
import { feedbackFormToFormValues } from "../feedbackForm";
import { buildFeedbackDraftInput } from "../feedback";
import type { LaneSummary } from "../../../../desktop/src/shared/types/lanes";

describe("rightPaneScrollableRowCount", () => {
it("counts details body lines and list rows; flows full diff bodies for scrolling", () => {
expect(rightPaneScrollableRowCount({ kind: "details", title: "t", body: "a\nb\nc" })).toBe(3);
expect(rightPaneScrollableRowCount({ kind: "list", title: "t", rows: ["a", "b"] })).toBe(2);
expect(rightPaneScrollableRowCount({ kind: "empty" })).toBe(0);
const bigBody = Array.from({ length: 50 }, (_, i) => `line ${i}`).join("\n");
// 1 header row + all 50 body rows (diffs scroll in full, capped per file at 600).
expect(rightPaneScrollableRowCount({ kind: "diff", title: "d", files: [{ path: "a.ts", body: bigBody }] })).toBe(51);
// Per-file cap: 600 body rows shown + 1 header + 1 "more lines" row.
const huge = Array.from({ length: 650 }, (_, i) => `+line ${i}`).join("\n");
expect(rightPaneScrollableRowCount({ kind: "diff", title: "d", files: [{ path: "a.ts", body: huge }] })).toBe(602);
});
});

function stripAnsi(text: string): string {
return text.replace(/\u001b(?:\[[0-9;]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001b\\))/g, "");
}
Expand Down Expand Up @@ -511,32 +527,45 @@ describe("RightPane setup panes", () => {
expect(frame).toContain("enter deletes this lane");
});

it("renders model setup rows and selected row detail", () => {
it("renders the unified model picker with model list and settings footer", () => {
const result = render(
<RightPane
content={{
kind: "model-setup",
rows: [
{ kind: "provider", label: "Provider", value: "Claude", detail: "Claude CLI", cyclable: true },
{ kind: "model", label: "Model", value: "Sonnet", detail: "3 available", cyclable: true },
kind: "model-picker",
surface: "chat",
query: "",
searchMode: false,
showAll: false,
selection: { kind: "provider", provider: "claude" },
providerTabKey: null,
focusedIndex: 0,
footerFocus: null,
settingsRows: [
{ kind: "reasoning", label: "Reasoning", value: "high", detail: "low, medium, high", cyclable: true },
{ kind: "permission", label: "Permissions", value: "auto", detail: "default · auto", cyclable: true },
],
}}
selectedIndex={2}
modelPickerInputs={{
models: [
{ id: "anthropic/claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", isDefault: true },
],
favorites: [],
recents: [],
activeModelId: "anthropic/claude-sonnet-4-6",
activeReasoningEffort: "high",
}}
focused
width={80}
/>,
);
const frame = stripAnsi(result.lastFrame() ?? "");

expect(frame).toContain("MODEL");
expect(frame).toContain("Provider: Claude");
expect(frame).toContain("Model: Sonnet");
expect(frame).toContain("Reasoning: high");
expect(frame).toContain("low, medium, high");
expect(frame).toContain("Permissions: auto");
expect(frame).toContain("↑↓ rows · ←→ change · ↵ apply · esc close");
expect(frame).toContain("Claude Sonnet 4.6");
// Reasoning is shown in the settings footer, not as an inline per-model
// "think high" chip (that detail was removed from model rows).
expect(frame).toContain("reasoning");
expect(frame).not.toContain("think high");
});
});

Expand All @@ -557,6 +586,121 @@ describe("RightPane details", () => {

expect(frame).toContain("SKILLS");
expect(frame).toContain("line 1");
expect(frame).toContain("… 14 more lines");
expect(frame).toContain("↓ 14 more lines");
});

it("renders context usage as a visual pane", () => {
const result = render(
<RightPane
content={{
kind: "context-usage",
title: "Context",
usage: {
totalTokens: 12000,
maxTokens: 20000,
percentage: 60,
model: "gpt-5.5",
categories: [
{ name: "messages", tokens: 8000, percentage: 40 },
{ name: "tools", tokens: 4000, percentage: 20 },
],
},
}}
focused
width={80}
/>,
);
const frame = stripAnsi(result.lastFrame() ?? "");

expect(frame).toContain("CONTEXT");
expect(frame).toContain("gpt-5.5");
expect(frame).toContain("12.0k / 20.0k (60%)");
expect(frame).toContain("messages");
});
});

describe("RightPane feedback form", () => {
// The feedback pane render is exercised end-to-end through the deterministic
// helper contract below (state rebuild -> serialize -> daemon draft). Full-frame
// string assertions are intentionally avoided: ink-testing-library leaks tall
// frames between renders in this suite, making pixel-frame matches flaky.

it("feedbackStateFromContent rebuilds the framework-free form state", () => {
const content = {
kind: "form" as const,
title: "Feedback",
command: "feedback" as const,
fields: [],
feedback: {
type: "idea" as const,
body: "add dark mode\nplease",
showContext: false,
provider: "anthropic",
model: "opus",
lane: "L",
lastError: "boom",
},
};
const state = feedbackStateFromContent(content);
expect(state.type).toBe("idea");
expect(state.text).toBe("add dark mode\nplease");
expect(state.showContext).toBe(false);
expect(state.context).toEqual({ provider: "anthropic", model: "opus", lane: "L", lastError: "boom" });
});

it("submit path: feedbackStateFromContent -> feedbackFormToFormValues -> buildFeedbackDraftInput keeps the multiline body", () => {
const content = {
kind: "form" as const,
title: "Feedback",
command: "feedback" as const,
fields: [],
feedback: {
type: "bug" as const,
body: "crash on launch\nstep one\nstep two",
showContext: true,
provider: "anthropic",
model: "opus",
lane: "my-lane",
lastError: "boom",
},
};
const values = feedbackFormToFormValues(feedbackStateFromContent(content));
expect(values.category).toBe("bug");
expect(values.summary).toBe("crash on launch");
const draft = buildFeedbackDraftInput(values);
expect(draft.category).toBe("bug");
expect(draft.summary).toBe("crash on launch");
if (draft.category === "bug") {
expect(draft.stepsToReproduce).toBe("crash on launch\nstep one\nstep two");
}
expect(draft.additionalContext).toContain("--- Context ---");
expect(draft.additionalContext).toContain("Provider/Model: anthropic / opus");
expect(draft.additionalContext).toContain("Lane: my-lane");
expect(draft.additionalContext).toContain("Last error/notice: boom");
});

it("omits the context footer from the serialized draft when showContext is false", () => {
const content = {
kind: "form" as const,
title: "Feedback",
command: "feedback" as const,
fields: [],
feedback: { type: "idea" as const, body: "an idea", showContext: false, lane: "L" },
};
const values = feedbackFormToFormValues(feedbackStateFromContent(content));
expect(values.additionalContext).toBeUndefined();
});

it("idea type maps to a feature draft", () => {
const content = {
kind: "form" as const,
title: "Feedback",
command: "feedback" as const,
fields: [],
feedback: { type: "idea" as const, body: "add dark mode\nplease", showContext: false },
};
const draft = buildFeedbackDraftInput(feedbackFormToFormValues(feedbackStateFromContent(content)));
expect(draft.category).toBe("feature");
expect(draft.summary).toBe("add dark mode");
});
});
81 changes: 81 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/RightPane.usage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from "react";
import { describe, expect, it } from "vitest";
import { render } from "ink-testing-library";
import { RightPane } from "../components/RightPane";
import type { RightPaneContent } from "../types";
import { SpinTickProvider } from "../spinTick";

function renderPane(content: RightPaneContent, width = 48) {
return render(
<SpinTickProvider active={false}>
<RightPane content={content} width={width} />
</SpinTickProvider>,
);
}

describe("RightPane usage", () => {
it("renders a quota window with percent + reset countdown and the session block", () => {
const resetAt = Math.round(Date.now() / 1000) + 2 * 3600; // 2h out
const { lastFrame } = renderPane({
kind: "usage",
title: "Usage",
quotaWindows: [{ id: "rate-limit", label: "Rate limit", percent: 42, resetAt }],
session: { input: 12_000, output: 3_400, cost: 0.42 },
});
const text = lastFrame() ?? "";
expect(text).toContain("USAGE");
expect(text).toContain("Rate limit");
expect(text).toContain("42%");
// Live reset countdown marker.
expect(text).toContain("↻");
// Session block.
expect(text).toContain("This session");
expect(text).toContain("12.0k");
expect(text).toContain("3.4k");
expect(text).toContain("$0.42");
});

it("degrades to the session block when quota windows are unavailable", () => {
const { lastFrame } = renderPane({
kind: "usage",
title: "Usage",
quotaWindows: undefined,
session: { input: 500, output: 100, cost: 0 },
});
const text = lastFrame() ?? "";
expect(text).toContain("Quota windows unavailable.");
expect(text).toContain("This session");
expect(text).toContain("$0.00");
});

it("renders a loading state while the snapshot is being fetched", () => {
const { lastFrame } = renderPane({ kind: "usage", title: "Usage", loading: true });
expect(lastFrame() ?? "").toContain("Loading usage…");
});

it("surfaces an error while keeping the pane title", () => {
const { lastFrame } = renderPane({
kind: "usage",
title: "Usage",
error: "daemon offline",
session: { input: 1, output: 2, cost: 3 },
});
const text = lastFrame() ?? "";
expect(text).toContain("USAGE");
expect(text).toContain("Usage unavailable");
expect(text).toContain("daemon offline");
});

it("renders a near-full window's percent (≥95% danger escalation) with no session", () => {
const { lastFrame } = renderPane({
kind: "usage",
title: "Usage",
quotaWindows: [{ id: "rate-limit", label: "Rate limit", percent: 97, resetAt: null }],
session: null,
});
const text = lastFrame() ?? "";
expect(text).toContain("Rate limit");
expect(text).toContain("97%");
expect(text).toContain("No session usage yet.");
});
});
Loading
Loading