mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: implement hybrid conversation history system with workspace hashes
- Add workspace hash functionality using VS Code stable identifiers - Update HistoryItem schema to include optional workspaceHash field - Implement automatic migration for existing path-based history - Add hybrid filtering with hash-based primary matching and path fallback - Include path search functionality with "path:" prefix - Add comprehensive test coverage for all new functionality - Maintain backward compatibility with existing workspace paths Fixes #6398
This commit is contained in:
parent
e654ced135
commit
746ceee20a
11 changed files with 546 additions and 16 deletions
|
|
@ -16,6 +16,7 @@ export const historyItemSchema = z.object({
|
|||
totalCost: z.number(),
|
||||
size: z.number().optional(),
|
||||
workspace: z.string().optional(),
|
||||
workspaceHash: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { combineCommandSequences } from "../../shared/combineCommandSequences"
|
|||
import { getApiMetrics } from "../../shared/getApiMetrics"
|
||||
import { findLastIndex } from "../../shared/array"
|
||||
import { getTaskDirectoryPath } from "../../utils/storage"
|
||||
import { getWorkspaceHash } from "../../utils/workspaceHash"
|
||||
import { t } from "../../i18n"
|
||||
|
||||
const taskSizeCache = new NodeCache({ stdTTL: 30, checkperiod: 5 * 60 })
|
||||
|
|
@ -18,6 +19,7 @@ export type TaskMetadataOptions = {
|
|||
taskNumber: number
|
||||
globalStoragePath: string
|
||||
workspace: string
|
||||
workspaceHash?: string
|
||||
mode?: string
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +29,7 @@ export async function taskMetadata({
|
|||
taskNumber,
|
||||
globalStoragePath,
|
||||
workspace,
|
||||
workspaceHash,
|
||||
mode,
|
||||
}: TaskMetadataOptions) {
|
||||
const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
|
||||
|
|
@ -79,6 +82,9 @@ export async function taskMetadata({
|
|||
}
|
||||
}
|
||||
|
||||
// Generate workspace hash if not provided, convert null to undefined
|
||||
const finalWorkspaceHash = workspaceHash || getWorkspaceHash() || undefined
|
||||
|
||||
// Create historyItem once with pre-calculated values
|
||||
const historyItem: HistoryItem = {
|
||||
id: taskId,
|
||||
|
|
@ -94,6 +100,7 @@ export async function taskMetadata({
|
|||
totalCost: tokenUsage.totalCost,
|
||||
size: taskDirSize,
|
||||
workspace,
|
||||
workspaceHash: finalWorkspaceHash,
|
||||
mode,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
|||
// utils
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { getWorkspaceHash } from "../../utils/workspaceHash"
|
||||
|
||||
// prompts
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ import { getNonce } from "./getNonce"
|
|||
import { getUri } from "./getUri"
|
||||
import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { migrateHistoryToWorkspaceHash, isMigrationNeeded } from "../../utils/historyMigration"
|
||||
import { webviewMessageHandler } from "./webviewMessageHandler"
|
||||
import { WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
|
||||
|
|
@ -1826,6 +1827,24 @@ export class ClineProvider
|
|||
return history
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates existing task history to include workspace hashes if needed
|
||||
*/
|
||||
async migrateHistoryIfNeeded(): Promise<void> {
|
||||
try {
|
||||
const taskHistory = this.getGlobalState("taskHistory") ?? []
|
||||
|
||||
if (isMigrationNeeded(taskHistory)) {
|
||||
this.log("Migrating task history to include workspace hashes...")
|
||||
const migratedHistory = migrateHistoryToWorkspaceHash(taskHistory)
|
||||
await this.updateGlobalState("taskHistory", migratedHistory)
|
||||
this.log(`Successfully migrated ${migratedHistory.length} history items`)
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Error during history migration: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ContextProxy
|
||||
|
||||
// @deprecated - Use `ContextProxy#setValue` instead.
|
||||
|
|
|
|||
231
src/utils/__tests__/historyMigration.spec.ts
Normal file
231
src/utils/__tests__/historyMigration.spec.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
import {
|
||||
migrateHistoryToWorkspaceHash,
|
||||
isMigrationNeeded,
|
||||
findOrphanedHistory,
|
||||
relinkHistoryItem,
|
||||
} from "../historyMigration"
|
||||
import * as workspaceHashModule from "../workspaceHash"
|
||||
import * as pathModule from "../path"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock workspaceHash module
|
||||
vi.mock("../workspaceHash", () => ({
|
||||
getWorkspaceHash: vi.fn(),
|
||||
areWorkspaceHashesEqual: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock path module
|
||||
vi.mock("../path", () => ({
|
||||
arePathsEqual: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("historyMigration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("migrateHistoryToWorkspaceHash", () => {
|
||||
it("should skip items that already have workspace hash", () => {
|
||||
vi.mocked(workspaceHashModule.getWorkspaceHash).mockReturnValue("current-hash")
|
||||
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/test/workspace",
|
||||
workspaceHash: "existing-hash",
|
||||
},
|
||||
]
|
||||
|
||||
const result = migrateHistoryToWorkspaceHash(historyItems)
|
||||
expect(result[0].workspaceHash).toBe("existing-hash")
|
||||
})
|
||||
|
||||
it("should add workspace hash for items matching current workspace", () => {
|
||||
vi.mocked(workspaceHashModule.getWorkspaceHash).mockReturnValue("current-hash")
|
||||
vi.mocked(pathModule.arePathsEqual).mockReturnValue(true)
|
||||
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/test/workspace",
|
||||
},
|
||||
]
|
||||
|
||||
const result = migrateHistoryToWorkspaceHash(historyItems)
|
||||
expect(result[0].workspaceHash).toBe("current-hash")
|
||||
})
|
||||
|
||||
it("should not add workspace hash for items from different workspace", () => {
|
||||
vi.mocked(workspaceHashModule.getWorkspaceHash).mockReturnValue("current-hash")
|
||||
vi.mocked(pathModule.arePathsEqual).mockReturnValue(false)
|
||||
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/different/workspace",
|
||||
},
|
||||
]
|
||||
|
||||
const result = migrateHistoryToWorkspaceHash(historyItems)
|
||||
expect(result[0].workspaceHash).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("isMigrationNeeded", () => {
|
||||
it("should return true when items without workspace hash exist", () => {
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/test/workspace",
|
||||
},
|
||||
]
|
||||
|
||||
expect(isMigrationNeeded(historyItems)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when all items have workspace hash", () => {
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/test/workspace",
|
||||
workspaceHash: "hash",
|
||||
},
|
||||
]
|
||||
|
||||
expect(isMigrationNeeded(historyItems)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for empty history", () => {
|
||||
expect(isMigrationNeeded([])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("findOrphanedHistory", () => {
|
||||
it("should find items with different workspace hash", () => {
|
||||
vi.mocked(workspaceHashModule.getWorkspaceHash).mockReturnValue("current-hash")
|
||||
vi.mocked(workspaceHashModule.areWorkspaceHashesEqual).mockReturnValue(false)
|
||||
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/test/workspace",
|
||||
workspaceHash: "different-hash",
|
||||
},
|
||||
]
|
||||
|
||||
const result = findOrphanedHistory(historyItems)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("1")
|
||||
})
|
||||
|
||||
it("should find items with different workspace path", () => {
|
||||
vi.mocked(workspaceHashModule.getWorkspaceHash).mockReturnValue("current-hash")
|
||||
vi.mocked(pathModule.arePathsEqual).mockReturnValue(false)
|
||||
;(vscode.workspace as any).workspaceFolders = [{ uri: { fsPath: "/test/workspace" } }]
|
||||
|
||||
const historyItems: HistoryItem[] = [
|
||||
{
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/different/workspace",
|
||||
},
|
||||
]
|
||||
|
||||
const result = findOrphanedHistory(historyItems)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("relinkHistoryItem", () => {
|
||||
it("should update workspace information", () => {
|
||||
const item: HistoryItem = {
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/old/workspace",
|
||||
workspaceHash: "old-hash",
|
||||
}
|
||||
|
||||
const result = relinkHistoryItem(item, "/new/workspace", "new-hash")
|
||||
|
||||
expect(result.workspace).toBe("/new/workspace")
|
||||
expect(result.workspaceHash).toBe("new-hash")
|
||||
expect(result.id).toBe("1") // Other properties preserved
|
||||
})
|
||||
|
||||
it("should handle null workspace hash", () => {
|
||||
const item: HistoryItem = {
|
||||
id: "1",
|
||||
number: 1,
|
||||
ts: Date.now(),
|
||||
task: "Test task",
|
||||
tokensIn: 100,
|
||||
tokensOut: 50,
|
||||
totalCost: 0.01,
|
||||
workspace: "/old/workspace",
|
||||
}
|
||||
|
||||
const result = relinkHistoryItem(item, "/new/workspace", null)
|
||||
|
||||
expect(result.workspace).toBe("/new/workspace")
|
||||
expect(result.workspaceHash).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
121
src/utils/__tests__/workspaceHash.spec.ts
Normal file
121
src/utils/__tests__/workspaceHash.spec.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import { getWorkspaceHash, getWorkspaceStoragePath, areWorkspaceHashesEqual } from "../workspaceHash"
|
||||
|
||||
// Mock vscode
|
||||
vi.mock("vscode", () => ({
|
||||
workspace: {
|
||||
workspaceFolders: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
describe("workspaceHash", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("getWorkspaceHash", () => {
|
||||
it("should return null when no workspace folders exist", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = undefined
|
||||
expect(getWorkspaceHash()).toBeNull()
|
||||
})
|
||||
|
||||
it("should return null when workspace folders array is empty", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = []
|
||||
expect(getWorkspaceHash()).toBeNull()
|
||||
})
|
||||
|
||||
it("should return a hash when workspace folder exists", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = [
|
||||
{
|
||||
uri: {
|
||||
toString: () => "file:///test/workspace",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const hash = getWorkspaceHash()
|
||||
expect(hash).toBeTruthy()
|
||||
expect(typeof hash).toBe("string")
|
||||
expect(hash).toHaveLength(40) // SHA1 hash length
|
||||
})
|
||||
|
||||
it("should return consistent hash for same workspace URI", () => {
|
||||
const mockUri = "file:///test/workspace"
|
||||
;(vscode.workspace as any).workspaceFolders = [
|
||||
{
|
||||
uri: {
|
||||
toString: () => mockUri,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const hash1 = getWorkspaceHash()
|
||||
const hash2 = getWorkspaceHash()
|
||||
expect(hash1).toBe(hash2)
|
||||
})
|
||||
|
||||
it("should return different hashes for different workspace URIs", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = [
|
||||
{
|
||||
uri: {
|
||||
toString: () => "file:///test/workspace1",
|
||||
},
|
||||
},
|
||||
]
|
||||
const hash1 = getWorkspaceHash()
|
||||
|
||||
;(vscode.workspace as any).workspaceFolders = [
|
||||
{
|
||||
uri: {
|
||||
toString: () => "file:///test/workspace2",
|
||||
},
|
||||
},
|
||||
]
|
||||
const hash2 = getWorkspaceHash()
|
||||
|
||||
expect(hash1).not.toBe(hash2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getWorkspaceStoragePath", () => {
|
||||
it("should return null when no workspace hash is available", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = undefined
|
||||
expect(getWorkspaceStoragePath()).toBeNull()
|
||||
})
|
||||
|
||||
it("should return the hash when workspace is available", () => {
|
||||
;(vscode.workspace as any).workspaceFolders = [
|
||||
{
|
||||
uri: {
|
||||
toString: () => "file:///test/workspace",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const storagePath = getWorkspaceStoragePath()
|
||||
const hash = getWorkspaceHash()
|
||||
expect(storagePath).toBe(hash)
|
||||
})
|
||||
})
|
||||
|
||||
describe("areWorkspaceHashesEqual", () => {
|
||||
it("should return true when both hashes are null", () => {
|
||||
expect(areWorkspaceHashesEqual(null, null)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when one hash is null and the other is not", () => {
|
||||
expect(areWorkspaceHashesEqual(null, "hash")).toBe(false)
|
||||
expect(areWorkspaceHashesEqual("hash", null)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true when hashes are identical", () => {
|
||||
const hash = "abc123def456"
|
||||
expect(areWorkspaceHashesEqual(hash, hash)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false when hashes are different", () => {
|
||||
expect(areWorkspaceHashesEqual("hash1", "hash2")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
90
src/utils/historyMigration.ts
Normal file
90
src/utils/historyMigration.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import * as vscode from "vscode"
|
||||
import type { HistoryItem } from "@roo-code/types"
|
||||
import { getWorkspaceHash, areWorkspaceHashesEqual } from "./workspaceHash"
|
||||
import { arePathsEqual } from "./path"
|
||||
|
||||
/**
|
||||
* Migrates existing path-based history items to include workspace hashes
|
||||
* @param taskHistory Array of history items to migrate
|
||||
* @returns Migrated history items with workspace hashes added
|
||||
*/
|
||||
export function migrateHistoryToWorkspaceHash(taskHistory: HistoryItem[]): HistoryItem[] {
|
||||
const currentWorkspaceHash = getWorkspaceHash()
|
||||
const currentWorkspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
|
||||
return taskHistory.map((item) => {
|
||||
// Skip items that already have a workspace hash
|
||||
if (item.workspaceHash) {
|
||||
return item
|
||||
}
|
||||
|
||||
// Try to determine the workspace hash for this item
|
||||
let workspaceHash: string | undefined
|
||||
|
||||
// If the item's workspace path matches the current workspace path,
|
||||
// use the current workspace hash
|
||||
if (currentWorkspacePath && item.workspace && arePathsEqual(item.workspace, currentWorkspacePath)) {
|
||||
workspaceHash = currentWorkspaceHash || undefined
|
||||
}
|
||||
|
||||
// Return the item with the workspace hash added (if determined)
|
||||
return {
|
||||
...item,
|
||||
workspaceHash,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if migration is needed for the given task history
|
||||
* @param taskHistory Array of history items to check
|
||||
* @returns True if migration is needed, false otherwise
|
||||
*/
|
||||
export function isMigrationNeeded(taskHistory: HistoryItem[]): boolean {
|
||||
// Migration is needed if there are items without workspace hashes
|
||||
return taskHistory.some((item) => !item.workspaceHash && item.workspace)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds orphaned history items that don't match the current workspace
|
||||
* @param taskHistory Array of history items to check
|
||||
* @returns Array of orphaned history items
|
||||
*/
|
||||
export function findOrphanedHistory(taskHistory: HistoryItem[]): HistoryItem[] {
|
||||
const currentWorkspaceHash = getWorkspaceHash()
|
||||
const currentWorkspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
|
||||
return taskHistory.filter((item) => {
|
||||
// If item has a workspace hash, check if it matches current workspace
|
||||
if (item.workspaceHash) {
|
||||
return !areWorkspaceHashesEqual(item.workspaceHash, currentWorkspaceHash)
|
||||
}
|
||||
|
||||
// If item only has workspace path, check if it matches current workspace path
|
||||
if (item.workspace && currentWorkspacePath) {
|
||||
return !arePathsEqual(item.workspace, currentWorkspacePath)
|
||||
}
|
||||
|
||||
// If no workspace info, consider it orphaned
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a history item's workspace information for re-linking
|
||||
* @param item History item to update
|
||||
* @param newWorkspacePath New workspace path
|
||||
* @param newWorkspaceHash New workspace hash
|
||||
* @returns Updated history item
|
||||
*/
|
||||
export function relinkHistoryItem(
|
||||
item: HistoryItem,
|
||||
newWorkspacePath: string,
|
||||
newWorkspaceHash: string | null,
|
||||
): HistoryItem {
|
||||
return {
|
||||
...item,
|
||||
workspace: newWorkspacePath,
|
||||
workspaceHash: newWorkspaceHash || undefined,
|
||||
}
|
||||
}
|
||||
35
src/utils/workspaceHash.ts
Normal file
35
src/utils/workspaceHash.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
/**
|
||||
* Generates a stable workspace hash based on the workspace URI
|
||||
* This hash is the same one VS Code uses to manage its own workspace storage
|
||||
* @returns The workspace hash or null if no workspace is available
|
||||
*/
|
||||
export function getWorkspaceHash(): string | null {
|
||||
const folderUri = vscode.workspace.workspaceFolders?.[0]?.uri.toString()
|
||||
if (!folderUri) return null
|
||||
return crypto.createHash("sha1").update(folderUri).digest("hex")
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a workspace storage path using the stable workspace hash
|
||||
* @returns The hash-based storage path or null if no workspace is available
|
||||
*/
|
||||
export function getWorkspaceStoragePath(): string | null {
|
||||
const hash = getWorkspaceHash()
|
||||
if (!hash) return null
|
||||
return hash
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two workspace hashes represent the same workspace
|
||||
* @param hash1 First workspace hash
|
||||
* @param hash2 Second workspace hash
|
||||
* @returns True if the hashes match (both null counts as a match)
|
||||
*/
|
||||
export function areWorkspaceHashesEqual(hash1: string | null, hash2: string | null): boolean {
|
||||
if (hash1 === null && hash2 === null) return true
|
||||
if (hash1 === null || hash2 === null) return false
|
||||
return hash1 === hash2
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
|
|||
<div className="flex flex-col gap-2">
|
||||
<VSCodeTextField
|
||||
className="w-full"
|
||||
placeholder={t("history:searchPlaceholder")}
|
||||
placeholder="Search history or use 'path:' to filter by workspace"
|
||||
value={searchQuery}
|
||||
data-testid="history-search-input"
|
||||
onInput={(e) => {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe("HistoryView", () => {
|
|||
// Check for main UI elements
|
||||
expect(screen.getByText("history:history")).toBeInTheDocument()
|
||||
expect(screen.getByText("history:done")).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText("history:searchPlaceholder")).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText("Search history or use 'path:' to filter by workspace")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("calls onDone when done button is clicked", () => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,23 @@ import { Fzf } from "fzf"
|
|||
import { highlightFzfMatch } from "@/utils/highlight"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
// Helper function to get workspace hash from current workspace
|
||||
const getCurrentWorkspaceHash = (): string | null => {
|
||||
// This will be populated by the extension state
|
||||
return null // Placeholder - will be updated when we add workspace hash to extension state
|
||||
}
|
||||
|
||||
// Helper function to check if a task belongs to the current workspace
|
||||
const isTaskInCurrentWorkspace = (item: any, cwd: string | undefined, currentWorkspaceHash: string | null): boolean => {
|
||||
// Primary method: Use workspace hash if available for both current workspace and task
|
||||
if (currentWorkspaceHash && item.workspaceHash) {
|
||||
return item.workspaceHash === currentWorkspaceHash
|
||||
}
|
||||
|
||||
// Fallback method: Use path-based matching for legacy items or when hash is unavailable
|
||||
return cwd ? item.workspace === cwd : false
|
||||
}
|
||||
|
||||
type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
|
||||
|
||||
export const useTaskSearch = () => {
|
||||
|
|
@ -26,7 +43,8 @@ export const useTaskSearch = () => {
|
|||
const presentableTasks = useMemo(() => {
|
||||
let tasks = taskHistory.filter((item) => item.ts && item.task)
|
||||
if (!showAllWorkspaces) {
|
||||
tasks = tasks.filter((item) => item.workspace === cwd)
|
||||
const currentWorkspaceHash = getCurrentWorkspaceHash()
|
||||
tasks = tasks.filter((item) => isTaskInCurrentWorkspace(item, cwd, currentWorkspaceHash))
|
||||
}
|
||||
return tasks
|
||||
}, [taskHistory, showAllWorkspaces, cwd])
|
||||
|
|
@ -41,20 +59,27 @@ export const useTaskSearch = () => {
|
|||
let results = presentableTasks
|
||||
|
||||
if (searchQuery) {
|
||||
const searchResults = fzf.find(searchQuery)
|
||||
results = searchResults.map((result) => {
|
||||
const positions = Array.from(result.positions)
|
||||
const taskEndIndex = result.item.task.length
|
||||
// Check if this is a path search (prefixed with "path:")
|
||||
if (searchQuery.startsWith("path:")) {
|
||||
const pathQuery = searchQuery.substring(5).trim()
|
||||
results = presentableTasks.filter((item) => item.workspace && item.workspace.includes(pathQuery))
|
||||
} else {
|
||||
// Regular fuzzy search
|
||||
const searchResults = fzf.find(searchQuery)
|
||||
results = searchResults.map((result) => {
|
||||
const positions = Array.from(result.positions)
|
||||
const taskEndIndex = result.item.task.length
|
||||
|
||||
return {
|
||||
...result.item,
|
||||
highlight: highlightFzfMatch(
|
||||
result.item.task,
|
||||
positions.filter((p) => p < taskEndIndex),
|
||||
),
|
||||
workspace: result.item.workspace,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...result.item,
|
||||
highlight: highlightFzfMatch(
|
||||
result.item.task,
|
||||
positions.filter((p) => p < taskEndIndex),
|
||||
),
|
||||
workspace: result.item.workspace,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Then sort the results
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue