Skip to content

Commit 13110ee

Browse files
feat(tui): add project path command palette flow
1 parent 162b43b commit 13110ee

3 files changed

Lines changed: 125 additions & 11 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from "vitest";
2+
import { isCommandPaletteProjectPathQuery } from "./commandPaletteProjects";
3+
4+
describe("isCommandPaletteProjectPathQuery", () => {
5+
it("detects explicit project path inputs", () => {
6+
expect(isCommandPaletteProjectPathQuery("~/.local/src/t3code")).toBe(true);
7+
expect(isCommandPaletteProjectPathQuery("~/")).toBe(true);
8+
expect(isCommandPaletteProjectPathQuery("/tmp/project")).toBe(true);
9+
expect(isCommandPaletteProjectPathQuery("./project")).toBe(true);
10+
expect(isCommandPaletteProjectPathQuery("../project")).toBe(true);
11+
expect(isCommandPaletteProjectPathQuery("C:\\Users\\maria\\project")).toBe(true);
12+
expect(isCommandPaletteProjectPathQuery("\\\\server\\share\\project")).toBe(true);
13+
});
14+
15+
it("does not treat ordinary command searches as project paths", () => {
16+
expect(isCommandPaletteProjectPathQuery("diag")).toBe(false);
17+
expect(isCommandPaletteProjectPathQuery("add project")).toBe(false);
18+
expect(isCommandPaletteProjectPathQuery("github repo")).toBe(false);
19+
expect(isCommandPaletteProjectPathQuery("")).toBe(false);
20+
});
21+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^(?:[a-zA-Z]:[\\/]|\\\\)/;
2+
3+
export function isCommandPaletteProjectPathQuery(query: string): boolean {
4+
const trimmed = query.trim();
5+
if (trimmed.length === 0) {
6+
return false;
7+
}
8+
9+
return (
10+
trimmed === "~" ||
11+
trimmed.startsWith("~/") ||
12+
trimmed.startsWith("~\\") ||
13+
trimmed.startsWith("/") ||
14+
trimmed.startsWith("./") ||
15+
trimmed.startsWith("../") ||
16+
trimmed.startsWith(".\\") ||
17+
trimmed.startsWith("..\\") ||
18+
WINDOWS_ABSOLUTE_PATH_PATTERN.test(trimmed)
19+
);
20+
}

apps/tui/src/ui.tsx

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ import { CODE_BLOCK_TREE_SITTER_PARSERS } from "./codeBlockParsers";
167167
import { resolveTuiPaths } from "./config";
168168
import { resolveComposerPrimaryAction } from "./composerAction";
169169
import { parseStandaloneComposerModeCommand } from "./composerCommands";
170+
import { isCommandPaletteProjectPathQuery } from "./commandPaletteProjects";
170171
import { clampSlashCommandMenuIndex, resolveTuiSlashCommandMenu } from "./composerSlashMenu";
171172
import { formatReasoningEffortLabel, truncateToolbarLabel } from "./composerControlLabels";
172173
import {
@@ -6059,13 +6060,34 @@ export function App({
60596060
],
60606061
[commandPaletteProjectItems, commandPaletteStaticItems, commandPaletteThreadItems],
60616062
);
6062-
const visibleCommandPaletteItems = useMemo(
6063-
() =>
6064-
commandPaletteItems
6065-
.filter((item) => commandPaletteTextMatches(item, commandPaletteQuery))
6066-
.slice(0, COMMAND_PALETTE_MAX_ITEMS),
6067-
[commandPaletteItems, commandPaletteQuery],
6068-
);
6063+
const commandPaletteProjectPathItem = useMemo<CommandPaletteItem | null>(() => {
6064+
const rawQuery = commandPaletteQuery.trim();
6065+
if (!isCommandPaletteProjectPathQuery(rawQuery)) {
6066+
return null;
6067+
}
6068+
6069+
const workspaceRoot = normalizeWorkspaceRoot(rawQuery, paths.homeDir);
6070+
const existingProject = projects.find((project) => project.workspaceRoot === workspaceRoot);
6071+
return {
6072+
id: "project:path",
6073+
section: "Projects",
6074+
icon: "󰉋",
6075+
label: existingProject ? `Open ${existingProject.title}` : "Add project from path",
6076+
description: workspaceRoot,
6077+
trailingLabel: "Enter",
6078+
keywords: ["add project", "workspace", "folder", "path", rawQuery, workspaceRoot],
6079+
disabled: projectPathBusy,
6080+
};
6081+
}, [commandPaletteQuery, paths.homeDir, projectPathBusy, projects]);
6082+
const visibleCommandPaletteItems = useMemo(() => {
6083+
const filteredItems = commandPaletteItems.filter((item) =>
6084+
commandPaletteTextMatches(item, commandPaletteQuery),
6085+
);
6086+
const items = commandPaletteProjectPathItem
6087+
? [commandPaletteProjectPathItem, ...filteredItems]
6088+
: filteredItems;
6089+
return items.slice(0, COMMAND_PALETTE_MAX_ITEMS);
6090+
}, [commandPaletteItems, commandPaletteProjectPathItem, commandPaletteQuery]);
60696091
const changedSettingLabels = [
60706092
...(appSettings.theme !== DEFAULT_APP_THEME ? ["Theme"] : []),
60716093
...(tuiThemeId !== DEFAULT_TUI_THEME_ID ? ["Theme preset"] : []),
@@ -8101,7 +8123,7 @@ export function App({
81018123
key.name === "linefeed"
81028124
) {
81038125
key.preventDefault();
8104-
runCommandPaletteItem(visibleCommandPaletteItems[commandPaletteIndex]);
8126+
void runCommandPaletteItem(visibleCommandPaletteItems[commandPaletteIndex]);
81058127
return;
81068128
}
81078129
}
@@ -9499,13 +9521,47 @@ export function App({
94999521
return projectId;
95009522
}
95019523

9524+
function openProjectChat(projectId: string) {
9525+
const latestThread = threadsByProject.get(projectId)?.[0];
9526+
if (latestThread) {
9527+
clearSelection();
9528+
setSelectionAnchorThreadId(latestThread.id);
9529+
selectThread(projectId, latestThread.id);
9530+
setStatus("Ready");
9531+
return;
9532+
}
9533+
9534+
openDraftThread(projectId);
9535+
}
9536+
9537+
async function submitCommandPaletteProjectPath(rawWorkspaceRoot: string): Promise<void> {
9538+
if (projectPathBusy) return;
9539+
setProjectPathBusy(true);
9540+
setProjectPathError(null);
9541+
9542+
try {
9543+
const projectId = await createProject(rawWorkspaceRoot);
9544+
openProjectChat(projectId);
9545+
setOverlayMenu(null);
9546+
setCommandPaletteQuery("");
9547+
} catch (error) {
9548+
const message =
9549+
error instanceof Error ? error.message : "Failed to add project from that path.";
9550+
setProjectPathError(message);
9551+
setStatus(message);
9552+
} finally {
9553+
setProjectPathBusy(false);
9554+
}
9555+
}
9556+
95029557
async function submitProjectPath(rawWorkspaceRoot: string): Promise<void> {
95039558
if (projectPathBusy) return;
95049559
setProjectPathBusy(true);
95059560
setProjectPathError(null);
95069561

95079562
try {
9508-
await createProject(rawWorkspaceRoot);
9563+
const projectId = await createProject(rawWorkspaceRoot);
9564+
openProjectChat(projectId);
95099565
closeProjectPathPrompt();
95109566
} catch (error) {
95119567
const message =
@@ -10433,18 +10489,24 @@ export function App({
1043310489
setOverlayAnchor(null);
1043410490
setCommandPaletteQuery("");
1043510491
setCommandPaletteIndex(0);
10492+
setProjectPathError(null);
1043610493
setFocusArea("settings");
1043710494
} else {
1043810495
setCommandPaletteQuery("");
10496+
setProjectPathError(null);
1043910497
setFocusArea("composer");
1044010498
}
1044110499
logger.log(next ? "overlay.open" : "overlay.close", { menu: "command-palette" });
1044210500
return next;
1044310501
});
1044410502
}
1044510503

10446-
function runCommandPaletteItem(item: CommandPaletteItem | undefined) {
10504+
async function runCommandPaletteItem(item: CommandPaletteItem | undefined): Promise<void> {
1044710505
if (!item || item.disabled) return;
10506+
if (item.id === "project:path") {
10507+
await submitCommandPaletteProjectPath(commandPaletteQuery);
10508+
return;
10509+
}
1044810510
setOverlayMenu(null);
1044910511
setCommandPaletteQuery("");
1045010512
if (item.id === "thread:new") {
@@ -16378,6 +16440,7 @@ export function App({
1637816440
onMouseDown={() => {
1637916441
setOverlayMenu(null);
1638016442
setCommandPaletteQuery("");
16443+
setProjectPathError(null);
1638116444
}}
1638216445
>
1638316446
<box
@@ -16418,12 +16481,14 @@ export function App({
1641816481
onInput={(value) => {
1641916482
setCommandPaletteQuery(value);
1642016483
setCommandPaletteIndex(0);
16484+
setProjectPathError(null);
1642116485
}}
1642216486
onKeyDown={(key) => {
1642316487
if (key.name === "escape") {
1642416488
key.preventDefault();
1642516489
setOverlayMenu(null);
1642616490
setCommandPaletteQuery("");
16491+
setProjectPathError(null);
1642716492
}
1642816493
}}
1642916494
style={{
@@ -16463,7 +16528,9 @@ export function App({
1646316528
{...(item.disabled !== undefined ? { disabled: item.disabled } : {})}
1646416529
{...(item.trailingLabel ? { trailingLabel: item.trailingLabel } : {})}
1646516530
onHover={() => setCommandPaletteIndex(index)}
16466-
onPress={() => runCommandPaletteItem(item)}
16531+
onPress={() => {
16532+
void runCommandPaletteItem(item);
16533+
}}
1646716534
/>
1646816535
{item.description ? (
1646916536
<text
@@ -16478,6 +16545,12 @@ export function App({
1647816545
);
1647916546
})
1648016547
)}
16548+
{projectPathError && commandPaletteProjectPathItem ? (
16549+
<text
16550+
content={truncateTitleForDisplay(projectPathError, commandPaletteWidth - 4)}
16551+
style={{ fg: PALETTE.warning, marginTop: 1, marginLeft: 1 }}
16552+
/>
16553+
) : null}
1648116554
<text
1648216555
content="↑↓ navigate · Enter run · Esc close"
1648316556
style={{ fg: PALETTE.subtle, marginTop: 1, marginLeft: 1 }}

0 commit comments

Comments
 (0)