From 6a64388f090f44c2b58c3e418da596413f59ef32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 16:11:07 -0300 Subject: [PATCH 1/9] fix: tasks history support --- .changeset/spicy-kings-appear.md | 5 + cli/TASKS_IMPLEMENTATION_PLAN.md | 354 ++++++++++++++ cli/src/commands/clear.ts | 4 +- cli/src/commands/core/types.ts | 14 + cli/src/commands/index.ts | 2 + cli/src/commands/new.ts | 5 +- cli/src/commands/tasks.ts | 586 +++++++++++++++++++++++ cli/src/services/autocomplete.ts | 1 + cli/src/state/atoms/effects.ts | 17 + cli/src/state/atoms/taskHistory.ts | 99 ++++ cli/src/state/atoms/ui.ts | 7 +- cli/src/state/hooks/useCommandContext.ts | 53 ++ cli/src/state/hooks/useCommandInput.ts | 4 + cli/src/state/hooks/useTaskHistory.ts | 109 +++++ cli/src/state/hooks/useTerminal.ts | 14 +- cli/src/ui/UI.tsx | 14 + cli/src/ui/messages/MessageDisplay.tsx | 1 + 17 files changed, 1279 insertions(+), 10 deletions(-) create mode 100644 .changeset/spicy-kings-appear.md create mode 100644 cli/TASKS_IMPLEMENTATION_PLAN.md create mode 100644 cli/src/commands/tasks.ts create mode 100644 cli/src/state/atoms/taskHistory.ts create mode 100644 cli/src/state/hooks/useTaskHistory.ts diff --git a/.changeset/spicy-kings-appear.md b/.changeset/spicy-kings-appear.md new file mode 100644 index 00000000000..4ec5fe7841b --- /dev/null +++ b/.changeset/spicy-kings-appear.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Tasks history support diff --git a/cli/TASKS_IMPLEMENTATION_PLAN.md b/cli/TASKS_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000000..998b3fcef8f --- /dev/null +++ b/cli/TASKS_IMPLEMENTATION_PLAN.md @@ -0,0 +1,354 @@ +# Implementation Plan: /tasks Command for CLI + +## Overview + +Implement a `/tasks` command in the CLI that provides similar functionality to the webview's HistoryView component, allowing users to: + +- View task history +- Search and filter tasks +- Switch between tasks +- Navigate with pagination + +## Architecture + +### 1. Message Flow + +``` +CLI (/tasks command) + ↓ +sendWebviewMessage({ type: "taskHistoryRequest", payload }) + ↓ +Extension (webviewMessageHandler.ts) + ↓ +getTaskHistory() processing + ↓ +postMessageToWebview({ type: "taskHistoryResponse", payload }) + ↓ +CLI (message handler in effects.ts) + ↓ +Update task history atoms + ↓ +Display in TUI +``` + +### 2. Required Components + +#### A. New Atoms (`cli/src/state/atoms/taskHistory.ts`) + +```typescript +// Task history state atoms +export const taskHistoryAtom = atom([]) +export const taskHistoryPageAtom = atom(0) +export const taskHistoryPageCountAtom = atom(1) +export const taskHistoryTotalAtom = atom(0) +export const taskHistorySearchQueryAtom = atom("") +export const taskHistorySortOptionAtom = atom("newest") +export const taskHistoryLoadingAtom = atom(false) +export const taskHistoryErrorAtom = atom(null) + +// Action atoms +export const requestTaskHistoryAtom = atom(null, async (get, set, params) => { + // Send taskHistoryRequest message +}) + +export const selectTaskAtom = atom(null, async (get, set, taskId: string) => { + // Send selectTask message to switch to a task +}) +``` + +#### B. New Hook (`cli/src/state/hooks/useTaskHistory.ts`) + +```typescript +export function useTaskHistory() { + const taskHistory = useAtomValue(taskHistoryAtom) + const currentPage = useAtomValue(taskHistoryPageAtom) + const pageCount = useAtomValue(taskHistoryPageCountAtom) + const searchQuery = useAtomValue(taskHistorySearchQueryAtom) + const sortOption = useAtomValue(taskHistorySortOptionAtom) + const isLoading = useAtomValue(taskHistoryLoadingAtom) + const error = useAtomValue(taskHistoryErrorAtom) + + const requestHistory = useSetAtom(requestTaskHistoryAtom) + const selectTask = useSetAtom(selectTaskAtom) + const setSearchQuery = useSetAtom(taskHistorySearchQueryAtom) + const setSortOption = useSetAtom(taskHistorySortOptionAtom) + const setPage = useSetAtom(taskHistoryPageAtom) + + return { + tasks: taskHistory, + currentPage, + pageCount, + searchQuery, + sortOption, + isLoading, + error, + requestHistory, + selectTask, + setSearchQuery, + setSortOption, + setPage, + } +} +``` + +#### C. Tasks Command (`cli/src/commands/tasks.ts`) + +```typescript +export const tasksCommand: Command = { + name: "tasks", + description: "View and manage task history", + arguments: [ + { + name: "action", + description: "Action to perform (list, search, select)", + required: false, + }, + { + name: "value", + description: "Value for the action (search query or task ID)", + required: false, + }, + ], + options: [ + { + name: "sort", + description: "Sort option (newest, oldest, mostExpensive, mostTokens)", + shorthand: "s", + }, + { + name: "page", + description: "Page number", + shorthand: "p", + }, + { + name: "workspace", + description: "Filter by workspace (current, all)", + shorthand: "w", + }, + ], + handler: async (context: CommandContext) => { + // Implementation + }, +} +``` + +#### D. Task History UI Component (`cli/src/ui/components/TaskHistoryView.tsx`) + +```typescript +export const TaskHistoryView: React.FC = () => { + const { + tasks, + currentPage, + pageCount, + searchQuery, + sortOption, + isLoading, + error, + requestHistory, + selectTask, + setSearchQuery, + setSortOption, + setPage, + } = useTaskHistory() + + // Render task list with: + // - Search input + // - Sort dropdown + // - Task items with ID, name, timestamp, mode + // - Pagination controls + // - Select action to switch tasks +} +``` + +### 3. Message Handlers + +#### A. Update `effects.ts` to handle task history responses: + +```typescript +case "taskHistoryResponse": + if (message.payload) { + set(updateTaskHistoryAtom, message.payload) + } + break +``` + +#### B. Add new message types to `cli/src/types/messages.ts`: + +```typescript +export interface TaskHistoryRequestPayload { + pageIndex: number + searchQuery?: string + sortOption?: SortOption + showAllWorkspaces?: boolean + showFavoritesOnly?: boolean +} + +export interface TaskHistoryResponsePayload { + historyItems: HistoryItem[] + pageIndex: number + pageCount: number + totalItems: number +} + +export type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant" +``` + +### 4. Implementation Steps + +1. **Create task history atoms** (`taskHistory.ts`) + + - State atoms for history data, pagination, search, sort + - Action atoms for requesting history and selecting tasks + +2. **Create useTaskHistory hook** (`useTaskHistory.ts`) + + - Encapsulate task history state and actions + - Provide clean interface for UI components + +3. **Implement /tasks command** (`tasks.ts`) + + - Parse command arguments and options + - Handle different actions: list, search, select + - Trigger appropriate state updates + +4. **Add message handlers** (`effects.ts`) + + - Handle taskHistoryResponse messages + - Update task history atoms with response data + +5. **Create TaskHistoryView component** (`TaskHistoryView.tsx`) + + - Display task list in terminal-friendly format + - Show search input and sort options + - Implement pagination controls + - Handle task selection for switching + +6. **Register command** (`commands/index.ts`) + + - Import and register the tasks command + +7. **Add task switching logic** + - Send selectTask message to extension + - Handle task restoration/resumption + +### 5. User Experience + +#### Command Usage Examples: + +```bash +# List all tasks (default) +/tasks + +# Search tasks +/tasks search "implement feature" + +# Select and switch to a task +/tasks select task-id-123 + +# List with options +/tasks --sort=oldest --page=2 --workspace=all + +# Short form +/tasks -s oldest -p 2 -w all +``` + +#### Display Format: + +``` +Task History (Page 1/5) +Search: [________________] Sort: [Newest ▼] + +1. [2024-01-15 10:30] Implement user authentication (code) + ID: task-123 | Workspace: /project/app + +2. [2024-01-15 09:15] Fix navigation bug (debug) + ID: task-124 | Workspace: /project/app + +3. [2024-01-14 16:45] Create API documentation (architect) + ID: task-125 | Workspace: /project/docs + +[Previous] Page 1 of 5 [Next] + +Actions: (s)elect, (f)ilter, (r)efresh, (q)uit +``` + +### 6. Testing Requirements + +1. **Unit Tests** + + - Test command parsing and validation + - Test atom state updates + - Test message handling + +2. **Integration Tests** + + - Test full flow from command to display + - Test pagination navigation + - Test search and filtering + - Test task switching + +3. **Edge Cases** + - Empty task history + - Network/service errors + - Invalid task IDs + - Pagination boundaries + +### 7. Future Enhancements + +1. **Advanced Filtering** + + - Filter by date range + - Filter by mode + - Filter by status (completed/active) + +2. **Bulk Operations** + + - Delete multiple tasks + - Export task history + +3. **Task Details View** + + - Show full task details + - Display task messages + - Show todo items + +4. **Keyboard Shortcuts** + - Quick navigation with arrow keys + - Vim-style navigation (j/k) + - Quick actions (Enter to select, / to search) + +## Implementation Priority + +1. **Phase 1: Core Functionality** + + - Basic /tasks command + - List tasks with pagination + - Simple task selection + +2. **Phase 2: Search & Filter** + + - Search by task name + - Sort options + - Workspace filtering + +3. **Phase 3: Enhanced UX** + - Interactive UI with keyboard navigation + - Task details view + - Bulk operations + +## Dependencies + +- Extension must support `taskHistoryRequest` message type +- Extension must support `selectTask` message type +- Existing message bridge infrastructure +- React and Ink for TUI components + +## Success Criteria + +- [ ] Users can list their task history +- [ ] Users can search tasks by name +- [ ] Users can sort tasks by different criteria +- [ ] Users can navigate pages of results +- [ ] Users can select and switch to a previous task +- [ ] Error states are handled gracefully +- [ ] Performance is acceptable for large task histories diff --git a/cli/src/commands/clear.ts b/cli/src/commands/clear.ts index 6872edd5589..71ff0b8a748 100644 --- a/cli/src/commands/clear.ts +++ b/cli/src/commands/clear.ts @@ -13,7 +13,7 @@ export const clearCommand: Command = { category: "system", priority: 8, handler: async (context) => { - const { setMessageCutoffTimestamp, addMessage } = context + const { setMessageCutoffTimestamp, addMessage, refreshTerminal } = context const now = Date.now() setMessageCutoffTimestamp(now) // Add Spacer message @@ -23,5 +23,7 @@ export const clearCommand: Command = { content: "", ts: now + 1, }) + // Refresh terminal to clear screen + await refreshTerminal() }, } diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index 197d0c843c3..ed5add9245b 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -5,6 +5,7 @@ import type { RouterModels } from "../../types/messages.js" import type { ProviderConfig } from "../../config/types.js" import type { ProfileData, BalanceData } from "../../state/atoms/profile.js" +import type { TaskHistoryData, TaskHistoryFilters } from "../../state/atoms/taskHistory.js" export interface Command { name: string @@ -55,6 +56,18 @@ export interface CommandContext { balanceData: BalanceData | null profileLoading: boolean balanceLoading: boolean + // Task history context + taskHistoryData: TaskHistoryData | null + taskHistoryFilters: TaskHistoryFilters + taskHistoryLoading: boolean + taskHistoryError: string | null + fetchTaskHistory: () => Promise + updateTaskHistoryFilters: (filters: Partial) => Promise + changeTaskHistoryPage: (pageIndex: number) => Promise + nextTaskHistoryPage: () => Promise + previousTaskHistoryPage: () => Promise + sendWebviewMessage: (message: any) => Promise + refreshTerminal: () => Promise } export type CommandHandler = (context: CommandContext) => Promise | void @@ -115,6 +128,7 @@ export interface ArgumentProviderContext { profileLoading: boolean updateProviderModel: (modelId: string) => Promise refreshRouterModels: () => Promise + taskHistoryData: TaskHistoryData | null } } diff --git a/cli/src/commands/index.ts b/cli/src/commands/index.ts index 615c970b68c..d8b4ba15e84 100644 --- a/cli/src/commands/index.ts +++ b/cli/src/commands/index.ts @@ -16,6 +16,7 @@ import { modelCommand } from "./model.js" import { profileCommand } from "./profile.js" import { teamsCommand } from "./teams.js" import { configCommand } from "./config.js" +import { tasksCommand } from "./tasks.js" /** * Initialize all commands @@ -31,4 +32,5 @@ export function initializeCommands(): void { commandRegistry.register(profileCommand) commandRegistry.register(teamsCommand) commandRegistry.register(configCommand) + commandRegistry.register(tasksCommand) } diff --git a/cli/src/commands/new.ts b/cli/src/commands/new.ts index f8746c0dd64..7d305ffab12 100644 --- a/cli/src/commands/new.ts +++ b/cli/src/commands/new.ts @@ -14,7 +14,7 @@ export const newCommand: Command = { category: "system", priority: 9, handler: async (context) => { - const { clearTask, replaceMessages } = context + const { clearTask, replaceMessages, refreshTerminal } = context // Clear the extension task state (this also clears extension messages) await clearTask() @@ -32,5 +32,8 @@ export const newCommand: Command = { ], }), ]) + + // Force terminal refresh to clear screen + await refreshTerminal() }, } diff --git a/cli/src/commands/tasks.ts b/cli/src/commands/tasks.ts new file mode 100644 index 00000000000..489541af550 --- /dev/null +++ b/cli/src/commands/tasks.ts @@ -0,0 +1,586 @@ +/** + * /tasks command - View and manage task history + */ + +import type { Command, ArgumentProviderContext } from "./core/types.js" +import type { HistoryItem } from "@roo-code/types" + +/** + * Map kebab-case sort options to camelCase + */ +const SORT_OPTION_MAP: Record = { + newest: "newest", + oldest: "oldest", + "most-expensive": "mostExpensive", + "most-tokens": "mostTokens", + "most-relevant": "mostRelevant", +} + +/** + * Format a timestamp as a relative time string + */ +function formatRelativeTime(ts: number): string { + const now = Date.now() + const diff = now - ts + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (days > 0) return `${days}d ago` + if (hours > 0) return `${hours}h ago` + if (minutes > 0) return `${minutes}m ago` + return "just now" +} + +/** + * Format cost as a currency string + */ +function formatCost(cost: number): string { + if (cost === 0) return "$0.00" + if (cost < 0.01) return "<$0.01" + return `$${cost.toFixed(2)}` +} + +/** + * Format tokens as a readable string + */ +function formatTokens(tokens: number): string { + if (tokens >= 1000000) { + return `${(tokens / 1000000).toFixed(1)}M` + } + if (tokens >= 1000) { + return `${(tokens / 1000).toFixed(1)}K` + } + return tokens.toString() +} + +/** + * Truncate text to a maximum length + */ +function truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) return text + return text.substring(0, maxLength - 3) + "..." +} + +/** + * Show current task history + */ +async function showTaskHistory(context: any): Promise { + const { taskHistoryData, taskHistoryLoading, taskHistoryError, fetchTaskHistory, addMessage } = context + + // If loading, show loading message + if (taskHistoryLoading) { + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Loading task history...", + ts: Date.now(), + }) + return + } + + // If error, show error message + if (taskHistoryError) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to load task history: ${taskHistoryError}`, + ts: Date.now(), + }) + return + } + + // If no data, fetch it + if (!taskHistoryData) { + await fetchTaskHistory() + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Loading task history...", + ts: Date.now(), + }) + return + } + + const { historyItems, pageIndex, pageCount } = taskHistoryData + + if (historyItems.length === 0) { + addMessage({ + id: Date.now().toString(), + type: "system", + content: "No tasks found in history.", + ts: Date.now(), + }) + return + } + + // Build the task list display + let content = `**Task History** (Page ${pageIndex + 1}/${pageCount}):\n\n` + + historyItems.forEach((task: HistoryItem, index: number) => { + const taskNum = pageIndex * 10 + index + 1 + const taskText = truncate(task.task || "Untitled task", 60) + const time = formatRelativeTime(task.ts || 0) + const cost = formatCost(task.totalCost || 0) + const totalTokens = (task.tokensIn || 0) + (task.tokensOut || 0) + const tokens = formatTokens(totalTokens) + const favorite = task.isFavorited ? "⭐ " : "" + + content += `${favorite}**${taskNum}.** ${taskText}\n` + content += ` ID: ${task.id} | ${time} | ${cost} | ${tokens} tokens\n\n` + }) + + addMessage({ + id: Date.now().toString(), + type: "system", + content, + ts: Date.now(), + }) +} + +/** + * Search tasks + */ +async function searchTasks(context: any, query: string): Promise { + const { updateTaskHistoryFilters, addMessage } = context + + if (!query) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "Usage: /tasks search ", + ts: Date.now(), + }) + return + } + + await updateTaskHistoryFilters({ search: query, sort: "mostRelevant" }) + + addMessage({ + id: Date.now().toString(), + type: "system", + content: `Searching for "${query}"...`, + ts: Date.now(), + }) + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Select a task by ID + */ +async function selectTask(context: any, taskId: string): Promise { + const { sendWebviewMessage, addMessage, replaceMessages, refreshTerminal } = context + + if (!taskId) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "Usage: /tasks select ", + ts: Date.now(), + }) + return + } + + try { + const now = Date.now() + replaceMessages([ + { + id: `empty-${now}`, + type: "empty", + content: "", + ts: 1, + }, + { + id: `system-${now + 1}`, + type: "system", + content: `Switching to task ${taskId}...`, + ts: 2, + }, + ]) + + await refreshTerminal() + + sendWebviewMessage({ + type: "showTaskWithId", + text: taskId, + }) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to switch to task: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } +} + +/** + * Change page + */ +async function changePage(context: any, pageNum: string): Promise { + const { taskHistoryData, changeTaskHistoryPage, addMessage } = context + + if (!taskHistoryData) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "No task history loaded. Use /tasks to load history first.", + ts: Date.now(), + }) + return + } + + const pageIndex = parseInt(pageNum, 10) - 1 // Convert to 0-based index + + if (isNaN(pageIndex) || pageIndex < 0 || pageIndex >= taskHistoryData.pageCount) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Invalid page number. Must be between 1 and ${taskHistoryData.pageCount}.`, + ts: Date.now(), + }) + return + } + + await changeTaskHistoryPage(pageIndex) + + addMessage({ + id: Date.now().toString(), + type: "system", + content: `Loading page ${pageIndex + 1}...`, + ts: Date.now(), + }) + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Go to next page + */ +async function nextPage(context: any): Promise { + const { taskHistoryData, nextTaskHistoryPage, addMessage } = context + + if (!taskHistoryData) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "No task history loaded. Use /tasks to load history first.", + ts: Date.now(), + }) + return + } + + if (taskHistoryData.pageIndex >= taskHistoryData.pageCount - 1) { + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Already on the last page.", + ts: Date.now(), + }) + return + } + + await nextTaskHistoryPage() + + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Loading next page...", + ts: Date.now(), + }) + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Go to previous page + */ +async function previousPage(context: any): Promise { + const { taskHistoryData, previousTaskHistoryPage, addMessage } = context + + if (!taskHistoryData) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: "No task history loaded. Use /tasks to load history first.", + ts: Date.now(), + }) + return + } + + if (taskHistoryData.pageIndex <= 0) { + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Already on the first page.", + ts: Date.now(), + }) + return + } + + await previousTaskHistoryPage() + + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Loading previous page...", + ts: Date.now(), + }) + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Change sort order + */ +async function changeSortOrder(context: any, sortOption: string): Promise { + const { updateTaskHistoryFilters, addMessage } = context + + const validSorts = Object.keys(SORT_OPTION_MAP) + const mappedSort = SORT_OPTION_MAP[sortOption] + + if (!mappedSort) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Invalid sort option. Valid options: ${validSorts.join(", ")}`, + ts: Date.now(), + }) + return + } + + await updateTaskHistoryFilters({ sort: mappedSort as any }) + + addMessage({ + id: Date.now().toString(), + type: "system", + content: `Sorting by ${sortOption}...`, + ts: Date.now(), + }) + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Change filter + */ +async function changeFilter(context: any, filterOption: string): Promise { + const { updateTaskHistoryFilters, addMessage } = context + + switch (filterOption) { + case "current": + await updateTaskHistoryFilters({ workspace: "current" }) + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Filtering to current workspace...", + ts: Date.now(), + }) + break + + case "all": + await updateTaskHistoryFilters({ workspace: "all" }) + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Showing all workspaces...", + ts: Date.now(), + }) + break + + case "favorites": + await updateTaskHistoryFilters({ favoritesOnly: true }) + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Showing favorites only...", + ts: Date.now(), + }) + break + + case "all-tasks": + await updateTaskHistoryFilters({ favoritesOnly: false }) + addMessage({ + id: Date.now().toString(), + type: "system", + content: "Showing all tasks...", + ts: Date.now(), + }) + break + + default: + addMessage({ + id: Date.now().toString(), + type: "error", + content: "Invalid filter option. Valid options: current, all, favorites, all-tasks", + ts: Date.now(), + }) + return + } + + // Show results after a brief delay + setTimeout(() => showTaskHistory(context), 100) +} + +/** + * Autocomplete provider for task IDs + */ +async function taskIdAutocompleteProvider(context: ArgumentProviderContext) { + if (!context.commandContext) { + return [] + } + + const { taskHistoryData } = context.commandContext + + if (!taskHistoryData || !taskHistoryData.historyItems) { + return [] + } + + return taskHistoryData.historyItems.map((task: HistoryItem) => ({ + value: task.id, + title: truncate(task.task || "Untitled task", 50), + description: `${formatRelativeTime(task.ts || 0)} | ${formatCost(task.totalCost || 0)}`, + matchScore: 1.0, + highlightedValue: task.id, + })) +} + +/** + * Autocomplete provider for sort options + */ +async function sortOptionAutocompleteProvider(_context: ArgumentProviderContext) { + return Object.keys(SORT_OPTION_MAP).map((option) => ({ + value: option, + description: `Sort by ${option}`, + matchScore: 1.0, + highlightedValue: option, + })) +} + +/** + * Autocomplete provider for filter options + */ +async function filterOptionAutocompleteProvider(_context: ArgumentProviderContext) { + return [ + { value: "current", description: "Current workspace only", matchScore: 1.0, highlightedValue: "current" }, + { value: "all", description: "All workspaces", matchScore: 1.0, highlightedValue: "all" }, + { value: "favorites", description: "Favorites only", matchScore: 1.0, highlightedValue: "favorites" }, + { value: "all-tasks", description: "All tasks (no filter)", matchScore: 1.0, highlightedValue: "all-tasks" }, + ] +} + +export const tasksCommand: Command = { + name: "tasks", + aliases: ["t", "history"], + description: "View and manage task history", + usage: "/tasks [subcommand] [args]", + examples: [ + "/tasks", + "/tasks search bug fix", + "/tasks select abc123", + "/tasks page 2", + "/tasks next", + "/tasks prev", + "/tasks sort most-expensive", + "/tasks filter favorites", + ], + category: "navigation", + priority: 9, + arguments: [ + { + name: "subcommand", + description: "Subcommand: search, select, page, next, prev, sort, filter", + required: false, + values: [ + { value: "search", description: "Search tasks by query" }, + { value: "select", description: "Switch to a specific task" }, + { value: "page", description: "Go to a specific page" }, + { value: "next", description: "Go to next page" }, + { value: "prev", description: "Go to previous page" }, + { value: "sort", description: "Change sort order" }, + { value: "filter", description: "Filter tasks" }, + ], + }, + { + name: "argument", + description: "Argument for the subcommand", + required: false, + conditionalProviders: [ + { + condition: (context) => context.getArgument("subcommand") === "select", + provider: taskIdAutocompleteProvider, + }, + { + condition: (context) => context.getArgument("subcommand") === "sort", + provider: sortOptionAutocompleteProvider, + }, + { + condition: (context) => context.getArgument("subcommand") === "filter", + provider: filterOptionAutocompleteProvider, + }, + ], + }, + ], + handler: async (context) => { + const { args } = context + + // No arguments - show current task history + if (args.length === 0) { + await showTaskHistory(context) + return + } + + const subcommand = args[0]?.toLowerCase() + if (!subcommand) { + await showTaskHistory(context) + return + } + + // Handle subcommands + switch (subcommand) { + case "search": + await searchTasks(context, args.slice(1).join(" ")) + break + + case "select": + await selectTask(context, args[1] || "") + break + + case "page": + await changePage(context, args[1] || "") + break + + case "next": + await nextPage(context) + break + + case "prev": + case "previous": + await previousPage(context) + break + + case "sort": + await changeSortOrder(context, args[1] || "") + break + + case "filter": + await changeFilter(context, args[1] || "") + break + + default: + context.addMessage({ + id: Date.now().toString(), + type: "error", + content: `Unknown subcommand "${subcommand}". Available: search, select, page, next, prev, sort, filter`, + ts: Date.now(), + }) + } + }, +} diff --git a/cli/src/services/autocomplete.ts b/cli/src/services/autocomplete.ts index 79365dd2a29..ab221ac5109 100644 --- a/cli/src/services/autocomplete.ts +++ b/cli/src/services/autocomplete.ts @@ -446,6 +446,7 @@ function createProviderContext( profileLoading: commandContext.profileLoading || false, updateProviderModel: commandContext.updateProviderModel, refreshRouterModels: commandContext.refreshRouterModels, + taskHistoryData: commandContext.taskHistoryData || null, } } diff --git a/cli/src/state/atoms/effects.ts b/cli/src/state/atoms/effects.ts index 827bab8eaac..cb750d527d5 100644 --- a/cli/src/state/atoms/effects.ts +++ b/cli/src/state/atoms/effects.ts @@ -16,6 +16,7 @@ import { setProfileErrorAtom, setBalanceErrorAtom, } from "./profile.js" +import { taskHistoryDataAtom, taskHistoryLoadingAtom, taskHistoryErrorAtom } from "./taskHistory.js" import { logs } from "../../services/logs.js" /** @@ -162,6 +163,22 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension } break + case "taskHistoryResponse": + // Handle task history response + set(taskHistoryLoadingAtom, false) + if (message.payload) { + const { historyItems, pageIndex, pageCount } = message.payload as any + set(taskHistoryDataAtom, { + historyItems: historyItems || [], + pageIndex: pageIndex || 0, + pageCount: pageCount || 1, + }) + set(taskHistoryErrorAtom, null) + } else { + set(taskHistoryErrorAtom, "Failed to fetch task history") + } + break + case "action": // Action messages are typically handled by the UI break diff --git a/cli/src/state/atoms/taskHistory.ts b/cli/src/state/atoms/taskHistory.ts new file mode 100644 index 00000000000..35c2ecdbb84 --- /dev/null +++ b/cli/src/state/atoms/taskHistory.ts @@ -0,0 +1,99 @@ +/** + * Task history state management atoms + */ + +import { atom } from "jotai" +import type { HistoryItem } from "@roo-code/types" + +/** + * Task history response data + */ +export interface TaskHistoryData { + historyItems: HistoryItem[] + pageIndex: number + pageCount: number +} + +/** + * Task history filter options + */ +export interface TaskHistoryFilters { + workspace: "current" | "all" + sort: "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant" + favoritesOnly: boolean + search?: string +} + +/** + * Current task history data + */ +export const taskHistoryDataAtom = atom(null) + +/** + * Current filters for task history + */ +export const taskHistoryFiltersAtom = atom({ + workspace: "current", + sort: "newest", + favoritesOnly: false, +}) + +/** + * Current page index (0-based) + */ +export const taskHistoryPageIndexAtom = atom(0) + +/** + * Loading state for task history + */ +export const taskHistoryLoadingAtom = atom(false) + +/** + * Error state for task history + */ +export const taskHistoryErrorAtom = atom(null) + +/** + * Request ID counter for tracking responses + */ +export const taskHistoryRequestIdAtom = atom(0) + +/** + * Action atom to fetch task history + */ +export const fetchTaskHistoryAtom = atom(null, async (get, set) => { + const filters = get(taskHistoryFiltersAtom) + const pageIndex = get(taskHistoryPageIndexAtom) + const requestId = get(taskHistoryRequestIdAtom) + 1 + + set(taskHistoryRequestIdAtom, requestId) + set(taskHistoryLoadingAtom, true) + set(taskHistoryErrorAtom, null) + + // This will be connected to the extension service + return { + requestId: requestId.toString(), + ...filters, + pageIndex, + } +}) + +/** + * Action atom to update filters + */ +export const updateTaskHistoryFiltersAtom = atom(null, (get, set, filters: Partial) => { + const currentFilters = get(taskHistoryFiltersAtom) + set(taskHistoryFiltersAtom, { ...currentFilters, ...filters }) + // Reset to first page when filters change + set(taskHistoryPageIndexAtom, 0) +}) + +/** + * Action atom to change page + */ +export const changeTaskHistoryPageAtom = atom(null, (get, set, pageIndex: number) => { + const data = get(taskHistoryDataAtom) + if (data && pageIndex >= 0 && pageIndex < data.pageCount) { + set(taskHistoryPageIndexAtom, pageIndex) + } +}) diff --git a/cli/src/state/atoms/ui.ts b/cli/src/state/atoms/ui.ts index ba9b58b0137..c4f9e7a5212 100644 --- a/cli/src/state/atoms/ui.ts +++ b/cli/src/state/atoms/ui.ts @@ -33,6 +33,7 @@ export const messagesAtom = atom([]) * Atom to track when messages have been reset/replaced * Increments each time replaceMessages is called to force Static component re-render */ +export const refreshTerminalCounterAtom = atom(0) export const messageResetCounterAtom = atom(0) /** @@ -313,8 +314,8 @@ export const clearMessagesAtom = atom(null, (get, set) => { * Increments the reset counter to force Static component re-render */ export const replaceMessagesAtom = atom(null, (get, set, messages: CliMessage[]) => { + set(messageCutoffTimestampAtom, 0) set(messagesAtom, messages) - set(messageResetCounterAtom, (prev) => prev + 1) }) /** @@ -352,6 +353,10 @@ export const updateTextBufferAtom = atom(null, (get, set, value: string) => { } }) +export const refreshTerminalAtom = atom(null, (get, set) => { + set(refreshTerminalCounterAtom, (prev) => prev + 1) +}) + /** * Action atom to clear the text buffer */ diff --git a/cli/src/state/hooks/useCommandContext.ts b/cli/src/state/hooks/useCommandContext.ts index 96bb73c842b..da4009d7d6a 100644 --- a/cli/src/state/hooks/useCommandContext.ts +++ b/cli/src/state/hooks/useCommandContext.ts @@ -13,14 +13,24 @@ import { replaceMessagesAtom, setMessageCutoffTimestampAtom, isCommittingParallelModeAtom, + refreshTerminalAtom, } from "../atoms/ui.js" import { setModeAtom, providerAtom, updateProviderAtom } from "../atoms/config.js" import { routerModelsAtom, extensionStateAtom, isParallelModeAtom } from "../atoms/extension.js" import { requestRouterModelsAtom } from "../atoms/actions.js" import { profileDataAtom, balanceDataAtom, profileLoadingAtom, balanceLoadingAtom } from "../atoms/profile.js" +import { + taskHistoryDataAtom, + taskHistoryFiltersAtom, + taskHistoryLoadingAtom, + taskHistoryErrorAtom, +} from "../atoms/taskHistory.js" import { useWebviewMessage } from "./useWebviewMessage.js" +import { useTaskHistory } from "./useTaskHistory.js" import { getModelIdKey } from "../../constants/providers/models.js" +const TERMINAL_CLEAR_DELAY_MS = 500 + /** * Factory function type for creating CommandContext */ @@ -67,6 +77,7 @@ export function useCommandContext(): UseCommandContextReturn { const refreshRouterModels = useSetAtom(requestRouterModelsAtom) const setMessageCutoffTimestamp = useSetAtom(setMessageCutoffTimestampAtom) const setCommittingParallelMode = useSetAtom(isCommittingParallelModeAtom) + const refreshTerminal = useSetAtom(refreshTerminalAtom) const { sendMessage, clearTask } = useWebviewMessage() // Get read-only state @@ -82,6 +93,19 @@ export function useCommandContext(): UseCommandContextReturn { const profileLoading = useAtomValue(profileLoadingAtom) const balanceLoading = useAtomValue(balanceLoadingAtom) + // Get task history state and functions + const taskHistoryData = useAtomValue(taskHistoryDataAtom) + const taskHistoryFilters = useAtomValue(taskHistoryFiltersAtom) + const taskHistoryLoading = useAtomValue(taskHistoryLoadingAtom) + const taskHistoryError = useAtomValue(taskHistoryErrorAtom) + const { + fetchTaskHistory, + updateFilters: updateTaskHistoryFiltersAndFetch, + changePage: changeTaskHistoryPageAndFetch, + nextPage: nextTaskHistoryPage, + previousPage: previousTaskHistoryPage, + } = useTaskHistory() + // Create the factory function const createContext = useCallback( (input: string, args: string[], options: Record, onExit: () => void): CommandContext => { @@ -98,6 +122,14 @@ export function useCommandContext(): UseCommandContextReturn { clearMessages: () => { clearMessages() }, + refreshTerminal: () => { + return new Promise((resolve) => { + refreshTerminal() + setTimeout(() => { + resolve() + }, TERMINAL_CLEAR_DELAY_MS) + }) + }, replaceMessages: (messages: CliMessage[]) => { replaceMessages(messages) }, @@ -143,6 +175,17 @@ export function useCommandContext(): UseCommandContextReturn { balanceData, profileLoading, balanceLoading, + // Task history context + taskHistoryData, + taskHistoryFilters, + taskHistoryLoading, + taskHistoryError, + fetchTaskHistory, + updateTaskHistoryFilters: updateTaskHistoryFiltersAndFetch, + changeTaskHistoryPage: changeTaskHistoryPageAndFetch, + nextTaskHistoryPage, + previousTaskHistoryPage, + sendWebviewMessage: sendMessage, } }, [ @@ -151,6 +194,7 @@ export function useCommandContext(): UseCommandContextReturn { setMode, sendMessage, clearTask, + refreshTerminal, routerModels, currentProvider, kilocodeDefaultModel, @@ -164,6 +208,15 @@ export function useCommandContext(): UseCommandContextReturn { balanceLoading, setCommittingParallelMode, isParallelMode, + taskHistoryData, + taskHistoryFilters, + taskHistoryLoading, + taskHistoryError, + fetchTaskHistory, + updateTaskHistoryFiltersAndFetch, + changeTaskHistoryPageAndFetch, + nextTaskHistoryPage, + previousTaskHistoryPage, ], ) diff --git a/cli/src/state/hooks/useCommandInput.ts b/cli/src/state/hooks/useCommandInput.ts index 2b6cb9055f0..926e346f187 100644 --- a/cli/src/state/hooks/useCommandInput.ts +++ b/cli/src/state/hooks/useCommandInput.ts @@ -35,6 +35,7 @@ import { routerModelsAtom, extensionStateAtom } from "../atoms/extension.js" import { providerAtom, updateProviderAtom } from "../atoms/config.js" import { requestRouterModelsAtom } from "../atoms/actions.js" import { profileDataAtom, profileLoadingAtom } from "../atoms/profile.js" +import { taskHistoryDataAtom } from "../atoms/taskHistory.js" import { getModelIdKey } from "../../constants/providers/models.js" /** @@ -147,6 +148,7 @@ export function useCommandInput(): UseCommandInputReturn { const kilocodeDefaultModel = extensionState?.kilocodeDefaultModel || "" const profileData = useAtomValue(profileDataAtom) const profileLoading = useAtomValue(profileLoadingAtom) + const taskHistoryData = useAtomValue(taskHistoryDataAtom) // Write atoms const setInputAction = useSetAtom(updateTextBufferAtom) @@ -210,6 +212,7 @@ export function useCommandInput(): UseCommandInputReturn { kilocodeDefaultModel, profileData, profileLoading, + taskHistoryData, updateProviderModel: async (modelId: string) => { if (!currentProvider) { throw new Error("No provider configured") @@ -241,6 +244,7 @@ export function useCommandInput(): UseCommandInputReturn { kilocodeDefaultModel, profileData, profileLoading, + taskHistoryData, updateProvider, refreshRouterModels, ]) diff --git a/cli/src/state/hooks/useTaskHistory.ts b/cli/src/state/hooks/useTaskHistory.ts new file mode 100644 index 00000000000..256a268ac53 --- /dev/null +++ b/cli/src/state/hooks/useTaskHistory.ts @@ -0,0 +1,109 @@ +/** + * Hook for managing task history + */ + +import { useAtom, useAtomValue, useSetAtom } from "jotai" +import { useCallback } from "react" +import { + taskHistoryDataAtom, + taskHistoryFiltersAtom, + taskHistoryPageIndexAtom, + taskHistoryLoadingAtom, + taskHistoryErrorAtom, + updateTaskHistoryFiltersAtom, + changeTaskHistoryPageAtom, + type TaskHistoryFilters, +} from "../atoms/taskHistory.js" +import { extensionServiceAtom } from "../atoms/service.js" + +export function useTaskHistory() { + const service = useAtomValue(extensionServiceAtom) + const [data] = useAtom(taskHistoryDataAtom) + const [filters] = useAtom(taskHistoryFiltersAtom) + const [pageIndex] = useAtom(taskHistoryPageIndexAtom) + const loading = useAtomValue(taskHistoryLoadingAtom) + const error = useAtomValue(taskHistoryErrorAtom) + const updateFilters = useSetAtom(updateTaskHistoryFiltersAtom) + const changePage = useSetAtom(changeTaskHistoryPageAtom) + + /** + * Fetch task history from the extension + */ + const fetchTaskHistory = useCallback(async () => { + if (!service) { + return + } + + try { + // Send task history request to extension + await service.sendWebviewMessage({ + type: "taskHistoryRequest", + payload: { + requestId: Date.now().toString(), + workspace: filters.workspace, + sort: filters.sort, + favoritesOnly: filters.favoritesOnly, + pageIndex, + search: filters.search, + }, + }) + } catch (err) { + console.error("Failed to fetch task history:", err) + } + }, [service, filters, pageIndex]) + + /** + * Update filters and fetch new data + */ + const updateFiltersAndFetch = useCallback( + async (newFilters: Partial) => { + updateFilters(newFilters) + // Wait a bit for the atom to update + setTimeout(() => fetchTaskHistory(), 50) + }, + [updateFilters, fetchTaskHistory], + ) + + /** + * Change page and fetch new data + */ + const changePageAndFetch = useCallback( + async (newPageIndex: number) => { + changePage(newPageIndex) + // Wait a bit for the atom to update + setTimeout(() => fetchTaskHistory(), 50) + }, + [changePage, fetchTaskHistory], + ) + + /** + * Go to next page + */ + const nextPage = useCallback(async () => { + if (data && pageIndex < data.pageCount - 1) { + await changePageAndFetch(pageIndex + 1) + } + }, [data, pageIndex, changePageAndFetch]) + + /** + * Go to previous page + */ + const previousPage = useCallback(async () => { + if (pageIndex > 0) { + await changePageAndFetch(pageIndex - 1) + } + }, [pageIndex, changePageAndFetch]) + + return { + data, + filters, + pageIndex, + loading, + error, + fetchTaskHistory, + updateFilters: updateFiltersAndFetch, + changePage: changePageAndFetch, + nextPage, + previousPage, + } +} diff --git a/cli/src/state/hooks/useTerminal.ts b/cli/src/state/hooks/useTerminal.ts index f6dd6dcdd22..0014343966b 100644 --- a/cli/src/state/hooks/useTerminal.ts +++ b/cli/src/state/hooks/useTerminal.ts @@ -1,12 +1,12 @@ import { useAtomValue, useSetAtom } from "jotai" -import { messageResetCounterAtom, messageCutoffTimestampAtom } from "../atoms/ui.js" +import { refreshTerminalCounterAtom, messageResetCounterAtom } from "../atoms/ui.js" import { useCallback, useEffect, useRef } from "react" export function useTerminal(): void { const width = useRef(process.stdout.columns) const incrementResetCounter = useSetAtom(messageResetCounterAtom) - const messageCutoffTimestamp = useAtomValue(messageCutoffTimestampAtom) + const refreshTerminalCounter = useAtomValue(refreshTerminalCounterAtom) const clearTerminal = useCallback(() => { // Clear the terminal screen and reset cursor position @@ -14,16 +14,16 @@ export function useTerminal(): void { // \x1b[3J - Clear scrollback buffer (needed for gnome-terminal) // \x1b[H - Move cursor to home position (0,0) process.stdout.write("\x1b[2J\x1b[3J\x1b[H") - - // Increment reset counter to force Static component remount + // Increment the message reset counter to force re-render of Static component incrementResetCounter((prev) => prev + 1) - }, [incrementResetCounter]) + }, []) + // Clear terminal when reset counter changes useEffect(() => { - if (messageCutoffTimestamp !== 0) { + if (refreshTerminalCounter !== 0) { clearTerminal() } - }, [messageCutoffTimestamp, clearTerminal]) + }, [refreshTerminalCounter, clearTerminal]) // Resize effect useEffect(() => { diff --git a/cli/src/ui/UI.tsx b/cli/src/ui/UI.tsx index b3cf77a6b51..0c8b2ee67e8 100644 --- a/cli/src/ui/UI.tsx +++ b/cli/src/ui/UI.tsx @@ -23,6 +23,7 @@ import { useMessageHandler } from "../state/hooks/useMessageHandler.js" import { useFollowupHandler } from "../state/hooks/useFollowupHandler.js" import { useApprovalMonitor } from "../state/hooks/useApprovalMonitor.js" import { useProfile } from "../state/hooks/useProfile.js" +import { useTaskHistory } from "../state/hooks/useTaskHistory.js" import { useCIMode } from "../state/hooks/useCIMode.js" import { useTheme } from "../state/hooks/useTheme.js" import { AppOptions } from "./App.js" @@ -73,6 +74,9 @@ export const UI: React.FC = ({ options, onExit }) => { // Profile hook for handling profile/balance data responses useProfile() + // Task history hook for fetching task history + const { fetchTaskHistory } = useTaskHistory() + // This clears the terminal and forces re-render of static components useTerminal() @@ -194,6 +198,7 @@ export const UI: React.FC = ({ options, onExit }) => { } }, []) + // Show update or notification messages useEffect(() => { if (!versionStatus) return @@ -205,6 +210,15 @@ export const UI: React.FC = ({ options, onExit }) => { } }, [notifications, versionStatus]) + // Fetch task history on mount if not in CI mode + const taskHistoryFetchedRef = useRef(false) + useEffect(() => { + if (!taskHistoryFetchedRef.current && !options.ci && configValidation.valid) { + taskHistoryFetchedRef.current = true + fetchTaskHistory() + } + }, [options.ci, configValidation.valid, fetchTaskHistory]) + // Exit if provider configuration is invalid useEffect(() => { if (!configValidation.valid) { diff --git a/cli/src/ui/messages/MessageDisplay.tsx b/cli/src/ui/messages/MessageDisplay.tsx index 0bdd30b4faf..39ca76c68dc 100644 --- a/cli/src/ui/messages/MessageDisplay.tsx +++ b/cli/src/ui/messages/MessageDisplay.tsx @@ -42,6 +42,7 @@ import { Box, Static } from "ink" import { useAtomValue } from "jotai" import { type UnifiedMessage, staticMessagesAtom, dynamicMessagesAtom } from "../../state/atoms/ui.js" import { MessageRow } from "./MessageRow.js" +import { logs } from "../../services/logs.js" interface MessageDisplayProps { /** Optional filter to show only specific message types */ From 31deddbda6cd5e3a4654abd9ca09de7ad75a8f3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 16:12:52 -0300 Subject: [PATCH 2/9] refactor: remove plan md --- cli/TASKS_IMPLEMENTATION_PLAN.md | 354 ------------------------------- 1 file changed, 354 deletions(-) delete mode 100644 cli/TASKS_IMPLEMENTATION_PLAN.md diff --git a/cli/TASKS_IMPLEMENTATION_PLAN.md b/cli/TASKS_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 998b3fcef8f..00000000000 --- a/cli/TASKS_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,354 +0,0 @@ -# Implementation Plan: /tasks Command for CLI - -## Overview - -Implement a `/tasks` command in the CLI that provides similar functionality to the webview's HistoryView component, allowing users to: - -- View task history -- Search and filter tasks -- Switch between tasks -- Navigate with pagination - -## Architecture - -### 1. Message Flow - -``` -CLI (/tasks command) - ↓ -sendWebviewMessage({ type: "taskHistoryRequest", payload }) - ↓ -Extension (webviewMessageHandler.ts) - ↓ -getTaskHistory() processing - ↓ -postMessageToWebview({ type: "taskHistoryResponse", payload }) - ↓ -CLI (message handler in effects.ts) - ↓ -Update task history atoms - ↓ -Display in TUI -``` - -### 2. Required Components - -#### A. New Atoms (`cli/src/state/atoms/taskHistory.ts`) - -```typescript -// Task history state atoms -export const taskHistoryAtom = atom([]) -export const taskHistoryPageAtom = atom(0) -export const taskHistoryPageCountAtom = atom(1) -export const taskHistoryTotalAtom = atom(0) -export const taskHistorySearchQueryAtom = atom("") -export const taskHistorySortOptionAtom = atom("newest") -export const taskHistoryLoadingAtom = atom(false) -export const taskHistoryErrorAtom = atom(null) - -// Action atoms -export const requestTaskHistoryAtom = atom(null, async (get, set, params) => { - // Send taskHistoryRequest message -}) - -export const selectTaskAtom = atom(null, async (get, set, taskId: string) => { - // Send selectTask message to switch to a task -}) -``` - -#### B. New Hook (`cli/src/state/hooks/useTaskHistory.ts`) - -```typescript -export function useTaskHistory() { - const taskHistory = useAtomValue(taskHistoryAtom) - const currentPage = useAtomValue(taskHistoryPageAtom) - const pageCount = useAtomValue(taskHistoryPageCountAtom) - const searchQuery = useAtomValue(taskHistorySearchQueryAtom) - const sortOption = useAtomValue(taskHistorySortOptionAtom) - const isLoading = useAtomValue(taskHistoryLoadingAtom) - const error = useAtomValue(taskHistoryErrorAtom) - - const requestHistory = useSetAtom(requestTaskHistoryAtom) - const selectTask = useSetAtom(selectTaskAtom) - const setSearchQuery = useSetAtom(taskHistorySearchQueryAtom) - const setSortOption = useSetAtom(taskHistorySortOptionAtom) - const setPage = useSetAtom(taskHistoryPageAtom) - - return { - tasks: taskHistory, - currentPage, - pageCount, - searchQuery, - sortOption, - isLoading, - error, - requestHistory, - selectTask, - setSearchQuery, - setSortOption, - setPage, - } -} -``` - -#### C. Tasks Command (`cli/src/commands/tasks.ts`) - -```typescript -export const tasksCommand: Command = { - name: "tasks", - description: "View and manage task history", - arguments: [ - { - name: "action", - description: "Action to perform (list, search, select)", - required: false, - }, - { - name: "value", - description: "Value for the action (search query or task ID)", - required: false, - }, - ], - options: [ - { - name: "sort", - description: "Sort option (newest, oldest, mostExpensive, mostTokens)", - shorthand: "s", - }, - { - name: "page", - description: "Page number", - shorthand: "p", - }, - { - name: "workspace", - description: "Filter by workspace (current, all)", - shorthand: "w", - }, - ], - handler: async (context: CommandContext) => { - // Implementation - }, -} -``` - -#### D. Task History UI Component (`cli/src/ui/components/TaskHistoryView.tsx`) - -```typescript -export const TaskHistoryView: React.FC = () => { - const { - tasks, - currentPage, - pageCount, - searchQuery, - sortOption, - isLoading, - error, - requestHistory, - selectTask, - setSearchQuery, - setSortOption, - setPage, - } = useTaskHistory() - - // Render task list with: - // - Search input - // - Sort dropdown - // - Task items with ID, name, timestamp, mode - // - Pagination controls - // - Select action to switch tasks -} -``` - -### 3. Message Handlers - -#### A. Update `effects.ts` to handle task history responses: - -```typescript -case "taskHistoryResponse": - if (message.payload) { - set(updateTaskHistoryAtom, message.payload) - } - break -``` - -#### B. Add new message types to `cli/src/types/messages.ts`: - -```typescript -export interface TaskHistoryRequestPayload { - pageIndex: number - searchQuery?: string - sortOption?: SortOption - showAllWorkspaces?: boolean - showFavoritesOnly?: boolean -} - -export interface TaskHistoryResponsePayload { - historyItems: HistoryItem[] - pageIndex: number - pageCount: number - totalItems: number -} - -export type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant" -``` - -### 4. Implementation Steps - -1. **Create task history atoms** (`taskHistory.ts`) - - - State atoms for history data, pagination, search, sort - - Action atoms for requesting history and selecting tasks - -2. **Create useTaskHistory hook** (`useTaskHistory.ts`) - - - Encapsulate task history state and actions - - Provide clean interface for UI components - -3. **Implement /tasks command** (`tasks.ts`) - - - Parse command arguments and options - - Handle different actions: list, search, select - - Trigger appropriate state updates - -4. **Add message handlers** (`effects.ts`) - - - Handle taskHistoryResponse messages - - Update task history atoms with response data - -5. **Create TaskHistoryView component** (`TaskHistoryView.tsx`) - - - Display task list in terminal-friendly format - - Show search input and sort options - - Implement pagination controls - - Handle task selection for switching - -6. **Register command** (`commands/index.ts`) - - - Import and register the tasks command - -7. **Add task switching logic** - - Send selectTask message to extension - - Handle task restoration/resumption - -### 5. User Experience - -#### Command Usage Examples: - -```bash -# List all tasks (default) -/tasks - -# Search tasks -/tasks search "implement feature" - -# Select and switch to a task -/tasks select task-id-123 - -# List with options -/tasks --sort=oldest --page=2 --workspace=all - -# Short form -/tasks -s oldest -p 2 -w all -``` - -#### Display Format: - -``` -Task History (Page 1/5) -Search: [________________] Sort: [Newest ▼] - -1. [2024-01-15 10:30] Implement user authentication (code) - ID: task-123 | Workspace: /project/app - -2. [2024-01-15 09:15] Fix navigation bug (debug) - ID: task-124 | Workspace: /project/app - -3. [2024-01-14 16:45] Create API documentation (architect) - ID: task-125 | Workspace: /project/docs - -[Previous] Page 1 of 5 [Next] - -Actions: (s)elect, (f)ilter, (r)efresh, (q)uit -``` - -### 6. Testing Requirements - -1. **Unit Tests** - - - Test command parsing and validation - - Test atom state updates - - Test message handling - -2. **Integration Tests** - - - Test full flow from command to display - - Test pagination navigation - - Test search and filtering - - Test task switching - -3. **Edge Cases** - - Empty task history - - Network/service errors - - Invalid task IDs - - Pagination boundaries - -### 7. Future Enhancements - -1. **Advanced Filtering** - - - Filter by date range - - Filter by mode - - Filter by status (completed/active) - -2. **Bulk Operations** - - - Delete multiple tasks - - Export task history - -3. **Task Details View** - - - Show full task details - - Display task messages - - Show todo items - -4. **Keyboard Shortcuts** - - Quick navigation with arrow keys - - Vim-style navigation (j/k) - - Quick actions (Enter to select, / to search) - -## Implementation Priority - -1. **Phase 1: Core Functionality** - - - Basic /tasks command - - List tasks with pagination - - Simple task selection - -2. **Phase 2: Search & Filter** - - - Search by task name - - Sort options - - Workspace filtering - -3. **Phase 3: Enhanced UX** - - Interactive UI with keyboard navigation - - Task details view - - Bulk operations - -## Dependencies - -- Extension must support `taskHistoryRequest` message type -- Extension must support `selectTask` message type -- Existing message bridge infrastructure -- React and Ink for TUI components - -## Success Criteria - -- [ ] Users can list their task history -- [ ] Users can search tasks by name -- [ ] Users can sort tasks by different criteria -- [ ] Users can navigate pages of results -- [ ] Users can select and switch to a previous task -- [ ] Error states are handled gracefully -- [ ] Performance is acceptable for large task histories From efb88b302a90250b83495f1a62eb59acd25f113c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 17:19:44 -0300 Subject: [PATCH 3/9] refactor: async get tasks --- cli/src/commands/core/types.ts | 8 +- cli/src/commands/tasks.ts | 159 +++++++++++++++++--------- cli/src/state/atoms/effects.ts | 30 ++++- cli/src/state/atoms/taskHistory.ts | 75 ++++++++++++ cli/src/state/hooks/useTaskHistory.ts | 115 ++++++++++++++++--- 5 files changed, 308 insertions(+), 79 deletions(-) diff --git a/cli/src/commands/core/types.ts b/cli/src/commands/core/types.ts index ed5add9245b..85dad4ca1a8 100644 --- a/cli/src/commands/core/types.ts +++ b/cli/src/commands/core/types.ts @@ -62,10 +62,10 @@ export interface CommandContext { taskHistoryLoading: boolean taskHistoryError: string | null fetchTaskHistory: () => Promise - updateTaskHistoryFilters: (filters: Partial) => Promise - changeTaskHistoryPage: (pageIndex: number) => Promise - nextTaskHistoryPage: () => Promise - previousTaskHistoryPage: () => Promise + updateTaskHistoryFilters: (filters: Partial) => Promise + changeTaskHistoryPage: (pageIndex: number) => Promise + nextTaskHistoryPage: () => Promise + previousTaskHistoryPage: () => Promise sendWebviewMessage: (message: any) => Promise refreshTerminal: () => Promise } diff --git a/cli/src/commands/tasks.ts b/cli/src/commands/tasks.ts index 489541af550..5ed3b2d01b9 100644 --- a/cli/src/commands/tasks.ts +++ b/cli/src/commands/tasks.ts @@ -66,11 +66,14 @@ function truncate(text: string, maxLength: number): string { /** * Show current task history */ -async function showTaskHistory(context: any): Promise { +async function showTaskHistory(context: any, dataOverride?: any): Promise { const { taskHistoryData, taskHistoryLoading, taskHistoryError, fetchTaskHistory, addMessage } = context + // Use override data if provided, otherwise use context data + const data = dataOverride || taskHistoryData + // If loading, show loading message - if (taskHistoryLoading) { + if (taskHistoryLoading && !dataOverride) { addMessage({ id: Date.now().toString(), type: "system", @@ -81,7 +84,7 @@ async function showTaskHistory(context: any): Promise { } // If error, show error message - if (taskHistoryError) { + if (taskHistoryError && !dataOverride) { addMessage({ id: Date.now().toString(), type: "error", @@ -92,7 +95,7 @@ async function showTaskHistory(context: any): Promise { } // If no data, fetch it - if (!taskHistoryData) { + if (!data) { await fetchTaskHistory() addMessage({ id: Date.now().toString(), @@ -103,7 +106,7 @@ async function showTaskHistory(context: any): Promise { return } - const { historyItems, pageIndex, pageCount } = taskHistoryData + const { historyItems, pageIndex, pageCount } = data if (historyItems.length === 0) { addMessage({ @@ -155,8 +158,6 @@ async function searchTasks(context: any, query: string): Promise { return } - await updateTaskHistoryFilters({ search: query, sort: "mostRelevant" }) - addMessage({ id: Date.now().toString(), type: "system", @@ -164,8 +165,19 @@ async function searchTasks(context: any, query: string): Promise { ts: Date.now(), }) - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + try { + // Wait for the new data to arrive + const newData = await updateTaskHistoryFilters({ search: query, sort: "mostRelevant" }) + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to search tasks: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** @@ -245,8 +257,6 @@ async function changePage(context: any, pageNum: string): Promise { return } - await changeTaskHistoryPage(pageIndex) - addMessage({ id: Date.now().toString(), type: "system", @@ -254,8 +264,19 @@ async function changePage(context: any, pageNum: string): Promise { ts: Date.now(), }) - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + try { + // Wait for the new data to arrive + const newData = await changeTaskHistoryPage(pageIndex) + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to load page: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** @@ -284,8 +305,6 @@ async function nextPage(context: any): Promise { return } - await nextTaskHistoryPage() - addMessage({ id: Date.now().toString(), type: "system", @@ -293,8 +312,19 @@ async function nextPage(context: any): Promise { ts: Date.now(), }) - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + try { + // Wait for the new data to arrive + const newData = await nextTaskHistoryPage() + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to load next page: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** @@ -323,8 +353,6 @@ async function previousPage(context: any): Promise { return } - await previousTaskHistoryPage() - addMessage({ id: Date.now().toString(), type: "system", @@ -332,8 +360,19 @@ async function previousPage(context: any): Promise { ts: Date.now(), }) - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + try { + // Wait for the new data to arrive + const newData = await previousTaskHistoryPage() + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to load previous page: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** @@ -355,8 +394,6 @@ async function changeSortOrder(context: any, sortOption: string): Promise return } - await updateTaskHistoryFilters({ sort: mappedSort as any }) - addMessage({ id: Date.now().toString(), type: "system", @@ -364,8 +401,19 @@ async function changeSortOrder(context: any, sortOption: string): Promise ts: Date.now(), }) - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + try { + // Wait for the new data to arrive + const newData = await updateTaskHistoryFilters({ sort: mappedSort as any }) + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to change sort order: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** @@ -374,45 +422,28 @@ async function changeSortOrder(context: any, sortOption: string): Promise async function changeFilter(context: any, filterOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context + let filterUpdate: any + let loadingMessage: string + switch (filterOption) { case "current": - await updateTaskHistoryFilters({ workspace: "current" }) - addMessage({ - id: Date.now().toString(), - type: "system", - content: "Filtering to current workspace...", - ts: Date.now(), - }) + filterUpdate = { workspace: "current" } + loadingMessage = "Filtering to current workspace..." break case "all": - await updateTaskHistoryFilters({ workspace: "all" }) - addMessage({ - id: Date.now().toString(), - type: "system", - content: "Showing all workspaces...", - ts: Date.now(), - }) + filterUpdate = { workspace: "all" } + loadingMessage = "Showing all workspaces..." break case "favorites": - await updateTaskHistoryFilters({ favoritesOnly: true }) - addMessage({ - id: Date.now().toString(), - type: "system", - content: "Showing favorites only...", - ts: Date.now(), - }) + filterUpdate = { favoritesOnly: true } + loadingMessage = "Showing favorites only..." break case "all-tasks": - await updateTaskHistoryFilters({ favoritesOnly: false }) - addMessage({ - id: Date.now().toString(), - type: "system", - content: "Showing all tasks...", - ts: Date.now(), - }) + filterUpdate = { favoritesOnly: false } + loadingMessage = "Showing all tasks..." break default: @@ -425,8 +456,26 @@ async function changeFilter(context: any, filterOption: string): Promise { return } - // Show results after a brief delay - setTimeout(() => showTaskHistory(context), 100) + addMessage({ + id: Date.now().toString(), + type: "system", + content: loadingMessage, + ts: Date.now(), + }) + + try { + // Wait for the new data to arrive + const newData = await updateTaskHistoryFilters(filterUpdate) + // Now display the fresh data + await showTaskHistory(context, newData) + } catch (error) { + addMessage({ + id: Date.now().toString(), + type: "error", + content: `Failed to change filter: ${error instanceof Error ? error.message : String(error)}`, + ts: Date.now(), + }) + } } /** diff --git a/cli/src/state/atoms/effects.ts b/cli/src/state/atoms/effects.ts index cb750d527d5..1aa509cf2f1 100644 --- a/cli/src/state/atoms/effects.ts +++ b/cli/src/state/atoms/effects.ts @@ -16,7 +16,12 @@ import { setProfileErrorAtom, setBalanceErrorAtom, } from "./profile.js" -import { taskHistoryDataAtom, taskHistoryLoadingAtom, taskHistoryErrorAtom } from "./taskHistory.js" +import { + taskHistoryDataAtom, + taskHistoryLoadingAtom, + taskHistoryErrorAtom, + resolveTaskHistoryRequestAtom, +} from "./taskHistory.js" import { logs } from "../../services/logs.js" /** @@ -167,15 +172,32 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension // Handle task history response set(taskHistoryLoadingAtom, false) if (message.payload) { - const { historyItems, pageIndex, pageCount } = message.payload as any - set(taskHistoryDataAtom, { + const { historyItems, pageIndex, pageCount, requestId } = message.payload as any + const data = { historyItems: historyItems || [], pageIndex: pageIndex || 0, pageCount: pageCount || 1, - }) + } + set(taskHistoryDataAtom, data) set(taskHistoryErrorAtom, null) + + // Resolve any pending request with this requestId + if (requestId) { + console.log("[taskHistoryResponse] Resolving request:", requestId) + set(resolveTaskHistoryRequestAtom, { requestId, data }) + } else { + console.warn("[taskHistoryResponse] No requestId in response!") + } } else { + console.error("[taskHistoryResponse] No payload in response") set(taskHistoryErrorAtom, "Failed to fetch task history") + // Reject any pending requests + if (message.payload?.requestId) { + set(resolveTaskHistoryRequestAtom, { + requestId: message.payload.requestId, + error: "Failed to fetch task history", + }) + } } break diff --git a/cli/src/state/atoms/taskHistory.ts b/cli/src/state/atoms/taskHistory.ts index 35c2ecdbb84..b7d2ff0c5d1 100644 --- a/cli/src/state/atoms/taskHistory.ts +++ b/cli/src/state/atoms/taskHistory.ts @@ -24,6 +24,16 @@ export interface TaskHistoryFilters { search?: string } +/** + * Pending request resolver + */ +interface PendingRequest { + requestId: string + resolve: (data: TaskHistoryData) => void + reject: (error: Error) => void + timeout: NodeJS.Timeout +} + /** * Current task history data */ @@ -58,6 +68,11 @@ export const taskHistoryErrorAtom = atom(null) */ export const taskHistoryRequestIdAtom = atom(0) +/** + * Map of pending requests waiting for responses + */ +export const taskHistoryPendingRequestsAtom = atom>(new Map()) + /** * Action atom to fetch task history */ @@ -97,3 +112,63 @@ export const changeTaskHistoryPageAtom = atom(null, (get, set, pageIndex: number set(taskHistoryPageIndexAtom, pageIndex) } }) + +/** + * Action atom to add a pending request + */ +export const addPendingRequestAtom = atom( + null, + ( + get, + set, + request: { + requestId: string + resolve: (data: TaskHistoryData) => void + reject: (error: Error) => void + timeout: NodeJS.Timeout + }, + ) => { + const pendingRequests = get(taskHistoryPendingRequestsAtom) + const newPendingRequests = new Map(pendingRequests) + newPendingRequests.set(request.requestId, request) + set(taskHistoryPendingRequestsAtom, newPendingRequests) + }, +) + +/** + * Action atom to remove a pending request + */ +export const removePendingRequestAtom = atom(null, (get, set, requestId: string) => { + const pendingRequests = get(taskHistoryPendingRequestsAtom) + const request = pendingRequests.get(requestId) + if (request) { + clearTimeout(request.timeout) + const newPendingRequests = new Map(pendingRequests) + newPendingRequests.delete(requestId) + set(taskHistoryPendingRequestsAtom, newPendingRequests) + } +}) + +/** + * Action atom to resolve a pending request + */ +export const resolveTaskHistoryRequestAtom = atom( + null, + (get, set, { requestId, data, error }: { requestId: string; data?: TaskHistoryData; error?: string }) => { + const pendingRequests = get(taskHistoryPendingRequestsAtom) + const request = pendingRequests.get(requestId) + + if (request) { + clearTimeout(request.timeout) + if (error) { + request.reject(new Error(error)) + } else if (data) { + request.resolve(data) + } + // Remove from pending requests + const newPendingRequests = new Map(pendingRequests) + newPendingRequests.delete(requestId) + set(taskHistoryPendingRequestsAtom, newPendingRequests) + } + }, +) diff --git a/cli/src/state/hooks/useTaskHistory.ts b/cli/src/state/hooks/useTaskHistory.ts index 256a268ac53..a4bfaf7a0dc 100644 --- a/cli/src/state/hooks/useTaskHistory.ts +++ b/cli/src/state/hooks/useTaskHistory.ts @@ -12,9 +12,13 @@ import { taskHistoryErrorAtom, updateTaskHistoryFiltersAtom, changeTaskHistoryPageAtom, + addPendingRequestAtom, + removePendingRequestAtom, type TaskHistoryFilters, + type TaskHistoryData, } from "../atoms/taskHistory.js" import { extensionServiceAtom } from "../atoms/service.js" +import { logs } from "../../services/logs.js" export function useTaskHistory() { const service = useAtomValue(extensionServiceAtom) @@ -25,6 +29,8 @@ export function useTaskHistory() { const error = useAtomValue(taskHistoryErrorAtom) const updateFilters = useSetAtom(updateTaskHistoryFiltersAtom) const changePage = useSetAtom(changeTaskHistoryPageAtom) + const addPendingRequest = useSetAtom(addPendingRequestAtom) + const removePendingRequest = useSetAtom(removePendingRequestAtom) /** * Fetch task history from the extension @@ -48,51 +54,128 @@ export function useTaskHistory() { }, }) } catch (err) { - console.error("Failed to fetch task history:", err) + logs.error("fetchTaskHistory error:", "useTaskHistory", { error: err }) } }, [service, filters, pageIndex]) /** - * Update filters and fetch new data + * Update filters and fetch new data - returns a Promise that resolves when data arrives */ const updateFiltersAndFetch = useCallback( - async (newFilters: Partial) => { + async (newFilters: Partial): Promise => { + if (!service) { + throw new Error("Extension service not available") + } + updateFilters(newFilters) - // Wait a bit for the atom to update - setTimeout(() => fetchTaskHistory(), 50) + + // Create a unique request ID + const requestId = `${Date.now()}-${Math.random()}` + + // Get the updated filters (filters will be reset to page 0 by updateFilters) + const updatedFilters = { ...filters, ...newFilters } + + // Create a promise that will be resolved when the response arrives + return new Promise((resolve, reject) => { + // Set up timeout (5 seconds) + const timeout = setTimeout(() => { + removePendingRequest(requestId) + reject(new Error("Request timeout - no response received")) + }, 5000) + + // Store the resolver using the action atom + addPendingRequest({ requestId, resolve, reject, timeout }) + + // Send the request with updated filters + service + .sendWebviewMessage({ + type: "taskHistoryRequest", + payload: { + requestId, + workspace: updatedFilters.workspace, + sort: updatedFilters.sort, + favoritesOnly: updatedFilters.favoritesOnly, + pageIndex: 0, // Filters reset to page 0 + search: updatedFilters.search, + }, + }) + .catch((err) => { + removePendingRequest(requestId) + reject(err) + }) + }) }, - [updateFilters, fetchTaskHistory], + [updateFilters, service, filters, addPendingRequest, removePendingRequest], ) /** - * Change page and fetch new data + * Change page and fetch new data - returns a Promise that resolves when data arrives */ const changePageAndFetch = useCallback( - async (newPageIndex: number) => { + async (newPageIndex: number): Promise => { + if (!service) { + throw new Error("Extension service not available") + } + changePage(newPageIndex) - // Wait a bit for the atom to update - setTimeout(() => fetchTaskHistory(), 50) + + // Create a unique request ID + const requestId = `${Date.now()}-${Math.random()}` + + // Create a promise that will be resolved when the response arrives + return new Promise((resolve, reject) => { + // Set up timeout (5 seconds) + const timeout = setTimeout(() => { + removePendingRequest(requestId) + reject(new Error("Request timeout - no response received")) + }, 5000) + + // Store the resolver using the action atom + addPendingRequest({ requestId, resolve, reject, timeout }) + + // Send the request + service + .sendWebviewMessage({ + type: "taskHistoryRequest", + payload: { + requestId, + workspace: filters.workspace, + sort: filters.sort, + favoritesOnly: filters.favoritesOnly, + pageIndex: newPageIndex, + search: filters.search, + }, + }) + .catch((err) => { + removePendingRequest(requestId) + reject(err) + }) + }) }, - [changePage, fetchTaskHistory], + [changePage, service, filters, addPendingRequest, removePendingRequest], ) /** * Go to next page */ - const nextPage = useCallback(async () => { + const nextPage = useCallback(async (): Promise => { if (data && pageIndex < data.pageCount - 1) { - await changePageAndFetch(pageIndex + 1) + return await changePageAndFetch(pageIndex + 1) } + // If already on last page, return current data + return data || { historyItems: [], pageIndex: 0, pageCount: 0 } }, [data, pageIndex, changePageAndFetch]) /** * Go to previous page */ - const previousPage = useCallback(async () => { + const previousPage = useCallback(async (): Promise => { if (pageIndex > 0) { - await changePageAndFetch(pageIndex - 1) + return await changePageAndFetch(pageIndex - 1) } - }, [pageIndex, changePageAndFetch]) + // If already on first page, return current data + return data || { historyItems: [], pageIndex: 0, pageCount: 0 } + }, [data, pageIndex, changePageAndFetch]) return { data, From 514be164c99641bb2527fbb9d6d95a23632b00a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 17:24:22 -0300 Subject: [PATCH 4/9] refactor: fix tests --- cli/src/commands/__tests__/clear.test.ts | 15 +++++++++++++ cli/src/commands/__tests__/new.test.ts | 27 +++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/__tests__/clear.test.ts b/cli/src/commands/__tests__/clear.test.ts index c78a5418ae0..1d4ff565310 100644 --- a/cli/src/commands/__tests__/clear.test.ts +++ b/cli/src/commands/__tests__/clear.test.ts @@ -32,6 +32,21 @@ describe("clearCommand", () => { balanceData: null, profileLoading: false, balanceLoading: false, + refreshTerminal: vi.fn().mockResolvedValue(undefined), + taskHistoryData: null, + taskHistoryFilters: { + workspace: "current", + sort: "newest", + favoritesOnly: false, + }, + taskHistoryLoading: false, + taskHistoryError: null, + fetchTaskHistory: vi.fn().mockResolvedValue(undefined), + updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), + changeTaskHistoryPage: vi.fn().mockResolvedValue(null), + nextTaskHistoryPage: vi.fn().mockResolvedValue(null), + previousTaskHistoryPage: vi.fn().mockResolvedValue(null), + sendWebviewMessage: vi.fn().mockResolvedValue(undefined), } }) diff --git a/cli/src/commands/__tests__/new.test.ts b/cli/src/commands/__tests__/new.test.ts index ee21870e19f..7b6de9f24c7 100644 --- a/cli/src/commands/__tests__/new.test.ts +++ b/cli/src/commands/__tests__/new.test.ts @@ -16,18 +16,39 @@ describe("/new command", () => { input: "/new", args: [], options: {}, - sendMessage: vi.fn(), + sendMessage: vi.fn().mockResolvedValue(undefined), addMessage: vi.fn(), clearMessages: vi.fn(), replaceMessages: vi.fn(), + setMessageCutoffTimestamp: vi.fn(), clearTask: vi.fn().mockResolvedValue(undefined), setMode: vi.fn(), exit: vi.fn(), routerModels: null, currentProvider: null, kilocodeDefaultModel: "", - updateProviderModel: vi.fn(), - refreshRouterModels: vi.fn(), + updateProviderModel: vi.fn().mockResolvedValue(undefined), + refreshRouterModels: vi.fn().mockResolvedValue(undefined), + updateProvider: vi.fn().mockResolvedValue(undefined), + profileData: null, + balanceData: null, + profileLoading: false, + balanceLoading: false, + refreshTerminal: vi.fn().mockResolvedValue(undefined), + taskHistoryData: null, + taskHistoryFilters: { + workspace: "current", + sort: "newest", + favoritesOnly: false, + }, + taskHistoryLoading: false, + taskHistoryError: null, + fetchTaskHistory: vi.fn().mockResolvedValue(undefined), + updateTaskHistoryFilters: vi.fn().mockResolvedValue(null), + changeTaskHistoryPage: vi.fn().mockResolvedValue(null), + nextTaskHistoryPage: vi.fn().mockResolvedValue(null), + previousTaskHistoryPage: vi.fn().mockResolvedValue(null), + sendWebviewMessage: vi.fn().mockResolvedValue(undefined), } }) From ddd8f145cb89511f9944fef4db51c703b864c96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 18:03:24 -0300 Subject: [PATCH 5/9] refactor: prevent duplicated messages --- cli/src/commands/tasks.ts | 113 +++++++++++++++++---------------- cli/src/state/atoms/effects.ts | 4 -- 2 files changed, 60 insertions(+), 57 deletions(-) diff --git a/cli/src/commands/tasks.ts b/cli/src/commands/tasks.ts index 5ed3b2d01b9..da0ada816df 100644 --- a/cli/src/commands/tasks.ts +++ b/cli/src/commands/tasks.ts @@ -68,6 +68,7 @@ function truncate(text: string, maxLength: number): string { */ async function showTaskHistory(context: any, dataOverride?: any): Promise { const { taskHistoryData, taskHistoryLoading, taskHistoryError, fetchTaskHistory, addMessage } = context + const now = Date.now() // Use override data if provided, otherwise use context data const data = dataOverride || taskHistoryData @@ -75,10 +76,10 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise // If loading, show loading message if (taskHistoryLoading && !dataOverride) { addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: "Loading task history...", - ts: Date.now(), + ts: now, }) return } @@ -86,10 +87,10 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise // If error, show error message if (taskHistoryError && !dataOverride) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to load task history: ${taskHistoryError}`, - ts: Date.now(), + ts: now, }) return } @@ -98,10 +99,10 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise if (!data) { await fetchTaskHistory() addMessage({ - id: Date.now().toString(), + id: `system-no-data-${now}`, type: "system", content: "Loading task history...", - ts: Date.now(), + ts: now, }) return } @@ -110,10 +111,10 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise if (historyItems.length === 0) { addMessage({ - id: Date.now().toString(), + id: `system-no-tasks-${now}`, type: "system", content: "No tasks found in history.", - ts: Date.now(), + ts: now, }) return } @@ -135,10 +136,10 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise }) addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content, - ts: Date.now(), + ts: now, }) } @@ -147,22 +148,23 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise */ async function searchTasks(context: any, query: string): Promise { const { updateTaskHistoryFilters, addMessage } = context + const now = Date.now() if (!query) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "Usage: /tasks search ", - ts: Date.now(), + ts: now, }) return } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: `Searching for "${query}"...`, - ts: Date.now(), + ts: now, }) try { @@ -172,10 +174,10 @@ async function searchTasks(context: any, query: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to search tasks: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -185,19 +187,19 @@ async function searchTasks(context: any, query: string): Promise { */ async function selectTask(context: any, taskId: string): Promise { const { sendWebviewMessage, addMessage, replaceMessages, refreshTerminal } = context + const now = Date.now() if (!taskId) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "Usage: /tasks select ", - ts: Date.now(), + ts: now, }) return } try { - const now = Date.now() replaceMessages([ { id: `empty-${now}`, @@ -221,10 +223,10 @@ async function selectTask(context: any, taskId: string): Promise { }) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to switch to task: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -234,13 +236,14 @@ async function selectTask(context: any, taskId: string): Promise { */ async function changePage(context: any, pageNum: string): Promise { const { taskHistoryData, changeTaskHistoryPage, addMessage } = context + const now = Date.now() if (!taskHistoryData) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "No task history loaded. Use /tasks to load history first.", - ts: Date.now(), + ts: now, }) return } @@ -249,19 +252,19 @@ async function changePage(context: any, pageNum: string): Promise { if (isNaN(pageIndex) || pageIndex < 0 || pageIndex >= taskHistoryData.pageCount) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: `Invalid page number. Must be between 1 and ${taskHistoryData.pageCount}.`, - ts: Date.now(), + ts: now, }) return } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: `Loading page ${pageIndex + 1}...`, - ts: Date.now(), + ts: now, }) try { @@ -271,10 +274,10 @@ async function changePage(context: any, pageNum: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to load page: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -284,10 +287,11 @@ async function changePage(context: any, pageNum: string): Promise { */ async function nextPage(context: any): Promise { const { taskHistoryData, nextTaskHistoryPage, addMessage } = context + const now = Date.now() if (!taskHistoryData) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "No task history loaded. Use /tasks to load history first.", ts: Date.now(), @@ -297,7 +301,7 @@ async function nextPage(context: any): Promise { if (taskHistoryData.pageIndex >= taskHistoryData.pageCount - 1) { addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: "Already on the last page.", ts: Date.now(), @@ -306,10 +310,10 @@ async function nextPage(context: any): Promise { } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: "Loading next page...", - ts: Date.now(), + ts: now, }) try { @@ -319,10 +323,10 @@ async function nextPage(context: any): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to load next page: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -332,32 +336,33 @@ async function nextPage(context: any): Promise { */ async function previousPage(context: any): Promise { const { taskHistoryData, previousTaskHistoryPage, addMessage } = context + const now = Date.now() if (!taskHistoryData) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "No task history loaded. Use /tasks to load history first.", - ts: Date.now(), + ts: now, }) return } if (taskHistoryData.pageIndex <= 0) { addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: "Already on the first page.", - ts: Date.now(), + ts: now, }) return } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: "Loading previous page...", - ts: Date.now(), + ts: now, }) try { @@ -367,10 +372,10 @@ async function previousPage(context: any): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to load previous page: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -380,22 +385,23 @@ async function previousPage(context: any): Promise { */ async function changeSortOrder(context: any, sortOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context + const now = Date.now() const validSorts = Object.keys(SORT_OPTION_MAP) const mappedSort = SORT_OPTION_MAP[sortOption] if (!mappedSort) { addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: `Invalid sort option. Valid options: ${validSorts.join(", ")}`, - ts: Date.now(), + ts: now, }) return } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: `Sorting by ${sortOption}...`, ts: Date.now(), @@ -408,10 +414,10 @@ async function changeSortOrder(context: any, sortOption: string): Promise await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to change sort order: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } @@ -421,6 +427,7 @@ async function changeSortOrder(context: any, sortOption: string): Promise */ async function changeFilter(context: any, filterOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context + const now = Date.now() let filterUpdate: any let loadingMessage: string @@ -448,16 +455,16 @@ async function changeFilter(context: any, filterOption: string): Promise { default: addMessage({ - id: Date.now().toString(), + id: `error-validation-${now}`, type: "error", content: "Invalid filter option. Valid options: current, all, favorites, all-tasks", - ts: Date.now(), + ts: now, }) return } addMessage({ - id: Date.now().toString(), + id: `system-${now}`, type: "system", content: loadingMessage, ts: Date.now(), @@ -470,10 +477,10 @@ async function changeFilter(context: any, filterOption: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: Date.now().toString(), + id: `error-fetch-${now}`, type: "error", content: `Failed to change filter: ${error instanceof Error ? error.message : String(error)}`, - ts: Date.now(), + ts: now, }) } } diff --git a/cli/src/state/atoms/effects.ts b/cli/src/state/atoms/effects.ts index 1aa509cf2f1..249d4c82966 100644 --- a/cli/src/state/atoms/effects.ts +++ b/cli/src/state/atoms/effects.ts @@ -183,13 +183,9 @@ export const messageHandlerEffectAtom = atom(null, (get, set, message: Extension // Resolve any pending request with this requestId if (requestId) { - console.log("[taskHistoryResponse] Resolving request:", requestId) set(resolveTaskHistoryRequestAtom, { requestId, data }) - } else { - console.warn("[taskHistoryResponse] No requestId in response!") } } else { - console.error("[taskHistoryResponse] No payload in response") set(taskHistoryErrorAtom, "Failed to fetch task history") // Reject any pending requests if (message.payload?.requestId) { From b43479a77197c59b15e44565ab21305465e0b03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 18:19:52 -0300 Subject: [PATCH 6/9] refactor: util to prevent duplicated messages --- cli/src/commands/tasks.ts | 96 ++++++++++++------------------------ cli/src/ui/utils/messages.ts | 8 +++ 2 files changed, 39 insertions(+), 65 deletions(-) create mode 100644 cli/src/ui/utils/messages.ts diff --git a/cli/src/commands/tasks.ts b/cli/src/commands/tasks.ts index da0ada816df..cc0c223ce01 100644 --- a/cli/src/commands/tasks.ts +++ b/cli/src/commands/tasks.ts @@ -2,6 +2,7 @@ * /tasks command - View and manage task history */ +import { generateMessage } from "../ui/utils/messages.js" import type { Command, ArgumentProviderContext } from "./core/types.js" import type { HistoryItem } from "@roo-code/types" @@ -68,7 +69,6 @@ function truncate(text: string, maxLength: number): string { */ async function showTaskHistory(context: any, dataOverride?: any): Promise { const { taskHistoryData, taskHistoryLoading, taskHistoryError, fetchTaskHistory, addMessage } = context - const now = Date.now() // Use override data if provided, otherwise use context data const data = dataOverride || taskHistoryData @@ -76,10 +76,9 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise // If loading, show loading message if (taskHistoryLoading && !dataOverride) { addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: "Loading task history...", - ts: now, }) return } @@ -87,10 +86,9 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise // If error, show error message if (taskHistoryError && !dataOverride) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to load task history: ${taskHistoryError}`, - ts: now, }) return } @@ -99,10 +97,9 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise if (!data) { await fetchTaskHistory() addMessage({ - id: `system-no-data-${now}`, + ...generateMessage(), type: "system", content: "Loading task history...", - ts: now, }) return } @@ -111,10 +108,9 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise if (historyItems.length === 0) { addMessage({ - id: `system-no-tasks-${now}`, + ...generateMessage(), type: "system", content: "No tasks found in history.", - ts: now, }) return } @@ -136,10 +132,9 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise }) addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content, - ts: now, }) } @@ -148,23 +143,20 @@ async function showTaskHistory(context: any, dataOverride?: any): Promise */ async function searchTasks(context: any, query: string): Promise { const { updateTaskHistoryFilters, addMessage } = context - const now = Date.now() if (!query) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "Usage: /tasks search ", - ts: now, }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: `Searching for "${query}"...`, - ts: now, }) try { @@ -174,10 +166,9 @@ async function searchTasks(context: any, query: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to search tasks: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -187,19 +178,18 @@ async function searchTasks(context: any, query: string): Promise { */ async function selectTask(context: any, taskId: string): Promise { const { sendWebviewMessage, addMessage, replaceMessages, refreshTerminal } = context - const now = Date.now() if (!taskId) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "Usage: /tasks select ", - ts: now, }) return } try { + const now = Date.now() replaceMessages([ { id: `empty-${now}`, @@ -223,10 +213,9 @@ async function selectTask(context: any, taskId: string): Promise { }) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to switch to task: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -236,14 +225,12 @@ async function selectTask(context: any, taskId: string): Promise { */ async function changePage(context: any, pageNum: string): Promise { const { taskHistoryData, changeTaskHistoryPage, addMessage } = context - const now = Date.now() if (!taskHistoryData) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "No task history loaded. Use /tasks to load history first.", - ts: now, }) return } @@ -252,19 +239,17 @@ async function changePage(context: any, pageNum: string): Promise { if (isNaN(pageIndex) || pageIndex < 0 || pageIndex >= taskHistoryData.pageCount) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: `Invalid page number. Must be between 1 and ${taskHistoryData.pageCount}.`, - ts: now, }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: `Loading page ${pageIndex + 1}...`, - ts: now, }) try { @@ -274,10 +259,9 @@ async function changePage(context: any, pageNum: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to load page: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -291,29 +275,26 @@ async function nextPage(context: any): Promise { if (!taskHistoryData) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "No task history loaded. Use /tasks to load history first.", - ts: Date.now(), }) return } if (taskHistoryData.pageIndex >= taskHistoryData.pageCount - 1) { addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: "Already on the last page.", - ts: Date.now(), }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: "Loading next page...", - ts: now, }) try { @@ -323,10 +304,9 @@ async function nextPage(context: any): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to load next page: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -336,33 +316,29 @@ async function nextPage(context: any): Promise { */ async function previousPage(context: any): Promise { const { taskHistoryData, previousTaskHistoryPage, addMessage } = context - const now = Date.now() if (!taskHistoryData) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "No task history loaded. Use /tasks to load history first.", - ts: now, }) return } if (taskHistoryData.pageIndex <= 0) { addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: "Already on the first page.", - ts: now, }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: "Loading previous page...", - ts: now, }) try { @@ -372,10 +348,9 @@ async function previousPage(context: any): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to load previous page: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -385,26 +360,23 @@ async function previousPage(context: any): Promise { */ async function changeSortOrder(context: any, sortOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context - const now = Date.now() const validSorts = Object.keys(SORT_OPTION_MAP) const mappedSort = SORT_OPTION_MAP[sortOption] if (!mappedSort) { addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: `Invalid sort option. Valid options: ${validSorts.join(", ")}`, - ts: now, }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: `Sorting by ${sortOption}...`, - ts: Date.now(), }) try { @@ -414,10 +386,9 @@ async function changeSortOrder(context: any, sortOption: string): Promise await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to change sort order: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -427,7 +398,6 @@ async function changeSortOrder(context: any, sortOption: string): Promise */ async function changeFilter(context: any, filterOption: string): Promise { const { updateTaskHistoryFilters, addMessage } = context - const now = Date.now() let filterUpdate: any let loadingMessage: string @@ -455,19 +425,17 @@ async function changeFilter(context: any, filterOption: string): Promise { default: addMessage({ - id: `error-validation-${now}`, + ...generateMessage(), type: "error", content: "Invalid filter option. Valid options: current, all, favorites, all-tasks", - ts: now, }) return } addMessage({ - id: `system-${now}`, + ...generateMessage(), type: "system", content: loadingMessage, - ts: Date.now(), }) try { @@ -477,10 +445,9 @@ async function changeFilter(context: any, filterOption: string): Promise { await showTaskHistory(context, newData) } catch (error) { addMessage({ - id: `error-fetch-${now}`, + ...generateMessage(), type: "error", content: `Failed to change filter: ${error instanceof Error ? error.message : String(error)}`, - ts: now, }) } } @@ -632,10 +599,9 @@ export const tasksCommand: Command = { default: context.addMessage({ - id: Date.now().toString(), + ...generateMessage(), type: "error", content: `Unknown subcommand "${subcommand}". Available: search, select, page, next, prev, sort, filter`, - ts: Date.now(), }) } }, diff --git a/cli/src/ui/utils/messages.ts b/cli/src/ui/utils/messages.ts new file mode 100644 index 00000000000..ccd477c92dd --- /dev/null +++ b/cli/src/ui/utils/messages.ts @@ -0,0 +1,8 @@ +export const generateMessage = () => { + const now = Date.now() + const uniqueSuffix = Math.floor(Math.random() * 10000) + return { + id: `msg-${now}-${uniqueSuffix}`, + ts: now, + } +} From cb1cb31579510995d7fa3d34c9acdae802779320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 18:22:38 -0300 Subject: [PATCH 7/9] Update cli/src/ui/messages/MessageDisplay.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cli/src/ui/messages/MessageDisplay.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/src/ui/messages/MessageDisplay.tsx b/cli/src/ui/messages/MessageDisplay.tsx index 39ca76c68dc..0bdd30b4faf 100644 --- a/cli/src/ui/messages/MessageDisplay.tsx +++ b/cli/src/ui/messages/MessageDisplay.tsx @@ -42,7 +42,6 @@ import { Box, Static } from "ink" import { useAtomValue } from "jotai" import { type UnifiedMessage, staticMessagesAtom, dynamicMessagesAtom } from "../../state/atoms/ui.js" import { MessageRow } from "./MessageRow.js" -import { logs } from "../../services/logs.js" interface MessageDisplayProps { /** Optional filter to show only specific message types */ From 56e298de8a7a2e651eb6c638368ed59653b286f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 18:26:24 -0300 Subject: [PATCH 8/9] refactor: copilot suggestion --- cli/src/state/hooks/useTerminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/state/hooks/useTerminal.ts b/cli/src/state/hooks/useTerminal.ts index 0014343966b..ce24e270da6 100644 --- a/cli/src/state/hooks/useTerminal.ts +++ b/cli/src/state/hooks/useTerminal.ts @@ -16,7 +16,7 @@ export function useTerminal(): void { process.stdout.write("\x1b[2J\x1b[3J\x1b[H") // Increment the message reset counter to force re-render of Static component incrementResetCounter((prev) => prev + 1) - }, []) + }, [incrementResetCounter]) // Clear terminal when reset counter changes useEffect(() => { From 7998d7b47f95257d4ac1e4d430283c9f06a3a9c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Catriel=20M=C3=BCller?= Date: Fri, 24 Oct 2025 18:42:22 -0300 Subject: [PATCH 9/9] fix: fix automatically load a task --- cli/src/state/hooks/useTerminal.ts | 4 +--- cli/src/ui/utils/welcomeMessage.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cli/src/state/hooks/useTerminal.ts b/cli/src/state/hooks/useTerminal.ts index ce24e270da6..bac749334b9 100644 --- a/cli/src/state/hooks/useTerminal.ts +++ b/cli/src/state/hooks/useTerminal.ts @@ -20,9 +20,7 @@ export function useTerminal(): void { // Clear terminal when reset counter changes useEffect(() => { - if (refreshTerminalCounter !== 0) { - clearTerminal() - } + clearTerminal() }, [refreshTerminalCounter, clearTerminal]) // Resize effect diff --git a/cli/src/ui/utils/welcomeMessage.ts b/cli/src/ui/utils/welcomeMessage.ts index 8e01958546a..5d509b84e18 100644 --- a/cli/src/ui/utils/welcomeMessage.ts +++ b/cli/src/ui/utils/welcomeMessage.ts @@ -49,7 +49,7 @@ export function createWelcomeMessage(options?: WelcomeMessageOptions): CliMessag id, type: "welcome", content: "", // Content is rendered by WelcomeMessageContent component - ts: timestamp, + ts: 0, // Welcome message should show at the top metadata: { welcomeOptions: options, },