Skip to content
Merged
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
7 changes: 6 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10354,7 +10354,12 @@ export default function ChatView(props: ChatViewProps) {
"flex shrink-0",
panelAnimationsActive &&
"motion-safe:transition-opacity motion-safe:duration-(--panel-animation-duration) motion-safe:ease-out",
rightPanelOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0",
// Closed, the control leaves the flex flow so the cluster is only as wide as the two
// toggles the header reserves room for; anchored to the cluster's left edge, it fades
// out where it stood rather than over the terminal toggle.
rightPanelOpen
? "pointer-events-auto opacity-100"
: "pointer-events-none absolute right-full mr-1 opacity-0",
)}
inert={!rightPanelOpen}
>
Expand Down
9 changes: 0 additions & 9 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,15 +1057,6 @@ describe("sortThreadsForSidebar", () => {
expect(sorted.map((thread) => thread.id)).toEqual(["newest", "middle", "oldest"]);
});

it("breaks creation-time ties by id so the order is stable", () => {
const sorted = sortThreadsForSidebar([
sortable({ id: "b", createdAt: "2026-03-09T10:00:00.000Z" }),
sortable({ id: "a", createdAt: "2026-03-09T10:00:00.000Z" }),
]);

expect(sorted.map((thread) => thread.id)).toEqual(["a", "b"]);
});

it("surfaces an un-settled thread at the top via its re-entry stamp", () => {
const sorted = sortThreadsForSidebar([
{
Expand Down
8 changes: 5 additions & 3 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,9 +492,11 @@ export const ChatHeader = memo(function ChatHeader({
data-chat-header-actions
className={cn(
"flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3",
// Reserve two panel toggles plus their 4px gaps and 1px edge inset.
// The page header adds 8px more right padding at sm.
rightPanelOpen ? "pr-0" : "pr-18.25 sm:pr-14.25",
// Reserve two panel toggles (32px, 28px at sm) with their 4px gap and 1px edge inset,
// plus the same gap the actions keep between themselves (gap-2, gap-3 at @3xl) so the
// terminal toggle does not sit against the last action. The page header adds 8px more
// right padding at sm.
rightPanelOpen ? "pr-0" : "pr-19.25 sm:pr-15.25 @3xl/header-actions:pr-16.25",
"[[data-panel-animations=true]_&]:motion-safe:transition-[padding-right] [[data-panel-animations=true]_&]:motion-safe:duration-(--panel-animation-duration) [[data-panel-animations=true]_&]:motion-safe:ease-out",
)}
>
Expand Down
15 changes: 11 additions & 4 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,8 @@ export function PullRequestDetailPanel({
const statePresentation = detail
? resolvePullRequestState({ state: detail.state, isDraft: detail.isDraft })
: null;
const showsApproveWorkflows =
workflowApprovalsRequired > 0 && !checksStale && can("approve-workflows");
const checksSummary = checksStale
? checksState === null
? "No checks reported"
Expand Down Expand Up @@ -2603,8 +2605,13 @@ export function PullRequestDetailPanel({
))}
</ToggleGroup>
{tab === "summary" ? (
<span className="ml-auto inline-flex shrink-0 items-center">
{workflowApprovalsRequired > 0 && !checksStale && can("approve-workflows") ? (
<span
className={cn(
"ml-auto flex items-center justify-end",
showsApproveWorkflows ? "shrink-0" : "min-w-0 flex-1",
)}
>
{showsApproveWorkflows ? (
<Tooltip>
<TooltipTrigger
render={
Expand Down Expand Up @@ -2640,7 +2647,7 @@ export function PullRequestDetailPanel({
</Tooltip>
) : (
<span
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground"
className="flex h-4 min-w-0 flex-wrap content-start items-center justify-end gap-x-1.5 overflow-hidden text-xs text-muted-foreground"
aria-label={checksSummary ? `Checks: ${checksSummary}` : "Checks"}
>
{checksState !== null ? (
Expand All @@ -2653,7 +2660,7 @@ export function PullRequestDetailPanel({
) : (
<CircleDotIcon aria-hidden className="size-3.5" />
)}
{checksSummary}
<span className="whitespace-nowrap">{checksSummary}</span>
</span>
)}
</span>
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/components/settings/KeybindingsSettings.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,31 @@ describe("KeybindingsSettings.logic", () => {
).toBe("mod+shift+k");
});

it.each([
["k", "KeyK", "k"],
["Tab", "Tab", "tab"],
["F5", "F5", "f5"],
])("captures %s without a modifier", (key, code, expected) => {
const noModifiers = { metaKey: false, ctrlKey: false, altKey: false, shiftKey: false };
expect(keybindingFromKeyboardEvent({ key, code, ...noModifiers }, "MacIntel")).toBe(expected);
});

it("waits for a key when only a modifier is pressed", () => {
expect(
keybindingFromKeyboardEvent(
{
key: "Meta",
code: "MetaLeft",
metaKey: true,
ctrlKey: false,
altKey: false,
shiftKey: false,
},
"MacIntel",
),
).toBeNull();
});

it.each([
["@", "Digit2", "mod+shift+2"],
['"', "Digit2", "mod+shift+2"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ function normalizeShortcutKeyToken(key: string): string | null {
return null;
}

/** Turns a keydown into a binding such as `mod+shift+k` or `tab`. Null for modifier-only presses. */
export function keybindingFromKeyboardEvent(
event: Pick<KeyboardEvent, "key" | "code" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey">,
platform: string,
Expand All @@ -372,9 +373,6 @@ export function keybindingFromKeyboardEvent(
}
if (event.altKey) parts.push("alt");
if (event.shiftKey) parts.push("shift");
if (parts.length === 0) {
return null;
}
parts.push(keyToken);
return parts.join("+");
}
6 changes: 4 additions & 2 deletions apps/web/src/components/settings/KeybindingsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,8 @@ function useKeybindingRowEditor({
};

const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Tab") return;
// Tab is recorded like any key while recording; after that it moves focus on.
if (event.key === "Tab" && !isRecording) return;
event.preventDefault();
if (event.key === "Escape") {
setDraft({ keyDraft: row.key, isRecording: false });
Expand Down Expand Up @@ -1129,7 +1130,8 @@ function useNewKeybindingDraft({
};

const captureKeybinding = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Tab") return;
// Tab is recorded like any key while recording; after that it moves focus on.
if (event.key === "Tab" && !isRecording) return;
event.preventDefault();
if (event.key === "Escape") {
setDraft({ keyDraft: "", isRecording: false });
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2679,7 +2679,7 @@ export function GeneralSettingsPanel() {
{...searchableSetting("continue-threads-after-server-update")}
serverScoped
settingKeys={["continueThreadsAfterServerUpdate"]}
description="Automatically resume interrupted threads after an update, crash, or machine restart on the selected environments. Update older servers first."
description="Automatically resume interrupted threads after an update, crash, or machine restart on the selected environments."
status={
!supportsRestartContinuation
? "All selected connected environments must support restart continuation."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ it("records macOS Command without treating it as Control", async () => {
(await start(true)).input.props.onKeyDown(event("k", "KeyK", { metaKey: true }));
expect(recorded).toHaveBeenCalledWith(expect.objectContaining({ key: "k", modKey: true }));
});
it.each([
["k", "KeyK"],
["F5", "F5"],
])("ignores unmodified %s and keeps recording for a global shortcut", async (key, code) => {
const recorder = await start();
recorder.input.props.onKeyDown(event(key, code));
expect(recorded).not.toHaveBeenCalled();
expect(render().recording).toBe(true);
expect(suppress).toHaveBeenLastCalledWith(true);
render().input.props.onKeyDown(event("k", "KeyK", { ctrlKey: true }));
expect(recorded).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ key: "k", modKey: true }),
);
expect(render().recording).toBe(false);
expect(suppress).toHaveBeenLastCalledWith(false);
});
it.each(["Escape", "blur", "unmount"])("cancels on %s without selecting keys", async (cancel) => {
const recorder = await start();
if (cancel === "Escape") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export function useSnapShotShortcutRecorder({
}
return;
}
// A global shortcut without a modifier would take that key from every app.
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) return;
const input = keybindingFromKeyboardEvent(event, navigator.platform);
if (!input) return;
const next = parseKeybindingShortcut(input);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/pierre-icons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ describe("Pierre file icons", () => {

it("normalizes common markdown fence language aliases", () => {
assert.equal(syntheticFileNameForLanguageId("typescript"), "file.ts");
assert.equal(syntheticFileNameForLanguageId("dockerfile"), "Dockerfile");
assert.equal(syntheticFileNameForLanguageId("shellscript"), "file.sh");
assert.equal(syntheticFileNameForLanguageId("python"), "file.py");
});

it("gives a dockerfile fence the Docker icon", () => {
assert.isTrue(hasSpecificPierreIconForFileName(syntheticFileNameForLanguageId("dockerfile")));
});
});
11 changes: 9 additions & 2 deletions apps/web/src/pierre-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const completeIconResolver = createFileTreeIconResolver(T3_PIERRE_ICONS);
const LANGUAGE_EXTENSION_ALIASES: Record<string, string> = {
bash: "sh",
csharp: "cs",
dockerfile: "dockerfile",
javascript: "js",
jsx: "jsx",
markdown: "md",
Expand Down Expand Up @@ -79,9 +78,17 @@ export function inferEntryKindFromPath(pathValue: string): "file" | "directory"
return base.includes(".") ? "file" : "directory";
}

/** Languages whose files are recognised by name rather than by extension. */
const LANGUAGE_FILE_NAMES: Record<string, string> = {
dockerfile: "Dockerfile",
};

export function syntheticFileNameForLanguageId(languageId: string): string {
const normalized = languageId.toLowerCase();
return `file.${LANGUAGE_EXTENSION_ALIASES[normalized] ?? normalized}`;
return (
LANGUAGE_FILE_NAMES[normalized] ??
`file.${LANGUAGE_EXTENSION_ALIASES[normalized] ?? normalized}`
);
}

export function resolvePierreIconForEntry(
Expand Down
Loading