diff --git a/packages/app/e2e/regression/new-session.spec.ts b/packages/app/e2e/regression/new-session.spec.ts index 3b9d8730f29..339116a1d95 100644 --- a/packages/app/e2e/regression/new-session.spec.ts +++ b/packages/app/e2e/regression/new-session.spec.ts @@ -1,7 +1,7 @@ import { expect, test, type Locator, type Page } from "@playwright/test" import type { OpenCodeEvent, WorktreeDirectory } from "@opencode/client/promise" -import { draftHref, expectPath, provider, sessionHref } from "../utils/app" -import type { MockAnswer } from "../utils/mock-server" +import { draftHref, expectPath, NO_PROVIDER, project, provider, seed, sessionHref, T0 } from "../utils/app" +import { mockOpenCodeServer, type MockAnswer } from "../utils/mock-server" import { newSessionLocation, openDraft, openSession, openWorktreeDraft, type WorkspaceInput } from "../utils/workspace" const directory = "C:/OpenCode/WorkspacePending" @@ -406,6 +406,90 @@ test("executes a selected slash command after creating its worktree", async ({ p expect(mock.prompts).toEqual([]) }) +test("the location dialog lists every ready server's projects in one list and tags the remote ones", async ({ + page, +}) => { + const apollo = "http://apollo.test:4096" + const entry = (id: string, directory: string, active: number) => + project({ id, directory, time: { created: T0, updated: T0, active: T0 + active } }) + const remote = entry("proj_apollo_opencode", "/home/dax/opencode", 4) + const files: string[] = [] + page.on("request", (request) => { + const url = new URL(request.url()) + + if (url.origin === apollo && url.pathname === "/api/fs/list") files.push(url.pathname) + }) + await mockOpenCodeServer(page, { + server: apollo, + directory: remote.worktree, + project: remote, + projects: () => [remote, entry("proj_apollo_sst", "/home/dax/sst", 3)], + worktrees: () => [{ directory: remote.worktree }, { directory: "/home/dax/opencode-beta", strategy: "git" }], + provider: NO_PROVIDER, + sessions: [], + pageMessages: () => ({ items: [] }), + fileList: () => [], + }) + // hailmary is listed but unreachable, so it contributes no projects and cannot take a new one. + await seed(page, { + servers: [ + { url: apollo, name: "apollo" }, + { url: "http://127.0.0.1:4999", name: "hailmary" }, + ], + usedProjects: { [apollo]: [remote.id] }, + }) + const local = entry("proj_opencode", "C:/work/opencode", 2) + await openDraft(page, { + name: "opencode", + directory: local.worktree, + project: { time: local.time }, + projects: () => [local, entry("proj_console", "C:/work/console", 9)], + }) + + await newSessionLocation(page).click() + const dialog = page.getByRole("dialog") + const search = dialog.getByRole("textbox") + await expect(search).toBeFocused() + const options = dialog.getByRole("option") + const names = options.locator(".new-session-location-name") + const paths = options.locator(".command-palette-description") + const chip = dialog.getByRole("button", { name: "Back to projects", exact: true }) + // Used projects first, then the rest, each by recent activity across servers; only remote ones carry a tag. + await expect(names).toHaveText(["opencodeapollo", "opencode", "console", "sstapollo"]) + await expect(paths).toHaveText(["/home/dax/opencode", "C:/work/opencode", "C:/work/console", "/home/dax/sst"]) + await expect(options.last()).toHaveText("Add project") + // It opens on the first row, not on the draft's current project (the second row). + await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") + + await search.fill("apollo") + await expect(names).toHaveText(["opencodeapollo", "sstapollo"]) + + // The worktree step keeps the server's tag beside the project. + await search.fill("opencode") + await expect(paths).toHaveText(["/home/dax/opencode", "C:/work/opencode"]) + await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") + await page.keyboard.press("ArrowRight") + await expect(chip).toContainText("opencode") + await expect(chip.locator('[data-component="tag"]')).toHaveText("apollo") + await expect(options.locator(".command-palette-title")).toHaveText(["opencode", "opencode-beta", "New worktree"]) + await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") + await page.keyboard.press("Backspace") + await expect(search).toHaveValue("opencode") + await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") + + // Adding a project picks one of the ready servers first, then browses that server. + await search.fill("") + await expect(options.nth(0)).toHaveAttribute("aria-selected", "true") + await page.keyboard.press("ArrowUp") + await expect(options.nth(4)).toHaveAttribute("aria-selected", "true") + await page.keyboard.press("ArrowRight") + await expect(chip).toHaveText("Add project") + await expect(options).toHaveText([/^127\.0\.0\.1:\d+$/, "apollo"]) + await options.filter({ hasText: "apollo" }).click() + await expect(page.getByRole("button", { name: "Select folder", exact: true })).toBeEnabled() + await expect.poll(() => files.length).toBeGreaterThan(0) +}) + const accentRoot = "C:/OpenCode/WorkspaceAccent" const accentWorkspace = `${accentRoot}/.worktrees/feature` diff --git a/packages/app/src/new-session/location/dialog.css b/packages/app/src/new-session/location/dialog.css index d36ab9b4358..37efdecc2e9 100644 --- a/packages/app/src/new-session/location/dialog.css +++ b/packages/app/src/new-session/location/dialog.css @@ -37,3 +37,32 @@ align-items: center; gap: 6px; } + +/* The project name truncates before its server tag. */ +.new-session-location-name { + display: flex; + min-width: 0; + max-width: 100%; + align-items: center; + gap: 6px; +} + +.new-session-location-tag { + max-width: 160px; + flex-shrink: 0; + gap: 3px; +} + +.new-session-location-tag [data-slot="icon-svg"] { + width: 12px; + height: 12px; + flex-shrink: 0; +} + +.new-session-location-tag > span { + min-width: 0; + overflow: hidden; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/packages/app/src/new-session/location/dialog.tsx b/packages/app/src/new-session/location/dialog.tsx index dfca1dd4d8b..088adb4d14f 100644 --- a/packages/app/src/new-session/location/dialog.tsx +++ b/packages/app/src/new-session/location/dialog.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@opencode/ui/badge" import { Dialog, DialogBody } from "@opencode/ui/dialog" import { useDialog } from "@opencode/ui/context/dialog" import { Icon } from "@opencode/ui/icon" @@ -10,10 +11,10 @@ import { } from "@opencode/ui/project-avatar" import { ScrollView } from "@opencode/ui/scroll-view" import { TextInput } from "@opencode/ui/text-input" -import { getFilename, sameDirectory } from "@opencode/util/path" -import { createEffect, createMemo, createResource, For, Match, onCleanup, Show, Switch } from "solid-js" +import { getFilename } from "@opencode/util/path" +import { createEffect, createMemo, createResource, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" -import type { PromptProject, PromptProjectController } from "@/new-session/project/selector" +import type { PromptProject, PromptProjectController, PromptServer } from "@/new-session/project/selector" import type { NewSessionWorkspaceController } from "@/new-session/workspace/controller" import { useLanguage } from "@/runtime/i18n/language" import { useServerSDK } from "@/runtime/server/client" @@ -24,11 +25,15 @@ import "@/shell/commands/dialog.css" import "./dialog.css" type LocationEntry = - | { id: string; type: "project"; group: string; project: PromptProject } - | { id: string; type: "add"; group: string; server?: string } - | { id: string; type: "workspace"; group: string; value: string; directory: string } + | { id: string; type: "project"; project: PromptProject } + | { id: string; type: "add" } + | { id: string; type: "server"; server: PromptServer } + | { id: string; type: "workspace"; value: string; directory: string } -/** Opens the new-session location dialog: projects first, and → on a Git project lists where the session can run. */ +/** + * Opens the new-session location dialog: one project list across every ready server, → on a Git project lists where + * the session can run, and → on "Add project" picks the server when there are several. + */ export function useNewSessionLocationDialog(input: { project: PromptProjectController workspace: NewSessionWorkspaceController @@ -62,9 +67,11 @@ function NewSessionLocationDialog(props: { const serverSDK = useServerSDK() const [store, setStore] = createStore<{ drilled?: PromptProject + /** Picking the server to add a project to. */ + picking?: boolean query: string active?: string - /** The projects view to restore when stepping back out of a project. */ + /** The projects view to restore when stepping back out of a project or the server step. */ back?: { query: string; active: string } }>({ query: "" }) @@ -97,83 +104,68 @@ function NewSessionLocationDialog(props: { return !query || values.some((value) => value?.toLowerCase().includes(query)) } - const projectEntries = createMemo(() => { - const grouped = props.project.servers().length > 1 - const projects = props.project + // Remote projects also match their server's name, which their tag shows. + const projectEntries = createMemo(() => [ + ...props.project .projects() - .filter((project) => matches(displayName(project), project.worktree)) - .map((project) => ({ - id: `project:${project.server?.key ?? ""}:${project.worktree}`, - type: "project" as const, - group: grouped ? project.server!.name : language.t("home.projects"), - project, - })) + .filter((project) => matches(displayName(project), project.worktree, tagged(project.server)?.name)) + .map((project) => ({ id: projectEntryID(project), type: "project" as const, project })), + { id: "add", type: "add" as const }, + ]) - if (!grouped) return [...projects, { id: "add", type: "add" as const, group: language.t("home.projects") }] - - // Projects keep their server grouping; adding a project stays one row per server at the end. - return [ - ...props.project - .servers() - .flatMap((server) => projects.filter((entry) => entry.project.server?.key === server.key)), - ...props.project.servers().map((server) => ({ - id: `add:${server.key}`, - type: "add" as const, - group: language.t("session.new.project.add"), - server: server.key, - })), - ] - }) + const serverEntries = createMemo(() => + props.project + .servers() + .filter((server) => matches(server.name)) + .map((server) => ({ id: `server:${server.key}`, type: "server" as const, server })), + ) const workspaceEntries = createMemo(() => { const project = store.drilled if (!project) return [] - const group = language.t("session.new.workspace.runIn") const directories = workspaceSelectionDirectories(project, listed.latest ?? project.worktrees ?? []) return [ - { id: "main", type: "workspace" as const, group, value: "main", directory: project.worktree }, + { id: "main", type: "workspace" as const, value: "main", directory: project.worktree }, ...directories.map((directory) => ({ id: `worktree:${directory}`, type: "workspace" as const, - group, value: directory, directory, })), - { id: "create", type: "workspace" as const, group, value: "create", directory: project.worktree }, + { id: "create", type: "workspace" as const, value: "create", directory: project.worktree }, ].filter((entry) => matches(workspaceTitle(project, entry.value), entry.directory)) }) - const entries = () => (store.drilled ? workspaceEntries() : projectEntries()) - const groups = createMemo(() => - entries().reduce<{ group: string; entries: LocationEntry[] }[]>((all, entry) => { - const last = all.at(-1) + const entries = () => { + if (store.drilled) return workspaceEntries() - if (last?.group === entry.group) last.entries.push(entry) + if (store.picking) return serverEntries() - if (last?.group !== entry.group) all.push({ group: entry.group, entries: [entry] }) + return projectEntries() + } - return all - }, []), - ) + const title = () => { + if (store.drilled) return language.t("session.new.workspace.runIn") - const currentEntry = () => - entries().find((entry) => { - if (entry.type === "project") return props.project.isSelected(entry.project) + if (store.picking) return language.t("session.new.location.servers") - if (entry.type === "workspace") return isCurrentWorkspace(entry) + return language.t("home.projects") + } - return false - }) + const placeholder = () => { + if (store.drilled) return language.t("session.new.workspace.search.placeholder") - // Follow the current location, then the first result, until the user explicitly chooses a row. - const activeEntry = createMemo( - () => - entries().find((entry) => entry.id === store.active) ?? - (store.query.trim() ? undefined : currentEntry()) ?? - entries()[0], - ) + if (store.picking) return language.t("session.new.location.servers.search") + + return language.t("session.new.project.search") + } + + const choosesServer = () => props.project.servers().length > 1 + + // Every step opens on its first row; the user's choice wins until the results change. + const activeEntry = createMemo(() => entries().find((entry) => entry.id === store.active) ?? entries()[0]) let resultsRef: HTMLDivElement | undefined let inputRef: HTMLInputElement | undefined @@ -194,32 +186,27 @@ function NewSessionLocationDialog(props: { return getFilename(value) } - function isCurrentWorkspace(entry: Extract) { - if (!store.drilled || !props.project.isSelected(store.drilled)) return false - const current = props.workspace.selection.value() - - if (entry.value === "main" || entry.value === "create") return current === entry.value - - return current !== "main" && current !== "create" && sameDirectory(current, entry.value) - } - function drill(project: PromptProject) { setStore({ drilled: project, query: "", active: undefined, - back: { query: store.query, active: `project:${project.server?.key ?? ""}:${project.worktree}` }, + back: { query: store.query, active: projectEntryID(project) }, }) inputRef?.focus() } - function back() { - const project = store.drilled + function pick() { + setStore({ picking: true, query: "", active: undefined, back: { query: store.query, active: "add" } }) + inputRef?.focus() + } + function back() { setStore({ drilled: undefined, + picking: undefined, query: store.back?.query ?? "", - active: store.back?.active ?? (project ? `project:${project.server?.key ?? ""}:${project.worktree}` : undefined), + active: store.back?.active, back: undefined, }) inputRef?.focus() @@ -228,9 +215,22 @@ function NewSessionLocationDialog(props: { function choose(entry: LocationEntry | undefined) { if (!entry) return + if (entry.type === "add" && choosesServer()) { + pick() + + return + } + if (entry.type === "add") { dialog.close() - props.project.add(entry.server) + props.project.add() + + return + } + + if (entry.type === "server") { + dialog.close() + props.project.add(entry.server.key) return } @@ -288,7 +288,17 @@ function NewSessionLocationDialog(props: { return } - if (store.drilled && ((event.key === "ArrowLeft" && atStart) || (event.key === "Backspace" && !target.value))) { + if (event.key === "ArrowRight" && atEnd && active?.type === "add" && choosesServer()) { + event.preventDefault() + pick() + + return + } + + if ( + (store.drilled || store.picking) && + ((event.key === "ArrowLeft" && atStart) || (event.key === "Backspace" && !target.value)) + ) { event.preventDefault() back() } @@ -305,36 +315,31 @@ function NewSessionLocationDialog(props: { autocomplete="off" spellcheck={false} appearance="large" - aria-label={ - store.drilled - ? language.t("session.new.workspace.search.placeholder") - : language.t("session.new.project.search") - } - placeholder={ - store.drilled - ? language.t("session.new.workspace.search.placeholder") - : language.t("session.new.project.search") - } + aria-label={placeholder()} + placeholder={placeholder()} leadingIcon={ - }> - {(project) => ( - - )} - + }> + + {(project) => ( + + + {displayName(project())} + + + )} + + + + + {language.t("session.new.project.add")} + + + } onInput={(event) => setStore({ query: event.currentTarget.value, active: undefined })} onKeyDown={handleKeyDown} @@ -350,72 +355,74 @@ function NewSessionLocationDialog(props: { } > - - {(group) => ( -
-
{group.group}
- - {(entry) => ( - - )} - -
- )} -
+
+
{title()}
+ + {(entry) => ( + + )} + +
@@ -425,11 +432,13 @@ function NewSessionLocationDialog(props: { {language.t("session.new.location.select")} - {language.t("session.new.location.worktrees")} + {activeEntry()?.type === "add" && choosesServer() + ? language.t("session.new.location.servers") + : language.t("session.new.location.worktrees")} } > @@ -455,31 +464,64 @@ function ProjectRow(props: { project: PromptProject; path: string; onDrill?: () variant={getProjectAvatarVariant(props.project.icon?.color)} />
- {displayName(props.project)} + + {displayName(props.project)} + + {props.path}
- - {(onDrill) => ( - { - event.stopPropagation() - onDrill()() - }} - > - - - )} - + {(onDrill) => } ) } +function DrillButton(props: { label: string; onDrill: () => void }) { + return ( + { + event.stopPropagation() + props.onDrill() + }} + > + + + ) +} + +function BackChip(props: { label: string; onBack: () => void; children: JSX.Element }) { + return ( + + ) +} + +/** The server a remote project lives on; local projects have none. */ +function ServerTag(props: { server?: PromptServer }) { + return ( + + {(server) => ( + + + {server().name} + + )} + + ) +} + function WorkspaceRow(props: { title: string; value: string; description: string }) { return ( <> @@ -503,3 +545,17 @@ function WorkspaceRow(props: { title: string; value: string; description: string function drillable(project: PromptProject) { return !!project.id && project.vcs === "git" } + +function projectEntryID(project: PromptProject) { + return `project:${project.server?.key ?? ""}:${project.worktree}` +} + +function tagged(server?: PromptServer) { + return server?.kind === "local" ? undefined : server +} + +function serverIcon(server: PromptServer) { + if (server.kind === "local") return "monitor" + + return server.kind === "extension" ? "terminal" : "globe" +} diff --git a/packages/app/src/new-session/project/controller.ts b/packages/app/src/new-session/project/controller.ts index a3a47f2d771..8b07cd1f81a 100644 --- a/packages/app/src/new-session/project/controller.ts +++ b/packages/app/src/new-session/project/controller.ts @@ -6,7 +6,7 @@ import { serverName, ServerConnection, useServers } from "@/runtime/server/regis import { useWorkspaceLocation } from "@/workspaces/location" import { workspaceSelectionDestination } from "@/workspaces/paths" import { useTabs } from "@/shell/tabs/tabs" -import type { PromptProjectControls } from "./selector" +import type { PromptProjectControls, PromptServer } from "./selector" export function createComposerProjectControls(props: { draftId: string; worktree: () => string }) { const servers = useServers() @@ -18,22 +18,33 @@ export function createComposerProjectControls(props: { draftId: string; worktree const projectServer = () => serverSDK.server const projectServerCtx = useServerCtx(projectServer) - // Every server project, one per directory: the ones this client has used first, then the rest, each by recency. - const ordered = (ctx: ServerCtx) => { + // Every server project, one per directory, marked as used by this client or not; the selector orders them. + const listed = (ctx: ServerCtx, server?: PromptServer) => { const used = ctx.projects.used() const ids = new Set(used.map((project) => project.id)) - return [...used, ...ctx.projects.unique().filter((project) => !ids.has(project.id))] + return [ + ...used.map((project) => ({ ...project, used: true, server })), + ...ctx.projects + .unique() + .filter((project) => !ids.has(project.id)) + .map((project) => ({ ...project, used: false, server })), + ] } + // Servers that are down or still connecting contribute nothing. + const ready = createMemo(() => + servers.list.filter((connection) => { + if (connection.type === "extension" && connection.state !== "ready") return false + + return global.servers.health[ServerConnection.key(connection)]?.healthy !== false + }), + ) + const projects = createMemo(() => { - if (servers.list.length <= 1) return ordered(projectServerCtx()) + if (servers.list.length <= 1) return listed(projectServerCtx()) - return servers.list.flatMap((connection) => { - const server = { key: ServerConnection.key(connection), name: serverName(connection) } - - return ordered(global.ensureServerCtx(connection)).map((project) => ({ ...project, server })) - }) + return ready().flatMap((connection) => listed(global.ensureServerCtx(connection), promptServer(connection))) }) const selectProject = (directory: string, serverKey?: string, workspace?: string) => { @@ -74,6 +85,7 @@ export function createComposerProjectControls(props: { draftId: string; worktree return createMemo(() => ({ available: projects(), + servers: servers.list.length > 1 ? ready().map(promptServer) : undefined, directory: location().directory, projectID: location().current?.project.id, server: servers.list.length > 1 ? ServerConnection.key(projectServer()) : undefined, @@ -81,3 +93,12 @@ export function createComposerProjectControls(props: { draftId: string; worktree add: addProject, })) } + +function promptServer(connection: ServerConnection.Any): PromptServer { + return { + key: ServerConnection.key(connection), + name: serverName(connection), + kind: ServerConnection.local(connection) ? "local" : connection.type === "extension" ? "extension" : "remote", + label: connection.label, + } +} diff --git a/packages/app/src/new-session/project/selector.test.tsx b/packages/app/src/new-session/project/selector.test.tsx index a8886482ad8..3e3bc280d9f 100644 --- a/packages/app/src/new-session/project/selector.test.tsx +++ b/packages/app/src/new-session/project/selector.test.tsx @@ -33,8 +33,12 @@ describe("new session project selection", () => { }) test("does not select a matching project ID from another server", () => { - const remote = { id: "shared", worktree: "/remote", server: { key: "remote", name: "Remote" } } - const local = { id: "shared", worktree: "/local", server: { key: "local", name: "Local" } } + const remote = { + id: "shared", + worktree: "/remote", + server: { key: "remote", name: "Remote", kind: "remote" as const }, + } + const local = { id: "shared", worktree: "/local", server: { key: "local", name: "Local", kind: "local" as const } } const controls: PromptProjectControls = { available: [remote, local], @@ -51,4 +55,43 @@ describe("new session project selection", () => { dispose() }) }) + + test("lists every server's projects in one list: used first, then the rest, each by recent activity", () => { + const local = { key: "local", name: "Local Server", kind: "local" as const } + const apollo = { key: "http://apollo:4096", name: "apollo", kind: "remote" as const } + const entry = (id: string, server: typeof local | typeof apollo, active: number, used: boolean) => ({ + id, + worktree: `/${id}`, + server, + used, + time: { active }, + }) + + // The same directory on two servers stays two rows. + const controls: PromptProjectControls = { + available: [ + entry("local-unused", local, 9, false), + entry("local-used", local, 2, true), + entry("opencode", local, 1, true), + entry("apollo-unused", apollo, 5, false), + entry("opencode", apollo, 3, true), + ], + directory: "/elsewhere", + server: "local", + select() {}, + add() {}, + } + + createRoot((dispose) => { + const controller = createPromptProjectController({ controls: () => controls }) + expect(controller.projects().map((project) => `${project.server?.name}:${project.id}`)).toEqual([ + "apollo:opencode", + "Local Server:local-used", + "Local Server:opencode", + "Local Server:local-unused", + "apollo:apollo-unused", + ]) + dispose() + }) + }) }) diff --git a/packages/app/src/new-session/project/selector.tsx b/packages/app/src/new-session/project/selector.tsx index c8e6ee98394..a3242e301dd 100644 --- a/packages/app/src/new-session/project/selector.tsx +++ b/packages/app/src/new-session/project/selector.tsx @@ -1,8 +1,17 @@ -import { type Accessor } from "solid-js" +import { type Accessor, createMemo } from "solid-js" import { Icon } from "@opencode/ui/icon" import { useLanguage } from "@/runtime/i18n/language" import { pathKey } from "@/workspaces/path-key" +/** A server listed in the location dialog; "local" servers get no tag. */ +export type PromptServer = { + key: string + name: string + kind: "local" | "remote" | "extension" + /** The extension's short badge, e.g. "SSH". */ + label?: string +} + export type PromptProject = { name?: string id?: string @@ -11,11 +20,16 @@ export type PromptProject = { sandboxes?: string[] worktrees?: { directory: string; strategy?: string }[] icon?: { color?: string; url?: string; override?: string } - server?: { key: string; name: string } + time?: { active: number } + /** This client has used the project on its server. */ + used?: boolean + server?: PromptServer } export type PromptProjectControls = { available: PromptProject[] + /** The ready servers a project can be added to, when more than one server is listed. */ + servers?: PromptServer[] directory: string projectID?: string server?: string @@ -40,7 +54,16 @@ export function createPromptProjectController(input: { controls: Accessor current() ?? input.controls().available[0] + // One list across servers: used projects first, then the rest, each by recent activity. + const projects = createMemo(() => + input + .controls() + .available.toSorted( + (a, b) => Number(!!b.used) - Number(!!a.used) || (b.time?.active ?? 0) - (a.time?.active ?? 0), + ), + ) + + const selected = () => current() ?? projects()[0] const matches = (project: PromptProject, other?: PromptProject) => !!other && pathKey(other.worktree) === pathKey(project.worktree) && other.server?.key === project.server?.key @@ -49,15 +72,8 @@ export function createPromptProjectController(input: { controls: Accessor matches(project, selected()), empty: () => input.controls().available.length === 0, - projects: () => input.controls().available, - servers: () => - input - .controls() - .available.map((project) => project.server) - .filter( - (server, index, all): server is NonNullable => - !!server && all.findIndex((item) => item?.key === server.key) === index, - ), + projects, + servers: () => input.controls().servers ?? [], labels: { add: () => language.t("session.new.project.add"), new: () => language.t("session.new.project.new"), diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index abbf094acc2..fd7030312a0 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -1030,6 +1030,8 @@ export const dict = { "session.new.location.select": "Select", "session.new.location.worktrees": "Worktrees", "session.new.location.back": "Back to projects", + "session.new.location.servers": "Servers", + "session.new.location.servers.search": "Search servers", "settings.tab.workspaces": "Worktrees", "settings.workspaces.description": "Review worktrees and manage disk usage", "settings.workspaces.filter.all": "All projects", diff --git a/packages/app/src/shell/commands/dialog.css b/packages/app/src/shell/commands/dialog.css index 284e2f9f8c0..9670fb76541 100644 --- a/packages/app/src/shell/commands/dialog.css +++ b/packages/app/src/shell/commands/dialog.css @@ -218,6 +218,7 @@ gap: 1px; } + .command-palette-title, .command-palette-description { max-width: 100%; }