From d217e3fd090200cc2b812a03247214a6351222d2 Mon Sep 17 00:00:00 2001 From: TroopJostle Date: Thu, 3 Sep 2026 14:52:41 +0400 Subject: [PATCH] feat: paste project paths into launcher --- .../features/launcher/AgentLaunchDialog.tsx | 68 ++++++++++++++++--- src/renderer/src/lib/i18n.ts | 8 +++ src/renderer/src/styles/app.css | 5 +- tests/agent-launch-dialog.test.mjs | 15 ++++ 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 tests/agent-launch-dialog.test.mjs diff --git a/src/renderer/src/features/launcher/AgentLaunchDialog.tsx b/src/renderer/src/features/launcher/AgentLaunchDialog.tsx index d268335..101c064 100644 --- a/src/renderer/src/features/launcher/AgentLaunchDialog.tsx +++ b/src/renderer/src/features/launcher/AgentLaunchDialog.tsx @@ -17,6 +17,33 @@ interface AgentLaunchDialogProps { onLaunch(provider: AgentProviderId, profile: LaunchProfileId, cwd: string): Promise; } +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, @@ -55,7 +82,24 @@ export function AgentLaunchDialog({ const chooseDirectory = async (): Promise => { const selected = await window.canvasTTY.dialog.pickDirectory(cwd); - if (selected) setCwd(selected); + if (selected) { + setCwd(selected); + setError(null); + } + }; + + const pasteDirectory = async (): Promise => { + 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 => { @@ -88,14 +132,20 @@ export function AgentLaunchDialog({
- +
+ + +
diff --git a/src/renderer/src/lib/i18n.ts b/src/renderer/src/lib/i18n.ts index 9687e5a..8fe6a10 100644 --- a/src/renderer/src/lib/i18n.ts +++ b/src/renderer/src/lib/i18n.ts @@ -216,6 +216,10 @@ const ru = { launchAgent: "Запустить агента", projectFolder: "Папка проекта", chooseFolder: "Выбрать папку", + pastePath: "Вставить путь", + pasteProjectPath: "Вставить путь к проекту из буфера обмена", + clipboardPathMissing: "В буфере обмена нет пути к папке проекта.", + clipboardReadFailed: "Не удалось прочитать буфер обмена.", normal: "Обычный", yolo: "YOLO", launch: "Запустить", @@ -633,6 +637,10 @@ const en: Record = { 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", diff --git a/src/renderer/src/styles/app.css b/src/renderer/src/styles/app.css index bd65b33..881787c 100644 --- a/src/renderer/src/styles/app.css +++ b/src/renderer/src/styles/app.css @@ -440,10 +440,13 @@ button { border: 0; } .launch-dialog__top { display: grid; grid-template-columns: 108px 1fr; gap: 14px; } .launch-dialog__provider { min-height: 108px; display: grid; place-items: center; border-radius: 18px; background: rgba(255,255,255,.5); } .launch-dialog__provider .provider-icon { width: 76px; height: 76px; } -.folder-field { min-width: 0; padding: 18px 25px; display: grid; grid-template-columns: 34px minmax(0, 1fr) 22px; align-items: center; gap: 16px; border-radius: 18px; color: var(--text-dark); background: rgba(255,255,255,.64); cursor: pointer; text-align: left; } +.folder-field { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; overflow: hidden; border-radius: 18px; color: var(--text-dark); background: rgba(255,255,255,.64); } +.folder-field__picker { min-width: 0; padding: 18px 20px 18px 25px; display: grid; grid-template-columns: 34px minmax(0, 1fr) 22px; align-items: center; gap: 16px; color: inherit; background: transparent; cursor: pointer; text-align: left; } .folder-field__copy { min-width: 0; display: flex; flex-direction: column; gap: 7px; } .folder-field small { font-size: 10px; font-weight: 900; letter-spacing: .11em; text-transform: uppercase; opacity: .58; } .folder-field strong { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 18px; } +.folder-field__paste { min-width: 100px; padding: 0 18px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 7px; border-left: 1px solid rgba(36,38,48,.12); color: inherit; background: rgba(255,255,255,.2); cursor: pointer; font-size: 10px; font-weight: 900; letter-spacing: .07em; text-transform: uppercase; } +.folder-field__picker:hover, .folder-field__paste:hover, .folder-field__picker:focus-visible, .folder-field__paste:focus-visible { background: rgba(255,255,255,.34); } .profile-row { margin-top: 13px; display: grid; grid-template-columns: 1fr 1fr 96px; gap: 9px; } .profile-button, .launch-submit { min-height: 70px; border-radius: 16px; cursor: pointer; font-weight: 900; transition: transform .15s ease, background .15s ease; } .profile-button { color: var(--text-dark); background: rgba(255,255,255,.38); } diff --git a/tests/agent-launch-dialog.test.mjs b/tests/agent-launch-dialog.test.mjs new file mode 100644 index 0000000..c20808d --- /dev/null +++ b/tests/agent-launch-dialog.test.mjs @@ -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"\)\}/); +});