generated from nhcarrigan/template
a4e6788573
## Summary This PR bundles a collection of new features and quality-of-life improvements identified during a Claude CLI 2.1.50 audit. - **Tab status indicator** — Tab stays yellow until the greeting is responded to, then turns green. Fixed disconnect not resetting to grey. Closes #157 - **Auth status display** — New "Account" section in settings sidebar showing login status, email, org, API key source, and Hikari override indicator. Includes login/logout buttons. Closes #153 - **CLI version badge** — New "Supported" badge showing the highest audited CLI version, colour-coded green/amber/red based on installed vs supported version. Closes #154 (bump to 2.1.50) - **Rate limit events** — `rate_limit_event` messages from the stream are now parsed and shown as amber `[rate-limit]` lines in the terminal instead of being silently dropped. Closes #155 - **"Prompt is too long" handling** — Detects this error in assistant messages and shows a ⚡ Compact Conversation button to send `/compact` directly. Closes #158 - **`last_assistant_message` in Agent Monitor** — Extracts the agent's final output from the `ToolResult` content block in the JSON stream and displays it as a snippet on completed agent cards. Closes #156 - **`--worktree` flag** — New "Worktree isolation" toggle in session settings passes `--worktree` to Claude Code. Hook events (`WorktreeCreate`/`WorktreeRemove`) are displayed as green `[worktree]` lines. Closes #152, Closes #150 - **ConfigChange hook events** — `[ConfigChange Hook]` stderr events are now displayed as cyan `[config]` lines instead of errors. Closes #151 - **`CLAUDE_CODE_DISABLE_1M_CONTEXT` toggle** — New "Disable 1M context" setting in session configuration injects this env var into the Claude process. Closes #154 ## Test plan - [ ] Tab status indicator: start a new session and verify the tab stays yellow until Claude responds to the greeting, then turns green - [ ] Auth status: open settings and verify the Account section shows correct login info - [ ] CLI version badge: verify the "Supported 2.1.50" badge shows green when CLI matches - [ ] Rate limit events: unit tests cover parsing; amber `[rate-limit]` lines display correctly - [ ] Compact button: unit tests cover detection; button renders correctly in terminal - [ ] Agent Monitor: use the Task tool and verify completed agent cards show a message snippet - [ ] Worktree: enable toggle, start session, verify `--worktree` flag appears in process args - [ ] ConfigChange: hook events display as `[config]` lines rather than errors - [ ] Disable 1M context: enable toggle, start session, verify `CLAUDE_CODE_DISABLE_1M_CONTEXT=1` in `/proc/<pid>/environ` ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #159 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
265 lines
7.8 KiB
TypeScript
265 lines
7.8 KiB
TypeScript
/**
|
|
* Terminal Component Tests
|
|
*
|
|
* Tests the pure helper functions extracted from the Terminal component:
|
|
* - getLineClass: maps line types to CSS class names
|
|
* - getLinePrefix: maps line types to display prefixes
|
|
* - formatTime: formats a Date as "HH:MM AM/PM"
|
|
* - isToolContentLong: checks if tool content exceeds collapse threshold
|
|
* - truncateToolContent: truncates long tool content with ellipsis
|
|
*
|
|
* Manual testing checklist:
|
|
* - [ ] rate-limit lines appear in amber
|
|
* - [ ] error lines appear in red
|
|
* - [ ] tool lines appear in purple
|
|
* - [ ] system lines appear in grey italic
|
|
* - [ ] user lines appear in cyan
|
|
* - [ ] assistant lines appear in primary text colour
|
|
* - [ ] long tool content is collapsed by default with a toggle button
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
// Mirror functions from Terminal.svelte for isolated testing
|
|
|
|
function getLineClass(type: string): string {
|
|
switch (type) {
|
|
case "user":
|
|
return "terminal-user";
|
|
case "assistant":
|
|
return "terminal-assistant";
|
|
case "system":
|
|
return "terminal-system italic";
|
|
case "tool":
|
|
return "terminal-tool";
|
|
case "error":
|
|
return "terminal-error";
|
|
case "thinking":
|
|
return "terminal-thinking";
|
|
case "rate-limit":
|
|
return "terminal-rate-limit";
|
|
case "compact-prompt":
|
|
return "terminal-compact-prompt";
|
|
case "worktree":
|
|
return "terminal-worktree";
|
|
case "config-change":
|
|
return "terminal-config-change";
|
|
default:
|
|
return "terminal-default";
|
|
}
|
|
}
|
|
|
|
function getLinePrefix(type: string): string {
|
|
switch (type) {
|
|
case "user":
|
|
return ">";
|
|
case "assistant":
|
|
return "";
|
|
case "system":
|
|
return "[system]";
|
|
case "tool":
|
|
return "[tool]";
|
|
case "error":
|
|
return "[error]";
|
|
case "rate-limit":
|
|
return "[rate-limit]";
|
|
case "worktree":
|
|
return "[worktree]";
|
|
case "config-change":
|
|
return "[config]";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function formatTime(date: Date): string {
|
|
return date.toLocaleTimeString("en-US", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
const TOOL_COLLAPSE_THRESHOLD = 60;
|
|
|
|
function isToolContentLong(content: string): boolean {
|
|
return content.length > TOOL_COLLAPSE_THRESHOLD;
|
|
}
|
|
|
|
function truncateToolContent(content: string): string {
|
|
return content.slice(0, TOOL_COLLAPSE_THRESHOLD) + "…";
|
|
}
|
|
|
|
// ---
|
|
|
|
describe("getLineClass", () => {
|
|
it("returns terminal-user for user lines", () => {
|
|
expect(getLineClass("user")).toBe("terminal-user");
|
|
});
|
|
|
|
it("returns terminal-assistant for assistant lines", () => {
|
|
expect(getLineClass("assistant")).toBe("terminal-assistant");
|
|
});
|
|
|
|
it("returns terminal-system italic for system lines", () => {
|
|
expect(getLineClass("system")).toBe("terminal-system italic");
|
|
});
|
|
|
|
it("returns terminal-tool for tool lines", () => {
|
|
expect(getLineClass("tool")).toBe("terminal-tool");
|
|
});
|
|
|
|
it("returns terminal-error for error lines", () => {
|
|
expect(getLineClass("error")).toBe("terminal-error");
|
|
});
|
|
|
|
it("returns terminal-thinking for thinking lines", () => {
|
|
expect(getLineClass("thinking")).toBe("terminal-thinking");
|
|
});
|
|
|
|
it("returns terminal-rate-limit for rate-limit lines", () => {
|
|
expect(getLineClass("rate-limit")).toBe("terminal-rate-limit");
|
|
});
|
|
|
|
it("returns terminal-compact-prompt for compact-prompt lines", () => {
|
|
expect(getLineClass("compact-prompt")).toBe("terminal-compact-prompt");
|
|
});
|
|
|
|
it("returns terminal-worktree for worktree lines", () => {
|
|
expect(getLineClass("worktree")).toBe("terminal-worktree");
|
|
});
|
|
|
|
it("returns terminal-config-change for config-change lines", () => {
|
|
expect(getLineClass("config-change")).toBe("terminal-config-change");
|
|
});
|
|
|
|
it("returns terminal-default for unknown line types", () => {
|
|
expect(getLineClass("unknown")).toBe("terminal-default");
|
|
expect(getLineClass("")).toBe("terminal-default");
|
|
expect(getLineClass("random-future-type")).toBe("terminal-default");
|
|
});
|
|
});
|
|
|
|
describe("getLinePrefix", () => {
|
|
it("returns > for user lines", () => {
|
|
expect(getLinePrefix("user")).toBe(">");
|
|
});
|
|
|
|
it("returns empty string for assistant lines", () => {
|
|
expect(getLinePrefix("assistant")).toBe("");
|
|
});
|
|
|
|
it("returns [system] for system lines", () => {
|
|
expect(getLinePrefix("system")).toBe("[system]");
|
|
});
|
|
|
|
it("returns [tool] for tool lines", () => {
|
|
expect(getLinePrefix("tool")).toBe("[tool]");
|
|
});
|
|
|
|
it("returns [error] for error lines", () => {
|
|
expect(getLinePrefix("error")).toBe("[error]");
|
|
});
|
|
|
|
it("returns [rate-limit] for rate-limit lines", () => {
|
|
expect(getLinePrefix("rate-limit")).toBe("[rate-limit]");
|
|
});
|
|
|
|
it("returns empty string for compact-prompt lines (button renders instead)", () => {
|
|
expect(getLinePrefix("compact-prompt")).toBe("");
|
|
});
|
|
|
|
it("returns [worktree] for worktree lines", () => {
|
|
expect(getLinePrefix("worktree")).toBe("[worktree]");
|
|
});
|
|
|
|
it("returns [config] for config-change lines", () => {
|
|
expect(getLinePrefix("config-change")).toBe("[config]");
|
|
});
|
|
|
|
it("returns empty string for thinking lines (no prefix)", () => {
|
|
expect(getLinePrefix("thinking")).toBe("");
|
|
});
|
|
|
|
it("returns empty string for unknown line types", () => {
|
|
expect(getLinePrefix("unknown")).toBe("");
|
|
expect(getLinePrefix("")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("formatTime", () => {
|
|
it("formats time in 12-hour format with AM/PM", () => {
|
|
const date = new Date(2026, 1, 7, 14, 35);
|
|
const formatted = formatTime(date);
|
|
expect(formatted).toMatch(/\d{2}:\d{2}\s?(AM|PM)/i);
|
|
});
|
|
|
|
it("formats afternoon times correctly", () => {
|
|
const date = new Date(2026, 1, 7, 14, 35);
|
|
const formatted = formatTime(date);
|
|
expect(formatted).toContain("02:35");
|
|
expect(formatted.toUpperCase()).toContain("PM");
|
|
});
|
|
|
|
it("formats morning times correctly", () => {
|
|
const date = new Date(2026, 1, 7, 9, 5);
|
|
const formatted = formatTime(date);
|
|
expect(formatted).toContain("09:05");
|
|
expect(formatted.toUpperCase()).toContain("AM");
|
|
});
|
|
|
|
it("formats midnight correctly", () => {
|
|
const date = new Date(2026, 1, 7, 0, 0);
|
|
const formatted = formatTime(date);
|
|
expect(formatted).toContain("12:00");
|
|
expect(formatted.toUpperCase()).toContain("AM");
|
|
});
|
|
|
|
it("formats noon correctly", () => {
|
|
const date = new Date(2026, 1, 7, 12, 0);
|
|
const formatted = formatTime(date);
|
|
expect(formatted).toContain("12:00");
|
|
expect(formatted.toUpperCase()).toContain("PM");
|
|
});
|
|
});
|
|
|
|
describe("isToolContentLong", () => {
|
|
it("returns false for content at or below the threshold", () => {
|
|
const exactThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD);
|
|
expect(isToolContentLong(exactThreshold)).toBe(false);
|
|
});
|
|
|
|
it("returns true for content exceeding the threshold", () => {
|
|
const overThreshold = "x".repeat(TOOL_COLLAPSE_THRESHOLD + 1);
|
|
expect(isToolContentLong(overThreshold)).toBe(true);
|
|
});
|
|
|
|
it("returns false for short content", () => {
|
|
expect(isToolContentLong("short")).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty content", () => {
|
|
expect(isToolContentLong("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("truncateToolContent", () => {
|
|
it("truncates content to the threshold length with an ellipsis", () => {
|
|
const long = "x".repeat(100);
|
|
const result = truncateToolContent(long);
|
|
expect(result).toBe("x".repeat(TOOL_COLLAPSE_THRESHOLD) + "…");
|
|
});
|
|
|
|
it("keeps content shorter than threshold unchanged (plus ellipsis)", () => {
|
|
const short = "hello";
|
|
const result = truncateToolContent(short);
|
|
expect(result).toBe("hello…");
|
|
});
|
|
|
|
it("uses the unicode ellipsis character (not three dots)", () => {
|
|
const long = "x".repeat(100);
|
|
const result = truncateToolContent(long);
|
|
expect(result.endsWith("…")).toBe(true);
|
|
expect(result.endsWith("...")).toBe(false);
|
|
});
|
|
});
|