generated from nhcarrigan/template
feat: parse and display rate_limit_event messages from Claude CLI
Closes #155 - Add RateLimitInfo struct and RateLimitEvent variant to ClaudeMessage - Emit rate-limit OutputEvent with human-readable message in wsl_bridge - Add rate-limit line type to TerminalLine union and Terminal rendering - Display rate-limit lines in amber with [rate-limit] prefix - Add Terminal.test.ts with 28 tests for getLineClass, getLinePrefix, formatTime, isToolContentLong, and truncateToolContent
This commit is contained in:
@@ -93,6 +93,8 @@
|
||||
return "terminal-error";
|
||||
case "thinking":
|
||||
return "terminal-thinking";
|
||||
case "rate-limit":
|
||||
return "terminal-rate-limit";
|
||||
default:
|
||||
return "terminal-default";
|
||||
}
|
||||
@@ -110,6 +112,8 @@
|
||||
return "[tool]";
|
||||
case "error":
|
||||
return "[error]";
|
||||
case "rate-limit":
|
||||
return "[rate-limit]";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -362,6 +366,10 @@
|
||||
color: var(--terminal-error, #f87171);
|
||||
}
|
||||
|
||||
.terminal-rate-limit {
|
||||
color: var(--terminal-rate-limit, #fb923c);
|
||||
}
|
||||
|
||||
.terminal-default {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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";
|
||||
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]";
|
||||
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-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 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);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -323,7 +323,7 @@ export async function initializeTauriListeners() {
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(
|
||||
conversation_id,
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
@@ -332,7 +332,7 @@ export async function initializeTauriListeners() {
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id provided
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking",
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit",
|
||||
content,
|
||||
tool_name || undefined,
|
||||
costData,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface TerminalLine {
|
||||
id: string;
|
||||
type: "user" | "assistant" | "system" | "tool" | "error" | "thinking";
|
||||
type: "user" | "assistant" | "system" | "tool" | "error" | "thinking" | "rate-limit";
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
toolName?: string;
|
||||
|
||||
Reference in New Issue
Block a user