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
68 changes: 59 additions & 9 deletions src/renderer/src/features/launcher/AgentLaunchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ interface AgentLaunchDialogProps {
onLaunch(provider: AgentProviderId, profile: LaunchProfileId, cwd: string): Promise<void>;
}

export function directoryPathFromClipboard(text: string): string | null {
let path = text.trim();
if (!path) return null;

// Finder and file managers may expose copied folders as a URI list.
if (path.includes("\n") || path.includes("\r")) {
path = path.split(/\r?\n/).map((line) => line.trim()).find((line) => line && !line.startsWith("#")) ?? "";
}
if ((path.startsWith('"') && path.endsWith('"')) || (path.startsWith("'") && path.endsWith("'"))) {
path = path.slice(1, -1).trim();
}
if (!path) return null;

if (path.toLowerCase().startsWith("file://")) {
try {
const url = new URL(path);
const decodedPath = decodeURIComponent(url.pathname);
path = url.hostname ? `//${url.hostname}${decodedPath}` : decodedPath;
if (/^\/[a-zA-Z]:\//.test(path)) path = path.slice(1);
} catch {
return null;
}
}

return path || null;
}

export function AgentLaunchDialog({
provider,
settings,
Expand Down Expand Up @@ -55,7 +82,24 @@ export function AgentLaunchDialog({

const chooseDirectory = async (): Promise<void> => {
const selected = await window.canvasTTY.dialog.pickDirectory(cwd);
if (selected) setCwd(selected);
if (selected) {
setCwd(selected);
setError(null);
}
};

const pasteDirectory = async (): Promise<void> => {
try {
const path = directoryPathFromClipboard(await window.canvasTTY.clipboard.readText());
if (!path) {
setError(t(locale, "clipboardPathMissing"));
return;
}
setCwd(path);
setError(null);
} catch {
setError(t(locale, "clipboardReadFailed"));
}
};

const submit = async (): Promise<void> => {
Expand Down Expand Up @@ -88,14 +132,20 @@ export function AgentLaunchDialog({

<div className="launch-dialog__top">
<div className="launch-dialog__provider"><ProviderIcon provider={provider} size="large" /></div>
<button className="folder-field" type="button" onClick={() => void chooseDirectory()}>
<UiIcon name="folder" size={28} />
<span className="folder-field__copy">
<small>{t(locale, "projectFolder")}</small>
<strong title={cwd}>{cwd}</strong>
</span>
<UiIcon name="chevron" size={20} />
</button>
<div className="folder-field">
<button className="folder-field__picker" type="button" onClick={() => void chooseDirectory()}>
<UiIcon name="folder" size={28} />
<span className="folder-field__copy">
<small>{t(locale, "projectFolder")}</small>
<strong title={cwd}>{cwd}</strong>
</span>
<UiIcon name="chevron" size={20} />
</button>
<button className="folder-field__paste" type="button" onClick={() => void pasteDirectory()} title={t(locale, "pasteProjectPath")} aria-label={t(locale, "pasteProjectPath")}>
<UiIcon name="copy" size={20} />
<span>{t(locale, "pastePath")}</span>
</button>
</div>
</div>

<div className="profile-row">
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@ const ru = {
launchAgent: "Запустить агента",
projectFolder: "Папка проекта",
chooseFolder: "Выбрать папку",
pastePath: "Вставить путь",
pasteProjectPath: "Вставить путь к проекту из буфера обмена",
clipboardPathMissing: "В буфере обмена нет пути к папке проекта.",
clipboardReadFailed: "Не удалось прочитать буфер обмена.",
normal: "Обычный",
yolo: "YOLO",
launch: "Запустить",
Expand Down Expand Up @@ -633,6 +637,10 @@ const en: Record<keyof typeof ru, string> = {
launchAgent: "Launch agent",
projectFolder: "Project folder",
chooseFolder: "Choose folder",
pastePath: "Paste path",
pasteProjectPath: "Paste project path from clipboard",
clipboardPathMissing: "The clipboard does not contain a project folder path.",
clipboardReadFailed: "Could not read the clipboard.",
normal: "Normal",
yolo: "YOLO",
launch: "Launch",
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/styles/app.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions tests/agent-launch-dialog.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

const dialogPath = new URL("../src/renderer/src/features/launcher/AgentLaunchDialog.tsx", import.meta.url);

test("agent launcher can import its project path from the clipboard", async () => {
const source = await readFile(dialogPath, "utf8");

assert.match(source, /window\.canvasTTY\.clipboard\.readText\(\)/);
assert.match(source, /directoryPathFromClipboard/);
assert.match(source, /file:\/\//);
assert.match(source, /folder-field__paste/);
assert.match(source, /aria-label=\{t\(locale, "pasteProjectPath"\)\}/);
});