Thread header
The bar above a chat: title and model with a share and options menu, a project breadcrumb with who can see it, a status bar for an agent working on a task, and a header that tucks away as you read.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/thread-header-default.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/thread-header-default.json
Usage & API
Example
import { ThreadHeader } from "@/components/ai/thread-header-default"
<ThreadHeader
title="…"
onTitleChange={() => {}}
models={[]}
model="…"
onModelChange={() => {}}
projects={[]}
project={null}
onProjectChange={() => {}}
pinned={true}
onAction={() => {}}
onShare={() => {}}
sidebarOpen={true}
onSidebarToggle={() => {}}
/>Props ThreadHeader
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
onTitleChange | (title: string) => void | Required |
models | ThreadModel[] | Required |
model | string | Required |
onModelChange | (id: string) => void | Required |
projects | ThreadProject[] | Required |
project | string | null | Required |
onProjectChange | (id: string | null) => void | Required |
pinned | boolean | Required |
archived | boolean | Optional |
onAction | (action: ThreadAction) => void | Required |
onShare | () => void | Required |
sidebarOpen | boolean | Required |
onSidebarToggle | () => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Header bar exports ThreadHeader. Props are title, onTitleChange, models, model, onModelChange, projects, project, onProjectChange, pinned, archived, onAction, onShare, sidebarOpen, and onSidebarToggle. A model has id, name, and note. A project has id, name, and color. The sidebar button reads Open sidebar or Close sidebar. The title becomes an input labelled Chat title. Enter saves a changed non-empty trim. Escape cancels. The model button's accessible name is Model: plus the current name. Share calls onShare, then shows Copied for 1600ms, and its accessible name becomes Link copied. The component does not write the clipboard itself. The options menu includes Rename, Pin or Unpin, Move to project with a nested list and Remove from project, Export as Markdown, Archive or Unarchive, and Delete. Choosing the current project again clears it through onProjectChange(null).
Example
import { ProjectThreadHeader } from "@/components/ai/thread-header-project"
<ProjectThreadHeader
title="…"
projects={[]}
project="…"
onProjectChange={() => {}}
visibility={visibility}
onVisibilityChange={() => {}}
people={[]}
onRoleChange={() => {}}
onRemove={() => {}}
directory={[]}
onInvite={() => {}}
link="…"
workspace="…"
/>Props ProjectThreadHeader
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
projects | Project[] | Required |
project | string | Required |
onProjectChange | (id: string) => void | Required |
visibility | Visibility | Required |
onVisibilityChange | (v: Visibility) => void | Required |
people | Person[] | Required |
onRoleChange | (id: string, role: Role) => void | Required |
onRemove | (id: string) => void | Required |
directory | Omit<Person, "role">[]People who could be invited, used for suggestions. | Required |
onInvite | (person: Omit<Person, "role">) => void | Required |
link | string | Required |
workspace | string | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Project and sharing exports ProjectThreadHeader. Props are title, projects, project, onProjectChange, visibility, onVisibilityChange, people, onRoleChange, onRemove, directory, onInvite, link, and workspace. Visibility is private, team, or public. A person has id, name, email, role of owner, edit, or view, a tone, and optional you and pending flags. The breadcrumb switches project. The visibility button reads Visibility: plus the current label. The share popover lists people, a role menu per person, remove, an invite field fed by directory, and general access. The owner row is marked Owner. Pending people can be shown from the pending flag. link is the string shown in the footer, with the scheme stripped. Copy link writes that string through the clipboard and swaps the label to Copied for 1600ms.
Example
import { AgentThreadHeader } from "@/components/ai/thread-header-agent"
<AgentThreadHeader
title="…"
status={status}
elapsed={1}
progress={1}
repo="…"
branch="…"
files={[]}
pr={pr}
onStop={() => {}}
onResume={() => {}}
onRunAgain={() => {}}
onOpenPr={() => {}}
/>Props AgentThreadHeader
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
status | RunStatus | Required |
elapsed | numberSeconds since the run started. | Required |
progress | number0 to 1. | Required |
repo | string | Required |
branch | string | Required |
files | ChangedFile[] | Required |
pr | PullRequest | Required |
onStop | () => void | Required |
onResume | () => void | Required |
onRunAgain | () => void | Required |
onOpenPr | (draft: boolean) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Agent task exports AgentThreadHeader and formatElapsed. Props are title, status of running, stopped, or done, elapsed in seconds, progress from 0 to 1, repo, branch, files, pr, onStop, onResume, onRunAgain, and onOpenPr. formatElapsed prints minutes and zero-padded seconds, so 252 becomes 4m 12s. The status line is Running, Stopped, or Done, then the clock, then repo and branch, then the summed additions and removals and the file count. A progressbar labels Task progress and sets aria-valuenow to progress times 100. running shows Stop. stopped shows Resume. done shows Run again, Review changes with the file list, and Open PR. pr is a number and a draft flag, or null. onOpenPr receives the draft boolean. The preview starts at 252 seconds already elapsed.
Example
import { ScrollAwareThread } from "@/components/ai/thread-header-floating"
<ScrollAwareThread
title="…"
date="…"
model="…"
messages={1}
pinned={true}
onPinnedChange={() => {}}
onShare={() => {}}
markdown="…"
children={children}
/>Props ScrollAwareThread
| Prop | Type & description | Required |
|---|---|---|
title | string | Required |
date | string | Required |
model | string | Required |
messages | number | Required |
pinned | boolean | Required |
onPinnedChange | (pinned: boolean) => void | Required |
onShare | () => voidCalled when the share link is copied. | Required |
markdown | stringMarkdown for "Copy as Markdown". | Required |
children | ReactNodeThe conversation. Scrolls inside the component. | Required |
footer | ReactNodePinned under the scroll area, e.g. a composer. | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Scroll-aware exports ScrollAwareThread. Props are title, date, model, messages, pinned, onPinnedChange, onShare, markdown, children, and an optional footer. children scroll inside the component. footer is pinned under the scroller, for a composer. The large header shows the title, a pin icon when pinned, the date, the model, and the message count. Between 36px and 104px of scroll, a pill fades and slides in. Past halfway, the large header is marked inert and the pill becomes the control. The pill's title button scrolls to the top. The menu includes Copy as Markdown, which writes the markdown prop, and Back to first message. Pin and share are on both the large header and the pill. Reduced motion skips the slide and the spring on the pill.
Choosing a layout
Use the header bar on a normal chat where the person renames the thread, switches the model for this chat only, and shares or files it. The delete confirm says Delete this chat and its files? You can undo for a few seconds. The action you receive is pin, export, archive, or delete. Use the project variant when the chat lives in a workspace and access is the point: who is invited, whether they can edit or only view, and whether the link is private, for the team, or public. Use the agent variant when the header should say what the run is doing. Use the scroll-aware header when the title should stay available without occupying a row, and the transcript scrolls inside the component.
Do not put run controls on the ordinary header. Stop, Resume, and Open PR exist only on the agent variant, and they call onStop, onResume, and onOpenPr. Do not use the scroll-aware header if the page around it scrolls and the transcript does not. It listens to scroll inside its own scroller, and the pill appears as that value moves from 36px to 104px. The full title fades across the first 84px. A list of chats does not belong in the bar. The sidebar toggle calls onSidebarToggle. The sidebar itself is Thread list.
The agent header says Done but the progress bar is still coloured. Is progress required to be 1?
progress is yours, from 0 to 1, and the bar scales to it. status only picks the colour and the buttons: indigo and Stop while running, amber and Resume while stopped, emerald and the done actions when done. When status is done the bar also fades to about 35 percent opacity after a short hold. The component will not set progress to 1 for you. formatElapsed does not read progress. It only formats elapsed. Review changes lists the files you passed, with each path, added, and removed.
The pill never appears, or it appears immediately. What is it watching?
Scroll inside ScrollAwareThread, not the window. The scroller is the div around children. The pill's progress runs from scroll offset 36 to 104, and the large header's opacity runs from 0 to 84. The switch that marks the header inert happens when that 36-to-104 value passes one half. If children do not overflow, scroll stays at 0 and the pill stays hidden. footer is outside the scroller, so a composer there does not count toward the scroll height. messages is only the number printed in the header. It is not used to decide when to condense.