Some cleanup

This commit is contained in:
cte 2026-01-07 23:47:16 -08:00
parent 245008a43c
commit c83e67eedb
37 changed files with 30 additions and 1450 deletions

View file

@ -1,187 +0,0 @@
/**
* Tests for Escape key cancel/pause functionality
*
* When the CLI is in a loading state (streaming LLM API calls),
* pressing Escape should send a "cancelTask" message to the extension,
* similar to the Cancel button in the webview-ui.
*/
describe("Escape key cancel behavior", () => {
describe("escape key detection logic", () => {
/**
* Simulates the escape key handling logic from App.tsx
*
* @param key - The key object from ink's useInput
* @param isLoading - Whether the app is currently loading (streaming)
* @param hasHostRef - Whether the extension host reference is available
* @param isPickerOpen - Whether an autocomplete picker is currently open
* @returns An object describing what action should be taken
*/
const handleEscapeKey = (
key: { escape: boolean },
isLoading: boolean,
hasHostRef: boolean,
isPickerOpen: boolean,
): { shouldCancel: boolean; reason?: string } => {
if (!key.escape) {
return { shouldCancel: false, reason: "Not escape key" }
}
if (!isLoading) {
return { shouldCancel: false, reason: "Not in loading state" }
}
if (!hasHostRef) {
return { shouldCancel: false, reason: "No host reference" }
}
if (isPickerOpen) {
// Let picker handle escape first
return { shouldCancel: false, reason: "Picker is open" }
}
return { shouldCancel: true }
}
it("should cancel task when escape is pressed during loading", () => {
const result = handleEscapeKey(
{ escape: true },
true, // isLoading
true, // hasHostRef
false, // isPickerOpen
)
expect(result.shouldCancel).toBe(true)
})
it("should not cancel when not loading", () => {
const result = handleEscapeKey(
{ escape: true },
false, // isLoading - not loading
true, // hasHostRef
false, // isPickerOpen
)
expect(result.shouldCancel).toBe(false)
expect(result.reason).toBe("Not in loading state")
})
it("should not cancel when host reference is not available", () => {
const result = handleEscapeKey(
{ escape: true },
true, // isLoading
false, // hasHostRef - no host reference
false, // isPickerOpen
)
expect(result.shouldCancel).toBe(false)
expect(result.reason).toBe("No host reference")
})
it("should not cancel when picker is open", () => {
const result = handleEscapeKey(
{ escape: true },
true, // isLoading
true, // hasHostRef
true, // isPickerOpen - picker is open
)
expect(result.shouldCancel).toBe(false)
expect(result.reason).toBe("Picker is open")
})
it("should not do anything for non-escape keys", () => {
const result = handleEscapeKey(
{ escape: false }, // Not escape key
true, // isLoading
true, // hasHostRef
false, // isPickerOpen
)
expect(result.shouldCancel).toBe(false)
expect(result.reason).toBe("Not escape key")
})
})
describe("cancel message format", () => {
it("should create the correct message format for cancelTask", () => {
// The message sent to extension should match webview-ui format
const cancelMessage = { type: "cancelTask" }
expect(cancelMessage).toEqual({ type: "cancelTask" })
expect(cancelMessage.type).toBe("cancelTask")
})
it("should match the webview-ui cancel message format", () => {
// From webview-ui/src/components/chat/ChatView.tsx line 750:
// vscode.postMessage({ type: "cancelTask" })
const webviewCancelMessage = { type: "cancelTask" }
const cliCancelMessage = { type: "cancelTask" }
expect(cliCancelMessage).toEqual(webviewCancelMessage)
})
})
describe("loading state scenarios", () => {
/**
* The isLoading state in the CLI store represents:
* - Active API request in progress
* - Task is streaming responses
* - Agent is "thinking" or processing
*/
it("should identify loading state during agent response", () => {
const view = "AgentResponse"
const isLoading = true
// During agent response, cancel should be available
expect(view).toBe("AgentResponse")
expect(isLoading).toBe(true)
})
it("should identify loading state during tool use", () => {
const view = "ToolUse"
const isLoading = true
// During tool use, cancel should be available
expect(view).toBe("ToolUse")
expect(isLoading).toBe(true)
})
it("should not identify loading state during user input", () => {
const view = "UserInput"
const isLoading = false
// During user input, no need for cancel
expect(view).toBe("UserInput")
expect(isLoading).toBe(false)
})
})
describe("cancel behavior expectations", () => {
it("should pause the task (not terminate)", () => {
// The cancelTask message pauses the task, allowing the user to:
// 1. Review the current state
// 2. Provide additional input
// 3. Resume the task by typing something
const cancelBehavior = {
action: "pause",
terminates: false,
allowsResume: true,
resumeMethod: "user provides input",
}
expect(cancelBehavior.action).toBe("pause")
expect(cancelBehavior.terminates).toBe(false)
expect(cancelBehavior.allowsResume).toBe(true)
})
it("should allow resuming by typing after cancel", () => {
// After cancel, the user can resume by typing a message
const postCancelState = {
isLoading: false, // Loading stops
canTypeMessage: true, // User can type
messageResumesTask: true, // Typing resumes the task
}
expect(postCancelState.isLoading).toBe(false)
expect(postCancelState.canTypeMessage).toBe(true)
expect(postCancelState.messageResumesTask).toBe(true)
})
})
})

View file

@ -1,568 +0,0 @@
/**
* Tests for MultilineTextInput component
*/
describe("MultilineTextInput", () => {
describe("cursor position calculations", () => {
// Test the getCursorPosition logic
const getCursorPosition = (value: string, cursorIndex: number): { line: number; col: number } => {
const lines = value.split("\n")
let pos = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]!
const lineEnd = pos + line.length
if (cursorIndex <= lineEnd) {
return { line: i, col: cursorIndex - pos }
}
pos = lineEnd + 1 // +1 for newline
}
// Cursor at very end
return { line: lines.length - 1, col: (lines[lines.length - 1] || "").length }
}
// Test the getIndexFromPosition logic
const getIndexFromPosition = (value: string, line: number, col: number): number => {
const lines = value.split("\n")
let index = 0
for (let i = 0; i < line && i < lines.length; i++) {
index += lines[i]!.length + 1 // +1 for newline
}
const targetLine = lines[line] || ""
index += Math.min(col, targetLine.length)
return index
}
it("should calculate cursor position for single line", () => {
const value = "hello"
expect(getCursorPosition(value, 0)).toEqual({ line: 0, col: 0 })
expect(getCursorPosition(value, 2)).toEqual({ line: 0, col: 2 })
expect(getCursorPosition(value, 5)).toEqual({ line: 0, col: 5 })
})
it("should calculate cursor position for multiple lines", () => {
const value = "hello\nworld"
// "hello" is 5 chars, newline at index 5
// "world" starts at index 6
expect(getCursorPosition(value, 0)).toEqual({ line: 0, col: 0 })
expect(getCursorPosition(value, 5)).toEqual({ line: 0, col: 5 }) // End of first line
expect(getCursorPosition(value, 6)).toEqual({ line: 1, col: 0 }) // Start of second line
expect(getCursorPosition(value, 8)).toEqual({ line: 1, col: 2 }) // Middle of second line
expect(getCursorPosition(value, 11)).toEqual({ line: 1, col: 5 }) // End of second line
})
it("should calculate cursor position for three lines", () => {
const value = "foo\nbar\nbaz"
// "foo" = 3 chars, newline at 3
// "bar" starts at 4, ends at 6, newline at 7
// "baz" starts at 8
expect(getCursorPosition(value, 0)).toEqual({ line: 0, col: 0 })
expect(getCursorPosition(value, 3)).toEqual({ line: 0, col: 3 })
expect(getCursorPosition(value, 4)).toEqual({ line: 1, col: 0 })
expect(getCursorPosition(value, 7)).toEqual({ line: 1, col: 3 })
expect(getCursorPosition(value, 8)).toEqual({ line: 2, col: 0 })
expect(getCursorPosition(value, 11)).toEqual({ line: 2, col: 3 })
})
it("should calculate index from position for single line", () => {
const value = "hello"
expect(getIndexFromPosition(value, 0, 0)).toBe(0)
expect(getIndexFromPosition(value, 0, 2)).toBe(2)
expect(getIndexFromPosition(value, 0, 5)).toBe(5)
})
it("should calculate index from position for multiple lines", () => {
const value = "hello\nworld"
expect(getIndexFromPosition(value, 0, 0)).toBe(0)
expect(getIndexFromPosition(value, 0, 5)).toBe(5)
expect(getIndexFromPosition(value, 1, 0)).toBe(6)
expect(getIndexFromPosition(value, 1, 2)).toBe(8)
expect(getIndexFromPosition(value, 1, 5)).toBe(11)
})
it("should clamp column to line length", () => {
const value = "hi\nworld"
// First line "hi" is only 2 chars, requesting col 5 should clamp to 2
expect(getIndexFromPosition(value, 0, 5)).toBe(2)
})
})
describe("line splitting", () => {
it("should split empty string into single empty line", () => {
const value = ""
const lines = value.split("\n")
expect(lines).toEqual([""])
})
it("should split single line correctly", () => {
const value = "hello world"
const lines = value.split("\n")
expect(lines).toEqual(["hello world"])
})
it("should split multiple lines correctly", () => {
const value = "foo\nbar\nbaz"
const lines = value.split("\n")
expect(lines).toEqual(["foo", "bar", "baz"])
})
it("should handle trailing newline", () => {
const value = "foo\nbar\n"
const lines = value.split("\n")
expect(lines).toEqual(["foo", "bar", ""])
})
it("should handle empty lines in middle", () => {
const value = "foo\n\nbaz"
const lines = value.split("\n")
expect(lines).toEqual(["foo", "", "baz"])
})
})
describe("line normalization", () => {
const normalizeLineEndings = (text: string): string => {
if (text == null) return ""
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
}
it("should normalize CRLF to LF", () => {
expect(normalizeLineEndings("hello\r\nworld")).toBe("hello\nworld")
})
it("should normalize CR to LF", () => {
expect(normalizeLineEndings("hello\rworld")).toBe("hello\nworld")
})
it("should leave LF unchanged", () => {
expect(normalizeLineEndings("hello\nworld")).toBe("hello\nworld")
})
it("should handle null/undefined", () => {
expect(normalizeLineEndings(null as unknown as string)).toBe("")
expect(normalizeLineEndings(undefined as unknown as string)).toBe("")
})
it("should handle mixed line endings", () => {
expect(normalizeLineEndings("a\r\nb\rc\nd")).toBe("a\nb\nc\nd")
})
})
describe("key binding behavior", () => {
it("should detect Ctrl+Enter for newline insertion", () => {
const isNewlineKey = (key: { return: boolean; ctrl: boolean }) => key.return && key.ctrl
expect(isNewlineKey({ return: true, ctrl: true })).toBe(true)
expect(isNewlineKey({ return: true, ctrl: false })).toBe(false)
expect(isNewlineKey({ return: false, ctrl: true })).toBe(false)
})
it("should detect Enter for submit", () => {
const isSubmitKey = (key: { return: boolean; ctrl: boolean }) => key.return && !key.ctrl
expect(isSubmitKey({ return: true, ctrl: false })).toBe(true)
expect(isSubmitKey({ return: true, ctrl: true })).toBe(false)
expect(isSubmitKey({ return: false, ctrl: false })).toBe(false)
})
})
describe("newline insertion", () => {
it("should insert newline at cursor position", () => {
const value = "hello"
const cursorIndex = 2
const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex)
expect(newValue).toBe("he\nllo")
})
it("should insert newline at end", () => {
const value = "hello"
const cursorIndex = 5
const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex)
expect(newValue).toBe("hello\n")
})
it("should insert newline at start", () => {
const value = "hello"
const cursorIndex = 0
const newValue = value.slice(0, cursorIndex) + "\n" + value.slice(cursorIndex)
expect(newValue).toBe("\nhello")
})
})
describe("backspace behavior", () => {
it("should delete character before cursor", () => {
const value = "hello"
const cursorIndex = 3
const newValue = value.slice(0, cursorIndex - 1) + value.slice(cursorIndex)
expect(newValue).toBe("helo")
})
it("should delete newline character (merge lines)", () => {
const value = "hello\nworld"
const cursorIndex = 6 // Start of "world" line
const newValue = value.slice(0, cursorIndex - 1) + value.slice(cursorIndex)
expect(newValue).toBe("helloworld")
})
it("should do nothing at start of input", () => {
const value = "hello"
const cursorIndex = 0
// In real implementation, we check if cursorIndex > 0
if (cursorIndex > 0) {
const newValue = value.slice(0, cursorIndex - 1) + value.slice(cursorIndex)
expect(newValue).not.toBe(value)
}
// At position 0, backspace does nothing
expect(value).toBe("hello")
})
})
describe("arrow key navigation", () => {
describe("up arrow", () => {
it("should move to previous line preserving column", () => {
const value = "hello\nworld"
const getCursorPosition = (v: string, i: number) => {
const lines = v.split("\n")
let pos = 0
for (let li = 0; li < lines.length; li++) {
const line = lines[li]!
const lineEnd = pos + line.length
if (i <= lineEnd) {
return { line: li, col: i - pos }
}
pos = lineEnd + 1
}
return { line: lines.length - 1, col: (lines[lines.length - 1] || "").length }
}
const getIndexFromPosition = (v: string, line: number, col: number) => {
const lines = v.split("\n")
let index = 0
for (let i = 0; i < line && i < lines.length; i++) {
index += lines[i]!.length + 1
}
const targetLine = lines[line] || ""
index += Math.min(col, targetLine.length)
return index
}
// Cursor at "world"[2] (index 8)
const cursorIndex = 8
const { line, col } = getCursorPosition(value, cursorIndex)
expect(line).toBe(1)
expect(col).toBe(2)
// Move up: should go to line 0, same column
const targetLine = 0
const newIndex = getIndexFromPosition(value, targetLine, col)
expect(newIndex).toBe(2) // "he|llo"
})
it("should clamp column if target line is shorter", () => {
const value = "hi\nworld"
const getIndexFromPosition = (v: string, line: number, col: number) => {
const lines = v.split("\n")
let index = 0
for (let i = 0; i < line && i < lines.length; i++) {
index += lines[i]!.length + 1
}
const targetLine = lines[line] || ""
index += Math.min(col, targetLine.length)
return index
}
// Cursor at "world"[4] (index 7)
// Moving up to "hi" which is only 2 chars, should clamp to col 2
const targetLine = 0
const col = 4
const newIndex = getIndexFromPosition(value, targetLine, col)
expect(newIndex).toBe(2) // End of "hi"
})
})
describe("down arrow", () => {
it("should move to next line preserving column", () => {
const value = "hello\nworld"
const getIndexFromPosition = (v: string, line: number, col: number) => {
const lines = v.split("\n")
let index = 0
for (let i = 0; i < line && i < lines.length; i++) {
index += lines[i]!.length + 1
}
const targetLine = lines[line] || ""
index += Math.min(col, targetLine.length)
return index
}
// Cursor at "hello"[2] (index 2)
const col = 2
const targetLine = 1
const newIndex = getIndexFromPosition(value, targetLine, col)
expect(newIndex).toBe(8) // "wo|rld"
})
})
describe("left/right arrows", () => {
it("should move left by 1", () => {
const cursorIndex = 5
const newIndex = Math.max(0, cursorIndex - 1)
expect(newIndex).toBe(4)
})
it("should not move left past 0", () => {
const cursorIndex = 0
const newIndex = Math.max(0, cursorIndex - 1)
expect(newIndex).toBe(0)
})
it("should move right by 1", () => {
const value = "hello"
const cursorIndex = 2
const newIndex = Math.min(value.length, cursorIndex + 1)
expect(newIndex).toBe(3)
})
it("should not move right past end", () => {
const value = "hello"
const cursorIndex = 5
const newIndex = Math.min(value.length, cursorIndex + 1)
expect(newIndex).toBe(5)
})
})
})
})
describe("word-boundary line wrapping", () => {
// Represents a visual row after wrapping a logical line
interface VisualRow {
text: string
logicalLineIndex: number
isFirstRowOfLine: boolean
startCol: number
}
/**
* Wrap a logical line into visual rows based on available width.
* Uses word-boundary wrapping: prefers to break at spaces rather than
* in the middle of words.
*/
function wrapLine(lineText: string, logicalLineIndex: number, availableWidth: number): VisualRow[] {
if (availableWidth <= 0 || lineText.length <= availableWidth) {
return [
{
text: lineText,
logicalLineIndex,
isFirstRowOfLine: true,
startCol: 0,
},
]
}
const rows: VisualRow[] = []
let remaining = lineText
let startCol = 0
let isFirst = true
while (remaining.length > 0) {
if (remaining.length <= availableWidth) {
rows.push({
text: remaining,
logicalLineIndex,
isFirstRowOfLine: isFirst,
startCol,
})
break
}
// Find a good break point - prefer breaking at a space
let breakPoint = availableWidth
// Look backwards from availableWidth for a space
const searchStart = Math.min(availableWidth, remaining.length)
let spaceIndex = -1
for (let i = searchStart - 1; i >= 0; i--) {
if (remaining[i] === " ") {
spaceIndex = i
break
}
}
if (spaceIndex > 0) {
// Found a space - break after it (include the space in this row)
breakPoint = spaceIndex + 1
}
// else: no space found, break at availableWidth (mid-word break as fallback)
const chunk = remaining.slice(0, breakPoint)
rows.push({
text: chunk,
logicalLineIndex,
isFirstRowOfLine: isFirst,
startCol,
})
remaining = remaining.slice(breakPoint)
startCol += breakPoint
isFirst = false
}
return rows
}
it("should not wrap text shorter than available width", () => {
const rows = wrapLine("hello world", 0, 20)
expect(rows).toHaveLength(1)
expect(rows[0]!.text).toBe("hello world")
expect(rows[0]!.isFirstRowOfLine).toBe(true)
expect(rows[0]!.startCol).toBe(0)
})
it("should wrap at word boundary when possible", () => {
const rows = wrapLine("hello world foo", 0, 10)
expect(rows).toHaveLength(2)
expect(rows[0]!.text).toBe("hello ")
expect(rows[0]!.isFirstRowOfLine).toBe(true)
expect(rows[0]!.startCol).toBe(0)
expect(rows[1]!.text).toBe("world foo")
expect(rows[1]!.isFirstRowOfLine).toBe(false)
expect(rows[1]!.startCol).toBe(6) // "hello " is 6 chars
})
it("should break mid-word when no space found", () => {
const rows = wrapLine("superlongwordwithoutspaces", 0, 10)
expect(rows).toHaveLength(3)
// Falls back to breaking at availableWidth when no space is found
expect(rows[0]!.text).toBe("superlongw")
expect(rows[1]!.text).toBe("ordwithout")
expect(rows[2]!.text).toBe("spaces")
})
it("should handle multiple word wraps", () => {
const rows = wrapLine("one two three four five six", 0, 8)
expect(rows).toHaveLength(4)
expect(rows[0]!.text).toBe("one two ")
expect(rows[1]!.text).toBe("three ")
expect(rows[2]!.text).toBe("four ")
expect(rows[3]!.text).toBe("five six")
})
it("should preserve logical line index", () => {
const rows = wrapLine("hello world", 2, 6)
expect(rows.every((r) => r.logicalLineIndex === 2)).toBe(true)
})
it("should handle empty string", () => {
const rows = wrapLine("", 0, 10)
expect(rows).toHaveLength(1)
expect(rows[0]!.text).toBe("")
})
it("should handle string that exactly matches width", () => {
const rows = wrapLine("hello", 0, 5)
expect(rows).toHaveLength(1)
expect(rows[0]!.text).toBe("hello")
})
it("should track correct startCol for wrapped rows", () => {
const rows = wrapLine("aa bb cc dd", 0, 5)
// "aa bb cc dd" = 11 chars, width = 5
// "aa bb cc dd": a(0) a(1) ' '(2) b(3) b(4) ' '(5) c(6) c(7) ' '(8) d(9) d(10)
// Search backwards from index 4:
// index 4='b', 3='b', 2=' ' -> space at 2, breakPoint=3
// Row 0: "aa " (3 chars), startCol=0
// Remaining: "bb cc dd" (8 chars), startCol=3
// Search backwards from index 4:
// "bb cc dd": b(0) b(1) ' '(2) c(3) c(4)...
// index 4='c', 3='c', 2=' ' -> space at 2, breakPoint=3
// Row 1: "bb " (3 chars), startCol=3
// Remaining: "cc dd" (5 chars), startCol=6
// 5 <= 5, fits in one row
// Row 2: "cc dd", startCol=6
expect(rows).toHaveLength(3)
expect(rows[0]!.text).toBe("aa ")
expect(rows[0]!.startCol).toBe(0)
expect(rows[1]!.text).toBe("bb ")
expect(rows[1]!.startCol).toBe(3)
expect(rows[2]!.text).toBe("cc dd")
expect(rows[2]!.startCol).toBe(6)
})
})
describe("multi-line history integration", () => {
it("should store multi-line entries with newlines", () => {
const entry = "foo\nbar\nbaz"
expect(entry.includes("\n")).toBe(true)
expect(entry.split("\n").length).toBe(3)
})
it("should restore multi-line entries correctly", () => {
const storedEntry = "foo\nbar\nbaz"
const lines = storedEntry.split("\n")
expect(lines).toEqual(["foo", "bar", "baz"])
})
})
describe("cursor overflow prevention", () => {
/**
* Tests the logic that prevents visual shift when cursor is at the end
* of a max-width row. Adding a cursor space character would overflow
* the terminal width, causing text to shift left.
*/
it("should detect when cursor space would overflow", () => {
// Simulates the overflow detection logic from renderVisualRow
const checkWouldOverflow = (
columns: number | undefined,
cursorAtEnd: boolean,
prefixLen: number,
textLen: number,
): boolean => {
return columns !== undefined && cursorAtEnd && prefixLen + textLen + 1 > columns
}
// Terminal width 80, prefix "> " (2 chars), text 78 chars = exactly full
// Adding cursor space would make it 81 chars -> overflow
expect(checkWouldOverflow(80, true, 2, 78)).toBe(true)
// Same scenario but cursor not at end -> no overflow issue
expect(checkWouldOverflow(80, false, 2, 78)).toBe(false)
// Text shorter than max width -> no overflow
expect(checkWouldOverflow(80, true, 2, 50)).toBe(false)
// No columns specified -> no overflow detection
expect(checkWouldOverflow(undefined, true, 2, 78)).toBe(false)
// Continuation line with shorter indent
expect(checkWouldOverflow(80, true, 2, 77)).toBe(false)
// Exactly at boundary (prefixLen + textLen + 1 === columns)
expect(checkWouldOverflow(80, true, 2, 77)).toBe(false)
// One character over would overflow
expect(checkWouldOverflow(80, true, 3, 77)).toBe(true)
})
it("should not overflow when cursor is in the middle of text", () => {
const checkWouldOverflow = (
columns: number | undefined,
cursorAtEnd: boolean,
prefixLen: number,
textLen: number,
): boolean => {
return columns !== undefined && cursorAtEnd && prefixLen + textLen + 1 > columns
}
// Cursor in middle of max-width row - no extra space added, no overflow
expect(checkWouldOverflow(80, false, 2, 78)).toBe(false)
expect(checkWouldOverflow(80, false, 2, 100)).toBe(false)
})
it("should correctly identify cursor at end of row", () => {
// Simulates the cursorAtEnd check
const isCursorAtEnd = (cursorColInRow: number, textLen: number): boolean => {
return cursorColInRow >= textLen
}
expect(isCursorAtEnd(5, 5)).toBe(true) // cursor at position 5, text length 5
expect(isCursorAtEnd(10, 5)).toBe(true) // cursor beyond text (clamped case)
expect(isCursorAtEnd(4, 5)).toBe(false) // cursor before end
expect(isCursorAtEnd(0, 0)).toBe(true) // empty text, cursor at start/end
})
})

View file

@ -1,485 +0,0 @@
/**
* Unit tests for ScrollArea component reducer logic
*/
// Since we can't easily test React components without a proper Ink test setup,
// we'll test the reducer logic that powers the ScrollArea behavior.
interface ScrollAreaState {
innerHeight: number
height: number
scrollTop: number
autoScroll: boolean
}
/**
* Calculate scrollbar handle position and size
*/
function calculateScrollbar(
viewportHeight: number,
contentHeight: number,
scrollTop: number,
): { handleStart: number; handleHeight: number; maxScroll: number } {
const maxScroll = Math.max(0, contentHeight - viewportHeight)
if (contentHeight <= viewportHeight || maxScroll === 0) {
// No scrolling needed - handle fills entire track
return { handleStart: 0, handleHeight: viewportHeight, maxScroll: 0 }
}
// Calculate handle height as ratio of viewport to content (minimum 1 line)
const handleHeight = Math.max(1, Math.round((viewportHeight / contentHeight) * viewportHeight))
// Calculate handle position
const trackSpace = viewportHeight - handleHeight
const scrollRatio = maxScroll > 0 ? scrollTop / maxScroll : 0
const handleStart = Math.round(scrollRatio * trackSpace)
return { handleStart, handleHeight, maxScroll }
}
type ScrollAreaAction =
| { type: "SET_INNER_HEIGHT"; innerHeight: number }
| { type: "SET_HEIGHT"; height: number }
| { type: "SCROLL_DOWN"; amount?: number }
| { type: "SCROLL_UP"; amount?: number }
| { type: "SCROLL_TO_BOTTOM" }
| { type: "SET_AUTO_SCROLL"; autoScroll: boolean }
// Copy of the reducer from ScrollArea.tsx for testing
function reducer(state: ScrollAreaState, action: ScrollAreaAction): ScrollAreaState {
const maxScroll = Math.max(0, state.innerHeight - state.height)
switch (action.type) {
case "SET_INNER_HEIGHT": {
const newMaxScroll = Math.max(0, action.innerHeight - state.height)
if (state.autoScroll && action.innerHeight > state.innerHeight) {
return {
...state,
innerHeight: action.innerHeight,
scrollTop: newMaxScroll,
}
}
return {
...state,
innerHeight: action.innerHeight,
scrollTop: Math.min(state.scrollTop, newMaxScroll),
}
}
case "SET_HEIGHT": {
const newMaxScroll = Math.max(0, state.innerHeight - action.height)
if (state.autoScroll) {
return {
...state,
height: action.height,
scrollTop: newMaxScroll,
}
}
return {
...state,
height: action.height,
scrollTop: Math.min(state.scrollTop, newMaxScroll),
}
}
case "SCROLL_DOWN": {
const amount = action.amount || 1
const newScrollTop = Math.min(maxScroll, state.scrollTop + amount)
const atBottom = newScrollTop >= maxScroll
return {
...state,
scrollTop: newScrollTop,
autoScroll: atBottom,
}
}
case "SCROLL_UP": {
const amount = action.amount || 1
const newScrollTop = Math.max(0, state.scrollTop - amount)
return {
...state,
scrollTop: newScrollTop,
autoScroll: newScrollTop >= maxScroll,
}
}
case "SCROLL_TO_BOTTOM":
return {
...state,
scrollTop: maxScroll,
autoScroll: true,
}
case "SET_AUTO_SCROLL":
return {
...state,
autoScroll: action.autoScroll,
scrollTop: action.autoScroll ? maxScroll : state.scrollTop,
}
default:
return state
}
}
describe("ScrollArea reducer", () => {
const initialState: ScrollAreaState = {
innerHeight: 0,
height: 10,
scrollTop: 0,
autoScroll: true,
}
describe("SET_INNER_HEIGHT", () => {
it("should update inner height", () => {
const state = reducer(initialState, { type: "SET_INNER_HEIGHT", innerHeight: 20 })
expect(state.innerHeight).toBe(20)
})
it("should auto-scroll to bottom when content grows and autoScroll is enabled", () => {
const state: ScrollAreaState = {
...initialState,
innerHeight: 15,
autoScroll: true,
}
const newState = reducer(state, { type: "SET_INNER_HEIGHT", innerHeight: 25 })
expect(newState.innerHeight).toBe(25)
// maxScroll = 25 - 10 = 15
expect(newState.scrollTop).toBe(15)
})
it("should NOT auto-scroll when autoScroll is disabled", () => {
const state: ScrollAreaState = {
...initialState,
innerHeight: 15,
scrollTop: 3,
autoScroll: false,
}
const newState = reducer(state, { type: "SET_INNER_HEIGHT", innerHeight: 25 })
expect(newState.innerHeight).toBe(25)
expect(newState.scrollTop).toBe(3) // Unchanged
})
it("should NOT auto-scroll when content grows if autoScroll is disabled (picker use case)", () => {
const state: ScrollAreaState = {
...initialState,
innerHeight: 5,
scrollTop: 0,
autoScroll: false,
}
const newState = reducer(state, { type: "SET_INNER_HEIGHT", innerHeight: 20 })
expect(newState.innerHeight).toBe(20)
// scrollTop should remain at 0, not jump to bottom
expect(newState.scrollTop).toBe(0)
})
it("should clamp scrollTop when content shrinks", () => {
const state: ScrollAreaState = {
...initialState,
innerHeight: 30,
scrollTop: 15,
autoScroll: false,
}
const newState = reducer(state, { type: "SET_INNER_HEIGHT", innerHeight: 15 })
// maxScroll = 15 - 10 = 5, scrollTop was 15 which is > 5
expect(newState.scrollTop).toBe(5)
})
})
describe("SET_HEIGHT", () => {
it("should update viewport height", () => {
const state: ScrollAreaState = {
...initialState,
innerHeight: 20,
}
const newState = reducer(state, { type: "SET_HEIGHT", height: 15 })
expect(newState.height).toBe(15)
})
it("should scroll to bottom when autoScroll is enabled and viewport changes", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 20, // at bottom
autoScroll: true,
}
const newState = reducer(state, { type: "SET_HEIGHT", height: 15 })
// maxScroll = 30 - 15 = 15
expect(newState.scrollTop).toBe(15)
})
it("should clamp scrollTop when viewport grows", () => {
const state: ScrollAreaState = {
innerHeight: 20,
height: 10,
scrollTop: 10, // maxScroll was 10
autoScroll: false,
}
const newState = reducer(state, { type: "SET_HEIGHT", height: 15 })
// maxScroll = 20 - 15 = 5
expect(newState.scrollTop).toBe(5)
})
})
describe("SCROLL_DOWN", () => {
it("should scroll down by 1 by default", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 5,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_DOWN" })
expect(newState.scrollTop).toBe(6)
})
it("should scroll down by specified amount", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 5,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_DOWN", amount: 5 })
expect(newState.scrollTop).toBe(10)
})
it("should not scroll past maxScroll", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 18,
autoScroll: false,
}
// maxScroll = 30 - 10 = 20
const newState = reducer(state, { type: "SCROLL_DOWN", amount: 10 })
expect(newState.scrollTop).toBe(20)
})
it("should re-enable autoScroll when reaching bottom", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 19,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_DOWN" })
expect(newState.scrollTop).toBe(20)
expect(newState.autoScroll).toBe(true)
})
})
describe("SCROLL_UP", () => {
it("should scroll up by 1 by default", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 10,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_UP" })
expect(newState.scrollTop).toBe(9)
})
it("should scroll up by specified amount", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 10,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_UP", amount: 5 })
expect(newState.scrollTop).toBe(5)
})
it("should not scroll past 0", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 3,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_UP", amount: 10 })
expect(newState.scrollTop).toBe(0)
})
it("should disable autoScroll when scrolling up from bottom", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 20, // at bottom
autoScroll: true,
}
const newState = reducer(state, { type: "SCROLL_UP" })
expect(newState.scrollTop).toBe(19)
expect(newState.autoScroll).toBe(false)
})
})
describe("SCROLL_TO_BOTTOM", () => {
it("should scroll to bottom and enable autoScroll", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 5,
autoScroll: false,
}
const newState = reducer(state, { type: "SCROLL_TO_BOTTOM" })
expect(newState.scrollTop).toBe(20) // maxScroll
expect(newState.autoScroll).toBe(true)
})
})
describe("SET_AUTO_SCROLL", () => {
it("should enable autoScroll and scroll to bottom", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 5,
autoScroll: false,
}
const newState = reducer(state, { type: "SET_AUTO_SCROLL", autoScroll: true })
expect(newState.autoScroll).toBe(true)
expect(newState.scrollTop).toBe(20) // scrolled to bottom
})
it("should disable autoScroll without changing scrollTop", () => {
const state: ScrollAreaState = {
innerHeight: 30,
height: 10,
scrollTop: 20,
autoScroll: true,
}
const newState = reducer(state, { type: "SET_AUTO_SCROLL", autoScroll: false })
expect(newState.autoScroll).toBe(false)
expect(newState.scrollTop).toBe(20)
})
})
describe("edge cases", () => {
it("should handle content smaller than viewport", () => {
const state: ScrollAreaState = {
innerHeight: 5, // smaller than viewport
height: 10,
scrollTop: 0,
autoScroll: true,
}
const downState = reducer(state, { type: "SCROLL_DOWN" })
expect(downState.scrollTop).toBe(0) // maxScroll is 0
const bottomState = reducer(state, { type: "SCROLL_TO_BOTTOM" })
expect(bottomState.scrollTop).toBe(0)
})
it("should handle empty content", () => {
const state: ScrollAreaState = {
innerHeight: 0,
height: 10,
scrollTop: 0,
autoScroll: true,
}
const newState = reducer(state, { type: "SCROLL_DOWN" })
expect(newState.scrollTop).toBe(0)
})
})
})
describe("calculateScrollbar", () => {
it("should return full height handle when content fits in viewport", () => {
const result = calculateScrollbar(10, 5, 0)
expect(result.handleHeight).toBe(10)
expect(result.handleStart).toBe(0)
expect(result.maxScroll).toBe(0)
})
it("should return full height handle when content equals viewport", () => {
const result = calculateScrollbar(10, 10, 0)
expect(result.handleHeight).toBe(10)
expect(result.handleStart).toBe(0)
expect(result.maxScroll).toBe(0)
})
it("should calculate handle height proportional to content ratio", () => {
// Viewport is half of content, handle should be ~half of viewport
const result = calculateScrollbar(10, 20, 0)
expect(result.handleHeight).toBe(5) // 10 / 20 * 10 = 5
expect(result.maxScroll).toBe(10)
})
it("should position handle at top when scrollTop is 0", () => {
const result = calculateScrollbar(10, 20, 0)
expect(result.handleStart).toBe(0)
})
it("should position handle at bottom when scrolled to max", () => {
// Viewport 10, content 20, maxScroll = 10
// Handle height = 5, track space = 10 - 5 = 5
// At max scroll, handle should be at position 5
const result = calculateScrollbar(10, 20, 10)
expect(result.handleStart).toBe(5)
})
it("should position handle in middle when scrolled halfway", () => {
// Viewport 10, content 20, maxScroll = 10
// Handle height = 5, track space = 5
// At scroll 5 (50%), handle should be at position 2-3
const result = calculateScrollbar(10, 20, 5)
expect(result.handleStart).toBe(3) // Math.round(0.5 * 5) = 3
})
it("should enforce minimum handle height of 1", () => {
// Very large content relative to viewport
const result = calculateScrollbar(10, 1000, 0)
expect(result.handleHeight).toBe(1) // Math.max(1, Math.round(10/1000 * 10)) = 1
})
it("should handle small viewports", () => {
const result = calculateScrollbar(3, 10, 0)
expect(result.handleHeight).toBe(1) // Math.round(3/10 * 3) = 1
expect(result.maxScroll).toBe(7)
})
it("should handle edge case where scrollTop exceeds maxScroll", () => {
// This shouldn't happen in practice, but test for robustness
const result = calculateScrollbar(10, 20, 15) // maxScroll is 10
// scrollRatio = 15/10 = 1.5, but handleStart should be clamped by trackSpace
expect(result.handleStart).toBe(8) // Math.round(1.5 * 5) = 8 (will be past track but shows calculation)
})
})
/**
* Helper function that mirrors the scrollbar visibility logic from ScrollArea.tsx
* This is used to test the visibility behavior without needing to render the component.
*/
function shouldShowScrollbar(showScrollbar: boolean, maxScroll: number, isActive: boolean): boolean {
// Show scrollbar when: there's content to scroll, OR when focused (to indicate focus state)
// Hide scrollbar only when: not focused AND nothing to scroll
return showScrollbar && (maxScroll > 0 || isActive)
}
describe("scrollbar visibility", () => {
it("should show scrollbar when there is content to scroll (regardless of focus)", () => {
// When maxScroll > 0, scrollbar should show regardless of isActive
expect(shouldShowScrollbar(true, 10, true)).toBe(true)
expect(shouldShowScrollbar(true, 10, false)).toBe(true)
})
it("should show scrollbar when focused, even if nothing to scroll", () => {
// When isActive is true but maxScroll is 0, scrollbar should show for focus indication
expect(shouldShowScrollbar(true, 0, true)).toBe(true)
})
it("should hide scrollbar when not focused and nothing to scroll", () => {
// Only hide when both: not focused AND nothing to scroll
expect(shouldShowScrollbar(true, 0, false)).toBe(false)
})
it("should respect showScrollbar prop", () => {
// When showScrollbar is false, never show scrollbar
expect(shouldShowScrollbar(false, 10, true)).toBe(false)
expect(shouldShowScrollbar(false, 0, true)).toBe(false)
expect(shouldShowScrollbar(false, 10, false)).toBe(false)
expect(shouldShowScrollbar(false, 0, false)).toBe(false)
})
})

View file

@ -1,163 +0,0 @@
import * as historyStorage from "../utils/historyStorage.js"
vi.mock("../utils/historyStorage.js")
// Track state and callbacks for testing.
let mockState: Record<string, unknown> = {}
let effectCallbacks: Array<() => void | (() => void)> = []
vi.mock("react", () => ({
useState: vi.fn((initial: unknown) => {
const key = `state_${Object.keys(mockState).length}`
if (!(key in mockState)) {
mockState[key] = initial
}
return [
mockState[key],
(newValue: unknown) => {
if (typeof newValue === "function") {
mockState[key] = (newValue as (prev: unknown) => unknown)(mockState[key])
} else {
mockState[key] = newValue
}
},
]
}),
useEffect: vi.fn((callback: () => void | (() => void)) => {
effectCallbacks.push(callback)
}),
useCallback: vi.fn((callback: unknown) => callback),
useRef: vi.fn((initial: unknown) => ({ current: initial })),
}))
describe("useInputHistory", () => {
beforeEach(() => {
vi.resetAllMocks()
mockState = {}
effectCallbacks = []
// Default mock for loadHistory
vi.mocked(historyStorage.loadHistory).mockResolvedValue([])
vi.mocked(historyStorage.addToHistory).mockImplementation(async (entry) => [entry])
})
describe("historyStorage functions", () => {
it("loadHistory should be called when hook effect runs", async () => {
vi.mocked(historyStorage.loadHistory).mockResolvedValue(["entry1", "entry2"])
// Import the hook (this triggers the module initialization)
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
useInputHistory()
// Run the effect callbacks
for (const cb of effectCallbacks) {
cb()
}
expect(historyStorage.loadHistory).toHaveBeenCalled()
})
it("addToHistory should be called with trimmed entry", async () => {
vi.mocked(historyStorage.addToHistory).mockResolvedValue(["new entry"])
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
await result.addEntry(" new entry ")
expect(historyStorage.addToHistory).toHaveBeenCalledWith("new entry")
})
it("addToHistory should not be called for empty entries", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
await result.addEntry("")
expect(historyStorage.addToHistory).not.toHaveBeenCalled()
})
it("addToHistory should not be called for whitespace-only entries", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
await result.addEntry(" ")
expect(historyStorage.addToHistory).not.toHaveBeenCalled()
})
})
describe("navigation logic", () => {
it("should have initial state with no history value", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
// Initial state should have null history value (not browsing)
expect(result.historyValue).toBeNull()
expect(result.isBrowsing).toBe(false)
})
it("should export navigateUp and navigateDown functions for manual navigation", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
expect(typeof result.navigateUp).toBe("function")
expect(typeof result.navigateDown).toBe("function")
})
})
describe("resetBrowsing", () => {
it("should be a function", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
expect(typeof result.resetBrowsing).toBe("function")
})
})
describe("return value structure", () => {
it("should return the expected interface", async () => {
const { useInputHistory } = await import("../ui/hooks/useInputHistory.js")
const result = useInputHistory()
expect(result).toHaveProperty("addEntry")
expect(result).toHaveProperty("historyValue")
expect(result).toHaveProperty("isBrowsing")
expect(result).toHaveProperty("resetBrowsing")
expect(result).toHaveProperty("history")
expect(result).toHaveProperty("draft")
expect(result).toHaveProperty("navigateUp")
expect(result).toHaveProperty("navigateDown")
expect(typeof result.addEntry).toBe("function")
expect(typeof result.resetBrowsing).toBe("function")
expect(typeof result.navigateUp).toBe("function")
expect(typeof result.navigateDown).toBe("function")
expect(Array.isArray(result.history)).toBe(true)
})
})
})
describe("historyStorage integration", () => {
// Test the actual historyStorage functions directly
// These are more reliable than hook tests with mocked React
beforeEach(() => {
vi.resetAllMocks()
})
it("MAX_HISTORY_ENTRIES should be 500", async () => {
const { MAX_HISTORY_ENTRIES } = await import("../utils/historyStorage.js")
expect(MAX_HISTORY_ENTRIES).toBe(500)
})
it("getHistoryFilePath should return path in ~/.roo directory", async () => {
// Un-mock for this test
vi.doUnmock("../utils/historyStorage.js")
const { getHistoryFilePath } = await import("../utils/historyStorage.js")
const path = getHistoryFilePath()
expect(path).toContain(".roo")
expect(path).toContain("cli-history.json")
})
})

View file

@ -18,7 +18,7 @@ import {
import { setLogger } from "@roo-code/vscode-shim"
import { ExtensionHost } from "./extension-host.js"
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "./utils.js"
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "./utils/extensionHostUtils.js"
const DEFAULTS = {
mode: "code",

View file

@ -8,15 +8,15 @@ import type { ExtensionMessage, ClineMessage, ClineAsk, ClineSay, TodoItem, Webv
import { consolidateTokenUsage, consolidateApiRequests, consolidateCommands } from "@roo-code/core/message-utils"
import { FOLLOWUP_TIMEOUT_SECONDS } from "../constants.js"
import { getGlobalCommand, getGlobalCommandsForAutocomplete } from "../globalCommands.js"
import { getGlobalCommand, getGlobalCommandsForAutocomplete } from "../utils/globalCommands.js"
import { toolInspectorLog, clearToolInspectorLog } from "../utils/toolInspectorLogger.js"
import { arePathsEqual } from "../utils/pathUtils.js"
import { getContextWindow } from "../utils/getContextWindow.js"
import type { AppProps, TUIMessage, PendingAsk, View, ToolData } from "./types.js"
import * as theme from "./utils/theme.js"
import { matchesGlobalSequence } from "./utils/globalInputSequences.js"
import * as theme from "./theme.js"
import { matchesGlobalSequence } from "../utils/globalInputSequences.js"
import { useCLIStore } from "./store.js"

View file

@ -1,4 +1,4 @@
import { useCLIStore } from "../ui/store.js"
import { useCLIStore } from "../store.js"
describe("useCLIStore", () => {
beforeEach(() => {

View file

@ -1,7 +1,7 @@
import { memo } from "react"
import { Box, Newline, Text } from "ink"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
import type { TUIMessage } from "../types.js"
import TodoDisplay from "./TodoDisplay.js"
import { getToolRenderer } from "./tools/index.js"

View file

@ -4,7 +4,7 @@ import { Text, Box } from "ink"
import type { TokenUsage } from "@roo-code/types"
import { useTerminalSize } from "../hooks/TerminalSizeContext.js"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
import MetricsDisplay from "./MetricsDisplay.js"
interface HeaderProps {

View file

@ -3,7 +3,7 @@ import { Text, Box } from "ink"
import type { TokenUsage } from "@roo-code/types"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
import ProgressBar from "./ProgressBar.js"
interface MetricsDisplayProps {

View file

@ -16,7 +16,7 @@
import { useState, useEffect, useMemo, useCallback, useRef } from "react"
import { Box, Text, useInput, type Key } from "ink"
import { isGlobalInputSequence } from "../utils/globalInputSequences.js"
import { isGlobalInputSequence } from "../../utils/globalInputSequences.js"
export interface MultilineTextInputProps {
/**

View file

@ -1,7 +1,7 @@
import { memo } from "react"
import { Text } from "ink"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
interface ProgressBarProps {
/** Current value (e.g., contextTokens) */

View file

@ -1,7 +1,7 @@
import { Box, DOMElement, measureElement, Text, useInput } from "ink"
import { useEffect, useReducer, useRef, useCallback, useMemo, useState } from "react"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
interface ScrollAreaState {
innerHeight: number

View file

@ -1,7 +1,7 @@
import { Box, Text } from "ink"
import { memo } from "react"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
interface ScrollIndicatorProps {
scrollTop: number

View file

@ -2,7 +2,7 @@ import { memo } from "react"
import { Text, Box } from "ink"
import type { Toast, ToastType } from "../hooks/useToast.js"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
interface ToastDisplayProps {
/** The current toast to display (null if no toast) */

View file

@ -3,7 +3,7 @@ import { Box, Text } from "ink"
import type { TodoItem } from "@roo-code/types"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
/**
* Status icons for TODO items using Unicode characters

View file

@ -3,7 +3,7 @@ import { Box, Text } from "ink"
import type { TodoItem } from "@roo-code/types"
import * as theme from "../utils/theme.js"
import * as theme from "../theme.js"
import ProgressBar from "./ProgressBar.js"
import { Icon, type IconName } from "./Icon.js"

View file

@ -2,7 +2,7 @@ import { Box, Text } from "ink"
import fuzzysort from "fuzzysort"
import type { AutocompleteTrigger, AutocompleteItem, TriggerDetectionResult } from "../types.js"
import { GlobalCommandAction } from "../../../../globalCommands.js"
import { GlobalCommandAction } from "../../../../utils/globalCommands.js"
export interface SlashCommandResult extends AutocompleteItem {
name: string

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { getToolDisplayName, getToolIconName } from "./utils.js"

View file

@ -1,6 +1,6 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolIconName } from "./utils.js"

View file

@ -1,6 +1,6 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent } from "./utils.js"

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolDisplayName, getToolIconName } from "./utils.js"

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolDisplayName, getToolIconName, parseDiff } from "./utils.js"

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolDisplayName, getToolIconName } from "./utils.js"

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolDisplayName, getToolIconName } from "./utils.js"

View file

@ -5,7 +5,7 @@
import { Box, Text } from "ink"
import * as theme from "../../utils/theme.js"
import * as theme from "../../theme.js"
import { Icon } from "../Icon.js"
import type { ToolRendererProps } from "./types.js"
import { truncateText, sanitizeContent, getToolDisplayName, getToolIconName } from "./utils.js"

View file

@ -1,4 +1,4 @@
import { useToastStore } from "../ui/hooks/useToast.js"
import { useToastStore } from "../useToast.js"
describe("useToastStore", () => {
beforeEach(() => {

View file

@ -17,7 +17,7 @@ export type { UseInputHistoryOptions, UseInputHistoryReturn } from "./hooks/useI
export { useCLIStore } from "./store.js"
// Theme
export * as theme from "./utils/theme.js"
export * as theme from "./theme.js"
// Types
export * from "./types.js"

View file

@ -1,12 +1,8 @@
/**
* Unit tests for CLI utility functions
*/
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "../utils.js"
import fs from "fs"
import path from "path"
// Mock fs module
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "../extensionHostUtils.js"
vi.mock("fs")
describe("getEnvVarName", () => {

View file

@ -1,14 +1,7 @@
import type { Key } from "ink"
import {
GLOBAL_INPUT_SEQUENCES,
isGlobalInputSequence,
matchesGlobalSequence,
} from "../ui/utils/globalInputSequences.js"
import { GLOBAL_INPUT_SEQUENCES, isGlobalInputSequence, matchesGlobalSequence } from "../globalInputSequences.js"
/**
* Helper to create a minimal Key object for testing
*/
function createKey(overrides: Partial<Key> = {}): Key {
return {
upArrow: false,

View file

@ -1,13 +1,7 @@
import * as fs from "fs/promises"
import * as path from "path"
import {
getHistoryFilePath,
loadHistory,
saveHistory,
addToHistory,
MAX_HISTORY_ENTRIES,
} from "../utils/historyStorage.js"
import { getHistoryFilePath, loadHistory, saveHistory, addToHistory, MAX_HISTORY_ENTRIES } from "../historyStorage.js"
vi.mock("fs/promises")