Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Upstream: t3code 0.0.31
- CLI versions printed with a leading v are detected (#11738 by NikodemNowak)
- Command palette search matches thread IDs (#11185 by Alex Southwell)
- Command palette jumps straight to the usage page (#12211 by oliver)
- Long branch names and paths truncate in the middle so the useful ends stay visible (#12805 by maria)
- Turn folds reveal their timestamp on hover (#8641 by Alex Southwell)
- Right-click a changed file or browser entry to reveal it or open it in your editor (#11842 by Alex Southwell)
- Undo an archive or snooze from the toast or with Ctrl+Z (#12848 by Julius Marminge)

## 0.0.31-avicode.9.14 (2026-09-09)

Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
MenuTrigger,
} from "./ui/menu";
import { Separator } from "./ui/separator";
import { MiddleTruncate } from "./ui/middle-truncate";

interface BranchToolbarProps {
environmentId: EnvironmentId;
Expand Down Expand Up @@ -188,9 +189,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
) : (
<FolderIcon className="size-3" />
)}
<span className="min-w-0 truncate">
{resolveCurrentWorkspaceLabel(activeWorktreePath)}
</span>
<MiddleTruncate value={resolveCurrentWorkspaceLabel(activeWorktreePath)} />
</span>
</MenuRadioItem>
<MenuRadioItem disabled={envModeLocked} value="worktree">
Expand All @@ -203,7 +202,7 @@ const MobileRunContextSelector = memo(function MobileRunContextSelector({
<MenuRadioItem disabled={envModeLocked} value="previous-worktree">
<span className="flex min-w-0 items-center gap-1.5">
<HistoryIcon className="size-3" />
<span className="min-w-0 truncate">{previousWorktreeLabel}</span>
<MiddleTruncate value={previousWorktreeLabel} />
</span>
</MenuRadioItem>
) : null}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
} from "./ui/combobox";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import { MiddleTruncate } from "./ui/middle-truncate";

interface BranchToolbarBranchSelectorProps {
className?: string;
Expand Down Expand Up @@ -689,7 +690,7 @@ export function BranchToolbarBranchSelector({
onContextMenu={(event) => handleBranchContextMenu(event, itemValue)}
>
<div className="flex w-full min-w-0 items-center justify-between gap-2">
<span className="min-w-0 flex-1 truncate">{itemValue}</span>
<MiddleTruncate value={itemValue} className="flex-1" />
{badge && <span className="shrink-0 text-[10px] text-muted-foreground/45">{badge}</span>}
</div>
</ComboboxItem>
Expand Down Expand Up @@ -750,7 +751,7 @@ export function BranchToolbarBranchSelector({
disabled={isInitialBranchesLoadPending || isBranchActionPending}
>
<GitBranchIcon className="size-3 shrink-0 opacity-70" />
<span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span>
<MiddleTruncate value={triggerLabel} className="max-w-[240px]" />
<ChevronDownIcon className="size-3 shrink-0 opacity-50" />
</ComboboxTrigger>
</span>
Expand Down
31 changes: 26 additions & 5 deletions apps/web/src/components/chat/ChangedFilesTree.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type TurnId } from "@t3tools/contracts";
import { memo, useCallback, useMemo, useState } from "react";
import { type MouseEvent, memo, useCallback, useMemo, useState } from "react";
import { type TurnDiffFileChange } from "../../types";
import {
buildTurnDiffTree,
Expand All @@ -19,6 +19,7 @@ import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel";
import { PierreEntryIcon } from "./PierreEntryIcon";
import { Button } from "../ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { MiddleTruncate } from "../ui/middle-truncate";
import {
changedFileName,
selectChangedFilePreview,
Expand All @@ -27,6 +28,12 @@ import {

const EMPTY_DIRECTORY_OVERRIDES: Record<string, boolean> = {};

/**
* Avi Code addition: opens the OS-level context menu for a changed file
* (reveal in file manager, open in editor). Upstream #11842.
*/
export type ChangedFileContextMenuHandler = (filePath: string, event: MouseEvent) => void;

export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
turnId: TurnId;
files: ReadonlyArray<TurnDiffFileChange>;
Expand All @@ -37,6 +44,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
onExpandedChange: (expanded: boolean) => void;
onToggleAllDirectories: () => void;
onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;
onFileContextMenu?: ChangedFileContextMenuHandler | undefined;
}) {
const {
turnId,
Expand All @@ -48,6 +56,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
onExpandedChange,
onToggleAllDirectories,
onOpenTurnDiff,
onFileContextMenu,
} = props;
const summaryStat = useMemo(() => summarizeTurnDiffStats(files), [files]);
const scopeSummary = useMemo(() => summarizeChangedFileScopes(files), [files]);
Expand Down Expand Up @@ -155,6 +164,7 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
allDirectoriesExpanded={allDirectoriesExpanded}
resolvedTheme={resolvedTheme}
onOpenTurnDiff={onOpenTurnDiff}
onFileContextMenu={onFileContextMenu}
/>
) : compactPreviewVisible ? (
<div className="px-2 pb-1.5 pt-1">
Expand Down Expand Up @@ -207,8 +217,10 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
allDirectoriesExpanded: boolean;
resolvedTheme: "light" | "dark";
onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;
onFileContextMenu?: ChangedFileContextMenuHandler | undefined;
}) {
const { files, allDirectoriesExpanded, onOpenTurnDiff, resolvedTheme, turnId } = props;
const { files, allDirectoriesExpanded, onOpenTurnDiff, onFileContextMenu, resolvedTheme, turnId } =
props;
const treeNodes = useMemo(() => buildTurnDiffTree(files), [files]);
const directoryPathsKey = useMemo(
() => collectDirectoryPaths(treeNodes).join("\u0000"),
Expand Down Expand Up @@ -294,6 +306,14 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
className="group flex w-full items-center gap-1.5 rounded-xl py-1 pr-3 text-left transition-colors hover:bg-accent/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
style={{ paddingLeft: `${leftPadding}px` }}
onClick={() => onOpenTurnDiff(turnId, node.path)}
onContextMenu={
onFileContextMenu
? (event) => {
event.preventDefault();
onFileContextMenu(node.path, event);
}
: undefined
}
>
{hasDirectoryNodes || depth > 0 ? (
<span aria-hidden="true" className="size-3.5 shrink-0" />
Expand All @@ -304,9 +324,10 @@ export const ChangedFilesTree = memo(function ChangedFilesTree(props: {
theme={resolvedTheme}
className="size-3.5 text-muted-foreground/70"
/>
<span className="truncate font-mono text-[11px] text-muted-foreground/80 group-hover:text-foreground/90">
{node.name}
</span>
<MiddleTruncate
value={node.name}
className="font-mono text-[11px] text-muted-foreground/80 group-hover:text-foreground/90"
/>
{node.stat && (
<span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
<DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
Expand Down
55 changes: 54 additions & 1 deletion apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ import { useUiStateStore } from "~/uiStateStore";
import { useClientSettings } from "~/hooks/useSettings";
import { type TimestampFormat } from "@t3tools/contracts/settings";
import { formatChatTimestampTooltip, formatShortTimestamp } from "../../timestampFormat";
// Avi Code addition: right-click reveal / open-with on changed files (upstream #11842).
import { useFileContextMenuHandler } from "../../fileContextMenu";

import {
buildInlineTerminalContextText,
Expand Down Expand Up @@ -1814,12 +1816,46 @@ function RevertUserMessageButton({ messageId }: { messageId: MessageId }) {
);
}

/**
* Avi Code addition: hover-revealed wall-clock time with a full-date tooltip,
* matching the message rows, for turn folds (ported from upstream #8641). The
* parent carries `group/timeline-row`; hover or focus reveals the time without
* adding a tab stop, and it stays outside the row layout while hidden.
*/
function TimelineRowTimestamp({
createdAt,
timestampFormat,
className,
}: {
createdAt: string;
timestampFormat: TimestampFormat;
className?: string;
}) {
return (
<Tooltip>
<TooltipTrigger
render={
<span
className={cn(
"pointer-events-none absolute me-1 shrink-0 whitespace-nowrap rounded-md text-muted-foreground text-xs tabular-nums opacity-0 group-hover/timeline-row:pointer-events-auto group-hover/timeline-row:static group-hover/timeline-row:opacity-100 group-focus-within/timeline-row:pointer-events-auto group-focus-within/timeline-row:static group-focus-within/timeline-row:opacity-100",
className,
)}
/>
}
>
{formatShortTimestamp(createdAt, timestampFormat)}
</TooltipTrigger>
<TooltipPopup>{formatChatTimestampTooltip(createdAt, timestampFormat)}</TooltipPopup>
</Tooltip>
);
}

function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-fold" }> }) {
const ctx = use(TimelineRowCtx);
const Icon = row.expanded ? ChevronDownIcon : ChevronRightIcon;

return (
<div className="border-b border-border/60 pb-2 pt-1">
<div className="group/timeline-row relative flex items-center gap-1 border-b border-border/60 pb-2 pe-0.5 pt-1">
<button
type="button"
aria-expanded={row.expanded}
Expand All @@ -1830,6 +1866,11 @@ function TurnFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "turn-
<span>{row.label}</span>
<Icon className="size-3.5" />
</button>
<TimelineRowTimestamp
createdAt={row.createdAt}
timestampFormat={ctx.timestampFormat}
className="ms-auto"
/>
</div>
);
}
Expand Down Expand Up @@ -2104,6 +2145,8 @@ function AssistantChangedFilesSectionInner({
onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;
}) {
const activity = use(TimelineRowActivityCtx);
const ctx = use(TimelineRowCtx);
const showFileContextMenu = useFileContextMenuHandler(ctx.activeThreadEnvironmentId);
const isLatestTurn = activity.latestTurnId === turnSummary.turnId;
const persistedExpanded = useUiStateStore(
(store) => store.threadChangedFilesExpandedById[routeThreadKey]?.[turnSummary.turnId],
Expand All @@ -2128,6 +2171,16 @@ function AssistantChangedFilesSectionInner({
}
onToggleAllDirectories={() => setAllDirectoriesExpanded((current) => !current)}
onOpenTurnDiff={onOpenTurnDiff}
onFileContextMenu={(filePath, event) =>
showFileContextMenu(
{
environmentId: ctx.activeThreadEnvironmentId,
filePath,
workspaceRoot: ctx.workspaceRoot,
},
event,
)
}
/>
);
}
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/components/files/FileBrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { useComposerHandleContext } from "~/composerHandleContext";
import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
import { useTheme } from "~/hooks/useTheme";
// Avi Code addition: right-click reveal / open-with for files (upstream #11842).
import { useFileContextMenu, type FileContextMenuAction } from "~/fileContextMenu";
import { cn } from "~/lib/utils";
import { readLocalApi } from "~/localApi";
import { T3_PIERRE_ICONS } from "~/pierre-icons";
Expand Down Expand Up @@ -125,6 +127,7 @@ export default function FileBrowserPanel({
}: FileBrowserPanelProps) {
const { resolvedTheme } = useTheme();
const composerRef = useComposerHandleContext();
const fileContextMenu = useFileContextMenu(environmentId);
const entriesQuery = useProjectEntriesQuery(environmentId, cwd);
const entries = entriesQuery.data?.entries ?? [];
const entryKinds = useMemo(
Expand Down Expand Up @@ -309,9 +312,14 @@ export default function FileBrowserPanel({
const position = pointerIsFresh
? { x: pointer.x, y: pointer.y }
: { x: anchorRect.left, y: anchorRect.bottom };
// Avi Code addition: reveal / open-with actions, only for file entries.
const fileTarget = { environmentId, filePath: relativePath, workspaceRoot: cwd };
const fileMenuItems =
item.kind === "file" ? fileContextMenu.buildItems(fileTarget) : [];
try {
const clicked = await api.contextMenu.show(
[
...fileMenuItems,
{ id: "new-file", label: "New File" },
{ id: "new-folder", label: "New Folder" },
{ id: "rename", label: "Rename" },
Expand All @@ -321,6 +329,13 @@ export default function FileBrowserPanel({
],
position,
);
if (clicked === null) return;
// "Open with" submenu selections report the child id ("editor:<id>"),
// which is not present in the top-level item list.
if (fileMenuItems.some((entry) => entry.id === clicked) || clicked.startsWith("editor:")) {
await fileContextMenu.activate(clicked as FileContextMenuAction, fileTarget);
return;
}
if (clicked === "new-file" || clicked === "new-folder") {
startCreateEntry(clicked === "new-folder" ? "directory" : "file", directoryPrefix);
return;
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/components/ui/middle-truncate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vite-plus/test";

import { splitForMiddleTruncate } from "./middle-truncate";

describe("splitForMiddleTruncate", () => {
it("keeps the meaningful tail of a long branch name", () => {
const value = "fix/cache-main-20260918-180825";
const split = splitForMiddleTruncate(value);
expect(split).not.toBeNull();
// Default keeps the trailing date that distinguishes similar branches.
expect(split?.tail).toBe("918-180825");
expect(`${split?.head}${split?.tail}`).toBe(value);
});

it("keeps a short last path segment intact", () => {
const split = splitForMiddleTruncate("apps/web/src/index.ts");
expect(split?.tail).toBe("index.ts");
});

it("does not split when the string is short enough to show whole", () => {
expect(splitForMiddleTruncate("main")).toBeNull();
});

it("does not split inside a surrogate pair", () => {
const split = splitForMiddleTruncate("πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€", 4);
expect(split).not.toBeNull();
expect(Array.from(split?.tail ?? "")).toHaveLength(4);
expect(`${split?.head}${split?.tail}`).toBe("πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€");
});
});
Loading
Loading