From 475a4d18a44d45a5a0998fd605f3e2c506816d59 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 8 Jul 2026 21:03:34 +0000 Subject: [PATCH] feat(app): add Run button to titlebar for project startup commands --- packages/app/src/components/titlebar.tsx | 72 ++++++++++++++++++++++++ packages/app/src/i18n/ar.ts | 1 + packages/app/src/i18n/br.ts | 1 + packages/app/src/i18n/bs.ts | 1 + packages/app/src/i18n/da.ts | 1 + packages/app/src/i18n/de.ts | 1 + packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 1 + packages/app/src/i18n/fr.ts | 1 + packages/app/src/i18n/ja.ts | 1 + packages/app/src/i18n/ko.ts | 1 + packages/app/src/i18n/no.ts | 1 + packages/app/src/i18n/pl.ts | 1 + packages/app/src/i18n/ru.ts | 1 + packages/app/src/i18n/th.ts | 1 + packages/app/src/i18n/tr.ts | 1 + packages/app/src/i18n/uk.ts | 1 + packages/app/src/i18n/zh.ts | 2 +- packages/app/src/i18n/zht.ts | 1 + packages/ui/src/v2/components/icon.tsx | 4 ++ 20 files changed, 94 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 2a818d565da1..ef1486def6f6 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -25,6 +25,10 @@ import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/comp import { useGlobal } from "@/context/global" import { ServerConnection, useServer } from "@/context/server" import { tabKey, useTabs } from "@/context/tabs" +import { useServerSync } from "@/context/server-sync" +import { useSDK } from "@/context/sdk" +import { defaultTitle } from "@/context/terminal-title" +import { decode64 } from "@/utils/base64" import "./titlebar.css" import { newTabTooltipKeybind } from "./command-tooltip-keybind" @@ -444,6 +448,60 @@ export function Titlebar(props: { update?: TitlebarUpdate }) { const [tabsAreOverflowing, setTabsAreOverflowing] = createSignal(false) + const getCurrentProject = () => { + const route = layout.route() + if (route.type === "session" || route.type === "draft") { + const conn = global.servers + .list() + .find((item) => ServerConnection.key(item) === (route.server ?? server.key)) + if (!conn) return undefined + const serverCtx = global.ensureServerCtx(conn) + const worktree = route.type === "session" && session()?.directory + ? session()!.directory + : route.type === "draft" + ? global + .ensureServerCtx(conn) + .projects.list() + .find((p) => p.worktree === decode64(params.dir ?? ""))?.worktree + : undefined + if (!worktree) return undefined + return serverCtx.projects.list().find((p) => p.worktree === worktree) + } + return undefined + } + + const runProjectCommand = async () => { + const project = getCurrentProject() + if (!project?.commands?.start) return + const route = layout.route() + const conn = global.servers + .list() + .find((item) => ServerConnection.key(item) === (route.server ?? server.key)) + if (!conn) return + const sdk = global.ensureServerCtx(conn).sdk + const worktree = project.worktree + + const pty = await sdk.client.pty + .create({ + body: { + command: project.commands.start, + cwd: worktree, + title: `Run: ${project.commands.start}`, + }, + query: { directory: worktree }, + }) + .catch((error: unknown) => { + console.error("Failed to create terminal for run command", error) + return undefined + }) + + if (!pty?.data) return + layout.terminal.open() + } + + const currentProject = () => getCurrentProject() + const hasRunCommand = () => !!currentProject()?.commands?.start + return (
+ + + } + onClick={runProjectCommand} + aria-label={language.t("command.project.run")} + /> + + + `, }, + play: { + viewBox: "0 0 16 16", + body: ``, + }, "settings-gear": { viewBox: "0 0 16 16", body: ``,