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
51 changes: 51 additions & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b");
assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b");
assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d");
assert.equal(defaultsByCommand.get("chat.newEnvironment"), "mod+shift+n");
assert.isFalse(defaultsByCommand.has("chat.newLocal"));
assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
assert.equal(defaultsByCommand.get("modelPicker.jump.9"), "mod+9");
}),
Expand Down Expand Up @@ -301,6 +303,55 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
}).pipe(Effect.provide(makeKeybindingsLayer())),
);

it.effect("migrates the previous new-local default to the environment picker", () =>
Effect.gen(function* () {
const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
yield* writeKeybindingsConfig(keybindingsConfigPath, [
{
key: "mod+shift+n",
command: "chat.newLocal",
when: "!terminalFocus",
},
{
key: "mod+shift+l",
command: "chat.newLocal",
when: "!terminalFocus",
},
]);

yield* Effect.gen(function* () {
const keybindings = yield* Keybindings.Keybindings;
yield* keybindings.syncDefaultKeybindingsOnStartup;
});

const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
assert.isTrue(
persisted.some(
(entry) =>
entry.command === "chat.newEnvironment" &&
entry.key === "mod+shift+n" &&
entry.when === "!terminalFocus",
),
);
assert.isFalse(
persisted.some(
(entry) =>
entry.command === "chat.newLocal" &&
entry.key === "mod+shift+n" &&
entry.when === "!terminalFocus",
),
);
assert.isTrue(
persisted.some(
(entry) =>
entry.command === "chat.newLocal" &&
entry.key === "mod+shift+l" &&
entry.when === "!terminalFocus",
),
);
}).pipe(Effect.provide(makeKeybindingsLayer())),
);

it.effect("skips conflicting default keybindings on startup and logs a detailed warning", () => {
const messages: string[] = [];
const logger = Logger.make(({ message }) => {
Expand Down
24 changes: 22 additions & 2 deletions apps/server/src/keybindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): bool
);
}

const LEGACY_NEW_LOCAL_DEFAULT: KeybindingRule = {
key: "mod+shift+n",
command: "chat.newLocal",
when: "!terminalFocus",
};
const NEW_ENVIRONMENT_DEFAULT: KeybindingRule = {
key: "mod+shift+n",
command: "chat.newEnvironment",
when: "!terminalFocus",
};

function keybindingShortcutContext(rule: KeybindingRule): string | null {
const parsed = parseKeybindingShortcut(rule.key);
if (!parsed) return null;
Expand Down Expand Up @@ -493,7 +504,16 @@ const make = Effect.gen(function* () {
yield* Cache.invalidate(resolvedConfigCache, resolvedConfigCacheKey);
return;
}
const customConfig = runtimeConfig.keybindings;
const shouldMigrateNewEnvironmentDefault =
!runtimeConfig.keybindings.some((entry) => entry.command === "chat.newEnvironment") &&
runtimeConfig.keybindings.some((entry) =>
isSameKeybindingRule(entry, LEGACY_NEW_LOCAL_DEFAULT),
);
const customConfig = shouldMigrateNewEnvironmentDefault
? runtimeConfig.keybindings.map((entry) =>
isSameKeybindingRule(entry, LEGACY_NEW_LOCAL_DEFAULT) ? NEW_ENVIRONMENT_DEFAULT : entry,
)
: runtimeConfig.keybindings;
const existingCommands = new Set(customConfig.map((entry) => entry.command));
const missingDefaults: KeybindingRule[] = [];
const shortcutConflictWarnings: Array<{
Expand Down Expand Up @@ -530,7 +550,7 @@ const make = Effect.gen(function* () {
reason: "shortcut context already used by existing rule",
});
}
if (missingDefaults.length === 0) {
if (missingDefaults.length === 0 && !shouldMigrateNewEnvironmentDefault) {
yield* Cache.invalidate(resolvedConfigCache, resolvedConfigCacheKey);
return;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/commandPaletteBus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const COMMAND_PALETTE_OPEN_EVENT = "t3code:open-command-palette";

export interface CommandPaletteOpenDetail {
readonly open?: "add-project" | "new-thread-in";
readonly open?: "add-project" | "new-thread-in" | "new-thread-on";
}

export function openCommandPalette(detail?: CommandPaletteOpenDetail): void {
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,38 @@ import {
buildThreadActionItems,
enumerateCommandPaletteItems,
filterCommandPaletteGroups,
resolveNewThreadOnIntent,
type CommandPaletteGroup,
} from "./CommandPalette.logic";
import { reduceCommandPaletteUiState } from "./CommandPalette";

describe("resolveNewThreadOnIntent", () => {
it("distinguishes loading from a loaded-empty environment list", () => {
expect(
resolveNewThreadOnIntent({ isActive: false, isLoaded: false, environmentItemCount: 0 }),
).toBe("ignore");
expect(
resolveNewThreadOnIntent({ isActive: true, isLoaded: false, environmentItemCount: 0 }),
).toBe("defer");
expect(
resolveNewThreadOnIntent({ isActive: true, isLoaded: true, environmentItemCount: 0 }),
).toBe("clear");
expect(
resolveNewThreadOnIntent({ isActive: true, isLoaded: true, environmentItemCount: 1 }),
).toBe("open");
});
});

describe("reduceCommandPaletteUiState", () => {
it("closes and clears a deferred open intent when no targets are available", () => {
expect(
reduceCommandPaletteUiState(
{ open: true, openIntent: { kind: "new-thread-on" } },
{ _tag: "SetOpen", open: false },
),
).toEqual({ open: false, openIntent: null });
});
});

describe("enumerateCommandPaletteItems", () => {
it("assigns positional jump shortcuts to the first nine displayed items", () => {
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export const RECENT_THREAD_LIMIT = 12;
export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80";
export const ADDON_ICON_CLASS = "size-4";

export function resolveNewThreadOnIntent(input: {
Comment thread
colonelpanic8 marked this conversation as resolved.
isActive: boolean;
isLoaded: boolean;
environmentItemCount: number;
}): "ignore" | "defer" | "clear" | "open" {
if (!input.isActive) return "ignore";
if (!input.isLoaded) return "defer";
return input.environmentItemCount > 0 ? "open" : "clear";
}

export interface CommandPaletteItem {
readonly kind: "action" | "submenu";
readonly value: string;
Expand Down
Loading
Loading