Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {useNavigate} from 'react-router-dom'
import {toast} from 'sonner'

import {useGetContextNodes} from '../api/get-context-nodes'
import {stalePathMessage} from '../utils/topic-viewer-navigation'
import {findNodeByPath} from '../utils/tree-utils'

export function useNavigateToContextPath() {
const navigate = useNavigate()
const {data} = useGetContextNodes()
const nodes = data?.nodes ?? []

return (path: string) => {
if (findNodeByPath(nodes, path)) {
navigate(`/contexts?path=${encodeURIComponent(path)}`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): This hook always queries the default branch (no branch arg to useGetContextNodes()) and navigates to /contexts?path=… without a branch query param. Two side-effects worth thinking about:

  1. The existence check (findNodeByPath(nodes, path)) runs against the default-branch tree, so a result whose path exists only on the user's currently-selected branch will toast as "stale" even though clicking through would have shown valid content (or vice versa).
  2. If the user had previously switched branches in the Context tab, navigating from a task result silently drops them back to the default branch, since /contexts?path=… is opened without branch.

For the local-only query-tool-mode use case today this is fine, but mirroring the branch-resolution that useContextTree/useTopicViewerNavigation use would make the behavior consistent. Could read searchParams.get('branch') (or read it from a shared store) and forward it to both the fetch and the navigation URL.

} else {
toast.error(stalePathMessage(path))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useMemo} from 'react'
import {toast} from 'sonner'

import {createTopicViewerNavigation} from '../utils/topic-viewer-navigation'
import {createTopicViewerNavigation, stalePathMessage} from '../utils/topic-viewer-navigation'
import {findNodeByPath} from '../utils/tree-utils'
import {useContextTree} from './use-context-tree'

Expand All @@ -12,7 +12,7 @@ export function useTopicViewerNavigation() {
() =>
createTopicViewerNavigation({
navigate: navigateToPath,
onStalePath: (path) => toast.error(`Path not found in context tree: ${path}`),
onStalePath: (path) => toast.error(stalePathMessage(path)),
pathExists: (path) => findNodeByPath(nodes, path) !== undefined,
}),
[navigateToPath, nodes],
Expand Down
2 changes: 2 additions & 0 deletions src/webui/features/context/utils/topic-viewer-navigation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const stalePathMessage = (path: string): string => `Path not found in context tree: ${path}`

interface TopicViewerNavigationDeps {
navigate: (path: string) => void
onStalePath: (path: string) => void
Expand Down
91 changes: 91 additions & 0 deletions src/webui/features/tasks/components/query-results-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {Badge} from '@campfirein/byterover-packages/components/badge'
import {Card} from '@campfirein/byterover-packages/components/card'
import {Tooltip, TooltipContent, TooltipTrigger} from '@campfirein/byterover-packages/components/tooltip'
import {cn} from '@campfirein/byterover-packages/lib/utils'
import {ChevronDown, ChevronUp, FileText} from 'lucide-react'
import {type ComponentRef, useLayoutEffect, useRef, useState} from 'react'

import type {QueryToolModeMatchedDoc} from '../utils/query-tool-mode-results'

import {useNavigateToContextPath} from '../../context/hooks/use-navigate-to-context-path'
import {MarkdownInline} from './markdown-inline'
import {SectionLabel, TerminalDot} from './task-detail-shared'

export function QueryResultsList({matchedDocs}: {matchedDocs: QueryToolModeMatchedDoc[]}) {
const label = `Result · ${matchedDocs.length} ${matchedDocs.length === 1 ? 'match' : 'matches'}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): SectionLabel already accepts a count prop that renders the number on the right side of the divider line (used elsewhere in the file). Could lean on that instead of baking "matches" / "match" into the label string:

<SectionLabel count={matchedDocs.length}>Result</SectionLabel>

If the inline "match/matches" pluralization was intentional (for readability), feel free to ignore.

const openPath = useNavigateToContextPath()

return (
<section className="relative pl-8">
<TerminalDot tone="completed" />
<SectionLabel>{label}</SectionLabel>
{matchedDocs.length === 0 ? (
<p className="text-muted-foreground text-sm">No matching documents.</p>
) : (
<div className="flex flex-col gap-3">
{matchedDocs.map((doc, index) => (
<QueryResultRow doc={doc} key={`${doc.path}-${index}`} onOpenPath={openPath} />
))}
</div>
)}
</section>
)
}

function QueryResultRow({doc, onOpenPath}: {doc: QueryToolModeMatchedDoc; onOpenPath: (path: string) => void}) {
const body = typeof doc.rendered_md === 'string' ? doc.rendered_md : undefined
const bodyRef = useRef<ComponentRef<'div'>>(null)
const [expanded, setExpanded] = useState(false)
const [overflowing, setOverflowing] = useState(false)

useLayoutEffect(() => {
const el = bodyRef.current
if (!el) return
setOverflowing(el.scrollHeight > el.clientHeight + 1)
}, [doc.rendered_md])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): The "Show more/Show less" toggle is only computed when doc.rendered_md changes — it won't re-evaluate if the panel/viewport resizes. A short markdown body that doesn't clip at 800px wide can clip at 500px (or vice versa) and the toggle won't appear/disappear correctly until the next mount.

A ResizeObserver on bodyRef.current (or on the parent panel) would keep overflowing accurate across resizes. Edge case for now, but the Tasks tab is in a resizable layout so it's reachable.


return (
<Card className="ring-border bg-card flex flex-col gap-1.5 p-4" size="sm">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<FileText className="text-muted-foreground size-3.5 shrink-0" />
<span className="text-foreground/90 truncate text-sm font-medium">{doc.title}</span>
</div>
<Tooltip>
<TooltipTrigger
render={
<Badge className="mono text-emerald-400 shrink-0" variant="outline">
{doc.score.toFixed(2)}
</Badge>
}
/>
<TooltipContent>Match score</TooltipContent>
</Tooltip>
</div>
<button
className="text-muted-foreground hover:text-foreground mono w-fit cursor-pointer pl-5 text-left text-[11px] break-all hover:underline"
onClick={() => onOpenPath(doc.path)}
type="button"
>
{doc.path}
</button>
{body && (
<div className="pl-5">
<div className={cn('overflow-hidden', {'max-h-36': !expanded})} ref={bodyRef}>
<MarkdownInline className="text-foreground/70 text-xs">{body}</MarkdownInline>
</div>
{overflowing && (
<button
className="text-muted-foreground hover:text-foreground mt-1.5 inline-flex cursor-pointer items-center gap-1 text-[11px]"
onClick={() => setExpanded((value) => !value)}
type="button"
>
{expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
{expanded ? 'Show less' : 'Show more'}
</button>
)}
</div>
)}
</Card>
)
}
261 changes: 0 additions & 261 deletions src/webui/features/tasks/components/task-detail-event-log.tsx

This file was deleted.

Loading
Loading