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
39 changes: 39 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ import {
useThread,
useThreadRefs,
useThreadShell,
readThreadShells,
readEnvironmentSupportsSettlement,
} from "../state/entities";
import { environmentShell } from "../state/shell";
import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer";
Expand Down Expand Up @@ -6105,6 +6107,35 @@ export default function ChatView(props: ChatViewProps) {
setUnsettlingThreadKey((current) => (current === threadKey ? null : current));
}
}, [activeThreadRef, unsettleThreadMutation]);
const handleUnsettleLastThread = useCallback(async () => {
const latest = readThreadShells()
.filter((thread) => {
if (
thread.archivedAt !== null ||
!readEnvironmentSupportsSettlement(thread.environmentId) ||
thread.settledOverride !== "settled" ||
thread.settledAt === null
)
return false;
return Number.isFinite(Date.parse(thread.settledAt));
})
.toSorted((left, right) => Date.parse(right.settledAt!) - Date.parse(left.settledAt!))[0];
if (!latest) return;
const result = await unsettleThreadMutation({
environmentId: latest.environmentId,
input: { threadId: latest.id, reason: "user" },
});
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to un-settle thread",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
}
}, [unsettleThreadMutation]);
const unsnoozeThreadMutation = useAtomCommand(threadEnvironment.unsnooze, {
reportFailure: false,
});
Expand Down Expand Up @@ -6762,6 +6793,13 @@ export default function ChatView(props: ChatViewProps) {
return;
}

if (command === "thread.unsettleLast") {
event.preventDefault();
event.stopPropagation();
if (!event.repeat) void handleUnsettleLastThread();
return;
}

if (command === "thread.pin") {
event.preventDefault();
event.stopPropagation();
Expand Down Expand Up @@ -6962,6 +7000,7 @@ export default function ChatView(props: ChatViewProps) {
splitPanelTerminal,
keybindings,
handleUnsettleActiveThread,
handleUnsettleLastThread,
isServerThread,
onInterrupt,
onToggleDiff,
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1331,3 +1331,16 @@ describe("composer and pull request shortcuts", () => {
});
}
});

describe("unsettle last thread shortcut", () => {
it("resolves mod+shift+u outside the terminal", () => {
assert.equal(
resolveShortcutCommand(
event({ key: "u", metaKey: true, shiftKey: true }),
DEFAULT_RESOLVED_KEYBINDINGS,
{ platform: "MacIntel", context: { terminalFocus: false } },
),
"thread.unsettleLast",
);
});
});
47 changes: 46 additions & 1 deletion apps/web/src/routes/_chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ import { ThreadRouteView } from "../components/ThreadRouteView";
import { resolveThreadRouteTarget } from "../threadRoutes";
import { useClientSettings, useLegacySidebarEnabled } from "../hooks/useSettings";
import { openCommandPalette } from "../commandPaletteBus";
import { useProjects } from "../state/entities";
import {
readEnvironmentSupportsSettlement,
readThreadShells,
useProjects,
} from "../state/entities";
import { threadEnvironment } from "../state/threads";
import { useAtomCommand } from "../state/use-atom-command";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { usePrimaryEnvironmentId } from "../state/environments";
import { selectProjectGroupingSettings } from "../logicalProject";
import { buildSidebarProjectSnapshots } from "../sidebarProjectGrouping";
Expand All @@ -25,6 +35,7 @@ import { stackedThreadToast, toastManager } from "~/components/ui/toast";
import { primaryServerKeybindingsAtom } from "~/state/server";

function ChatRouteGlobalShortcuts() {
const unsettleThread = useAtomCommand(threadEnvironment.unsettle, { reportFailure: false });
const clearSelection = useThreadSelectionStore((state) => state.clearSelection);
const selectedThreadKeysSize = useThreadSelectionStore((state) => state.selectedThreadKeys.size);
const { activeDraftThread, activeThread, defaultProjectRef, handleNewThread, routeThreadRef } =
Expand Down Expand Up @@ -73,6 +84,39 @@ function ChatRouteGlobalShortcuts() {
return;
}

if (command === "thread.unsettleLast") {
event.preventDefault();
event.stopPropagation();
if (event.repeat) return;
const latest = readThreadShells()
.filter(
(thread) =>
thread.archivedAt === null &&
readEnvironmentSupportsSettlement(thread.environmentId) &&
thread.settledOverride === "settled" &&
thread.settledAt !== null &&
Number.isFinite(Date.parse(thread.settledAt)),
)
.toSorted((left, right) => Date.parse(right.settledAt!) - Date.parse(left.settledAt!))[0];
if (!latest) return;
void unsettleThread({
environmentId: latest.environmentId,
input: { threadId: latest.id, reason: "user" },
}).then((result) => {
if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Failed to un-settle thread",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
}
});
return;
}

if (event.key === "Escape" && selectedThreadKeysSize > 0) {
event.preventDefault();
clearSelection();
Expand Down Expand Up @@ -171,6 +215,7 @@ function ChatRouteGlobalShortcuts() {
selectedThreadKeysSize,
legacySidebarEnabled,
terminalOpen,
unsettleThread,
]);

return null;
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const THREAD_KEYBINDING_COMMANDS = [
"thread.next",
"thread.copyReference",
"thread.settle",
"thread.unsettleLast",
"thread.pin",
...THREAD_JUMP_KEYBINDING_COMMANDS,
] as const;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
{ key: "mod+shift+]", command: "thread.next" },
{ key: "mod+shift+c", command: "thread.copyReference", when: "!terminalFocus" },
{ key: "mod+shift+s", command: "thread.settle", when: "!terminalFocus" },
{ key: "mod+shift+u", command: "thread.unsettleLast", when: "!terminalFocus" },
{ key: "mod+shift+p", command: "thread.pin", when: "!terminalFocus" },
...THREAD_JUMP_KEYBINDING_COMMANDS.map((command, index) => ({
key: `mod+${index + 1}`,
Expand Down
Loading