Add configurable terminal profiles
This commit is contained in:
@@ -69,34 +69,46 @@ export async function pressNewTabShortcut(page: Page): Promise<void> {
|
|||||||
|
|
||||||
// ─── Tab bar assertions ───────────────────────────────────────────────────
|
// ─── Tab bar assertions ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Assert the new agent tab button is visible in the tab bar. */
|
/** Assert the inline new-agent plus button is visible in the tab bar. */
|
||||||
export async function assertNewChatTileVisible(page: Page): Promise<void> {
|
export async function assertNewChatTileVisible(page: Page): Promise<void> {
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("workspace-new-agent-tab").filter({ visible: true }).first(),
|
page.getByTestId("workspace-new-agent-tab-inline").filter({ visible: true }).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assert the new terminal button is visible in the tab bar. */
|
/** Assert the new-tab dropdown trigger is visible in the tab bar. */
|
||||||
export async function assertTerminalTileVisible(page: Page): Promise<void> {
|
export async function assertNewTabMenuTriggerVisible(page: Page): Promise<void> {
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("workspace-new-terminal").filter({ visible: true }).first(),
|
page.getByTestId("workspace-new-tab-menu-trigger").filter({ visible: true }).first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tab creation actions ─────────────────────────────────────────────────
|
// ─── Tab creation actions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Click the new agent tab button to create a draft/chat tab. */
|
/** Click the inline plus button to create a draft/chat tab. */
|
||||||
export async function clickNewChat(page: Page): Promise<void> {
|
export async function clickNewChat(page: Page): Promise<void> {
|
||||||
const button = page.getByTestId("workspace-new-agent-tab").filter({ visible: true }).first();
|
const button = page
|
||||||
|
.getByTestId("workspace-new-agent-tab-inline")
|
||||||
|
.filter({ visible: true })
|
||||||
|
.first();
|
||||||
await expect(button).toBeVisible({ timeout: 10_000 });
|
await expect(button).toBeVisible({ timeout: 10_000 });
|
||||||
await button.click();
|
await button.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Click the new terminal button to create a terminal tab. */
|
/** Open the new-tab menu and click "New terminal". */
|
||||||
export async function clickNewTerminal(page: Page): Promise<void> {
|
export async function clickNewTerminal(page: Page): Promise<void> {
|
||||||
const button = page.getByTestId("workspace-new-terminal").filter({ visible: true }).first();
|
const trigger = page
|
||||||
await expect(button).toBeVisible({ timeout: 10_000 });
|
.getByTestId("workspace-new-tab-menu-trigger")
|
||||||
await button.click();
|
.filter({ visible: true })
|
||||||
|
.first();
|
||||||
|
await expect(trigger).toBeVisible({ timeout: 10_000 });
|
||||||
|
await trigger.click();
|
||||||
|
const item = page
|
||||||
|
.getByTestId("workspace-new-tab-menu-terminal")
|
||||||
|
.filter({ visible: true })
|
||||||
|
.first();
|
||||||
|
await expect(item).toBeVisible({ timeout: 10_000 });
|
||||||
|
await item.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tab title assertions ──────────────────────────────────────────────────
|
// ─── Tab title assertions ──────────────────────────────────────────────────
|
||||||
@@ -117,9 +129,9 @@ export async function waitForTabWithTitle(
|
|||||||
).toBeVisible({ timeout });
|
).toBeVisible({ timeout });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assert the new agent tab button is visible in the tab bar. */
|
/** Assert the inline new-agent plus button is visible in the tab bar. */
|
||||||
export async function assertSingleNewTabButton(page: Page): Promise<void> {
|
export async function assertSingleNewTabButton(page: Page): Promise<void> {
|
||||||
const buttons = page.getByTestId("workspace-new-agent-tab").filter({ visible: true });
|
const buttons = page.getByTestId("workspace-new-agent-tab-inline").filter({ visible: true });
|
||||||
const count = await buttons.count();
|
const count = await buttons.count();
|
||||||
expect(count).toBeGreaterThanOrEqual(1);
|
expect(count).toBeGreaterThanOrEqual(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export async function waitForWorkspaceTabsVisible(page: Page): Promise<void> {
|
|||||||
await expect(visibleTestId(page, "workspace-tabs-row").first()).toBeVisible({
|
await expect(visibleTestId(page, "workspace-tabs-row").first()).toBeVisible({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
await expect(visibleTestId(page, "workspace-new-agent-tab").first()).toBeVisible({
|
await expect(visibleTestId(page, "workspace-new-agent-tab-inline").first()).toBeVisible({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { test, expect } from "./fixtures";
|
|||||||
import {
|
import {
|
||||||
gotoWorkspace,
|
gotoWorkspace,
|
||||||
assertNewChatTileVisible,
|
assertNewChatTileVisible,
|
||||||
assertTerminalTileVisible,
|
assertNewTabMenuTriggerVisible,
|
||||||
assertSingleNewTabButton,
|
assertSingleNewTabButton,
|
||||||
pressNewTabShortcut,
|
pressNewTabShortcut,
|
||||||
clickNewChat,
|
clickNewChat,
|
||||||
@@ -91,7 +91,7 @@ test.describe("Tab creation", () => {
|
|||||||
await gotoWorkspace(page, workspace.workspaceId);
|
await gotoWorkspace(page, workspace.workspaceId);
|
||||||
await assertSingleNewTabButton(page);
|
await assertSingleNewTabButton(page);
|
||||||
await assertNewChatTileVisible(page);
|
await assertNewChatTileVisible(page);
|
||||||
await assertTerminalTileVisible(page);
|
await assertNewTabMenuTriggerVisible(page);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ test.describe("Workspace agent title handoff", () => {
|
|||||||
|
|
||||||
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
|
await page.goto(buildHostWorkspaceRoute(getServerId(), workspace.workspaceId));
|
||||||
await waitForWorkspaceTabsVisible(page);
|
await waitForWorkspaceTabsVisible(page);
|
||||||
await page.getByTestId("workspace-new-agent-tab").click();
|
await page.getByTestId("workspace-new-agent-tab-inline").click();
|
||||||
await expectComposerVisible(page);
|
await expectComposerVisible(page);
|
||||||
|
|
||||||
const promptTitle = "Investigate optimistic tab title handoff";
|
const promptTitle = "Investigate optimistic tab title handoff";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Vendored ACP provider SVG assets.
|
// Vendored ACP provider SVG assets.
|
||||||
|
|
||||||
export const ACP_PROVIDER_ICON_SVGS = {
|
export const ACP_PROVIDER_ICON_SVGS = {
|
||||||
|
agy: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd">\n <path d="M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z"/>\n</svg>\n',
|
||||||
"agoragentic-acp":
|
"agoragentic-acp":
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">\n <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>\n <path d="M5.5 10.5L8 4L10.5 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>\n <line x1="6.25" y1="8.75" x2="9.75" y2="8.75" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>\n</svg>\n',
|
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">\n <circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.5"/>\n <path d="M5.5 10.5L8 4L10.5 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>\n <line x1="6.25" y1="8.75" x2="9.75" y2="8.75" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>\n</svg>\n',
|
||||||
"amp-acp":
|
"amp-acp":
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
BUILTIN_PROVIDER_ICON_NAMES,
|
||||||
|
KNOWN_PROVIDER_ICON_NAMES,
|
||||||
|
TERMINAL_PROFILE_ICON_NAMES,
|
||||||
|
} from "@getpaseo/protocol/provider-icon-names";
|
||||||
|
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
|
||||||
import { resolveProviderIconName } from "./provider-icon-name";
|
import { resolveProviderIconName } from "./provider-icon-name";
|
||||||
|
|
||||||
describe("resolveProviderIconName", () => {
|
describe("resolveProviderIconName", () => {
|
||||||
@@ -10,9 +16,34 @@ describe("resolveProviderIconName", () => {
|
|||||||
|
|
||||||
it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => {
|
it("returns the catalog identifier for ACP catalog provider ids that ship an icon", () => {
|
||||||
expect(resolveProviderIconName("amp-acp")).toEqual({ kind: "catalog", id: "amp-acp" });
|
expect(resolveProviderIconName("amp-acp")).toEqual({ kind: "catalog", id: "amp-acp" });
|
||||||
|
expect(resolveProviderIconName("gemini")).toEqual({ kind: "catalog", id: "gemini" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to the bot icon for unknown custom providers", () => {
|
it("falls back to the bot icon for unknown custom providers", () => {
|
||||||
expect(resolveProviderIconName("custom-claude-profile")).toEqual({ kind: "bot" });
|
expect(resolveProviderIconName("custom-claude-profile")).toEqual({ kind: "bot" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("known provider icon names", () => {
|
||||||
|
it("includes every ACP catalog entry that ships an icon", () => {
|
||||||
|
const known = new Set(KNOWN_PROVIDER_ICON_NAMES);
|
||||||
|
for (const entry of ACP_PROVIDER_CATALOG) {
|
||||||
|
if (entry.iconSvg) {
|
||||||
|
expect(known).toContain(entry.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only lists ACP icon ids that have a catalog entry with an icon", () => {
|
||||||
|
const builtin = new Set(BUILTIN_PROVIDER_ICON_NAMES);
|
||||||
|
const terminalOnly = new Set(TERMINAL_PROFILE_ICON_NAMES);
|
||||||
|
const catalogIdsWithIcons = new Set(
|
||||||
|
ACP_PROVIDER_CATALOG.filter((entry) => entry.iconSvg).map((entry) => entry.id),
|
||||||
|
);
|
||||||
|
for (const name of KNOWN_PROVIDER_ICON_NAMES) {
|
||||||
|
if (!builtin.has(name) && !terminalOnly.has(name)) {
|
||||||
|
expect(catalogIdsWithIcons).toContain(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,38 +1,21 @@
|
|||||||
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
|
import {
|
||||||
|
BUILTIN_PROVIDER_ICON_NAMES,
|
||||||
export type BuiltinProviderIconName =
|
KNOWN_PROVIDER_ICON_NAMES,
|
||||||
| "claude"
|
} from "@getpaseo/protocol/provider-icon-names";
|
||||||
| "codex"
|
|
||||||
| "copilot"
|
|
||||||
| "kiro"
|
|
||||||
| "omp"
|
|
||||||
| "opencode"
|
|
||||||
| "pi";
|
|
||||||
|
|
||||||
export type ProviderIconName =
|
export type ProviderIconName =
|
||||||
| { kind: "builtin"; id: BuiltinProviderIconName }
|
| { kind: "builtin"; id: string }
|
||||||
| { kind: "catalog"; id: string }
|
| { kind: "catalog"; id: string }
|
||||||
| { kind: "bot" };
|
| { kind: "bot" };
|
||||||
|
|
||||||
const BUILTIN_PROVIDER_IDS: ReadonlySet<BuiltinProviderIconName> = new Set([
|
const BUILTIN_PROVIDER_IDS = new Set(BUILTIN_PROVIDER_ICON_NAMES);
|
||||||
"claude",
|
const KNOWN_PROVIDER_IDS = new Set(KNOWN_PROVIDER_ICON_NAMES);
|
||||||
"codex",
|
|
||||||
"copilot",
|
|
||||||
"kiro",
|
|
||||||
"omp",
|
|
||||||
"opencode",
|
|
||||||
"pi",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const CATALOG_ICON_PROVIDER_IDS: ReadonlySet<string> = new Set(
|
|
||||||
ACP_PROVIDER_CATALOG.flatMap((entry) => (entry.iconSvg ? [entry.id] : [])),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function resolveProviderIconName(provider: string): ProviderIconName {
|
export function resolveProviderIconName(provider: string): ProviderIconName {
|
||||||
if (BUILTIN_PROVIDER_IDS.has(provider as BuiltinProviderIconName)) {
|
if (BUILTIN_PROVIDER_IDS.has(provider)) {
|
||||||
return { kind: "builtin", id: provider as BuiltinProviderIconName };
|
return { kind: "builtin", id: provider };
|
||||||
}
|
}
|
||||||
if (CATALOG_ICON_PROVIDER_IDS.has(provider)) {
|
if (KNOWN_PROVIDER_IDS.has(provider)) {
|
||||||
return { kind: "catalog", id: provider };
|
return { kind: "catalog", id: provider };
|
||||||
}
|
}
|
||||||
return { kind: "bot" };
|
return { kind: "bot" };
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import { OpenCodeIcon } from "@/components/icons/opencode-icon";
|
|||||||
import { OmpIcon } from "@/components/icons/omp-icon";
|
import { OmpIcon } from "@/components/icons/omp-icon";
|
||||||
import { PiIcon } from "@/components/icons/pi-icon";
|
import { PiIcon } from "@/components/icons/pi-icon";
|
||||||
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
|
import { ACP_PROVIDER_CATALOG } from "@/data/acp-provider-catalog";
|
||||||
import {
|
import { resolveProviderIconName } from "@/components/provider-icon-name";
|
||||||
resolveProviderIconName,
|
|
||||||
type BuiltinProviderIconName,
|
|
||||||
} from "@/components/provider-icon-name";
|
|
||||||
|
|
||||||
export interface ProviderIconProps {
|
export interface ProviderIconProps {
|
||||||
size: number;
|
size: number;
|
||||||
@@ -20,7 +17,7 @@ export interface ProviderIconProps {
|
|||||||
|
|
||||||
export type ProviderIconComponent = ComponentType<ProviderIconProps>;
|
export type ProviderIconComponent = ComponentType<ProviderIconProps>;
|
||||||
|
|
||||||
const BUILTIN_PROVIDER_ICONS: Record<BuiltinProviderIconName, ProviderIconComponent> = {
|
const BUILTIN_PROVIDER_ICONS: Record<string, ProviderIconComponent> = {
|
||||||
claude: ClaudeIcon as unknown as ProviderIconComponent,
|
claude: ClaudeIcon as unknown as ProviderIconComponent,
|
||||||
codex: CodexIcon as unknown as ProviderIconComponent,
|
codex: CodexIcon as unknown as ProviderIconComponent,
|
||||||
copilot: CopilotIcon as unknown as ProviderIconComponent,
|
copilot: CopilotIcon as unknown as ProviderIconComponent,
|
||||||
@@ -53,7 +50,10 @@ function getCatalogProviderIcon(provider: string): ProviderIconComponent {
|
|||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
const iconSvg = CATALOG_ICON_SVGS.get(provider) ?? "";
|
const iconSvg = CATALOG_ICON_SVGS.get(provider);
|
||||||
|
if (!iconSvg) {
|
||||||
|
return Bot;
|
||||||
|
}
|
||||||
const icon = createCatalogIcon(provider, iconSvg);
|
const icon = createCatalogIcon(provider, iconSvg);
|
||||||
catalogIconComponents.set(provider, icon);
|
catalogIconComponents.set(provider, icon);
|
||||||
return icon;
|
return icon;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {
|
|||||||
WorkspaceDesktopTabsRow,
|
WorkspaceDesktopTabsRow,
|
||||||
type WorkspaceDesktopTabRowItem,
|
type WorkspaceDesktopTabRowItem,
|
||||||
} from "@/screens/workspace/workspace-desktop-tabs-row";
|
} from "@/screens/workspace/workspace-desktop-tabs-row";
|
||||||
|
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
||||||
import {
|
import {
|
||||||
WorkspaceTabPresentationResolver,
|
WorkspaceTabPresentationResolver,
|
||||||
WorkspaceTabIcon,
|
WorkspaceTabIcon,
|
||||||
@@ -92,7 +93,7 @@ interface SplitContainerProps {
|
|||||||
onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
||||||
onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise<void> | void;
|
||||||
onCreateDraftTab: (input: { paneId?: string }) => void;
|
onCreateDraftTab: (input: { paneId?: string }) => void;
|
||||||
onCreateTerminalTab: (input: { paneId?: string }) => void;
|
onCreateTerminalTab: (input: { paneId?: string; profile?: TerminalProfileInput }) => void;
|
||||||
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
||||||
showCreateBrowserTab?: boolean;
|
showCreateBrowserTab?: boolean;
|
||||||
buildPaneContentModel: (input: {
|
buildPaneContentModel: (input: {
|
||||||
|
|||||||
@@ -901,8 +901,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
labelText: {
|
labelText: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
},
|
},
|
||||||
separator: {
|
separator: {
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ export const ar: TranslationResources = {
|
|||||||
loadingAgentTitle: "جارٍ تحميل عنوان الوكيل",
|
loadingAgentTitle: "جارٍ تحميل عنوان الوكيل",
|
||||||
emptyPane: "لا توجد علامات تبويب في هذا الجزء.",
|
emptyPane: "لا توجد علامات تبويب في هذا الجزء.",
|
||||||
fallback: {
|
fallback: {
|
||||||
newAgent: "جديد Agent",
|
newAgent: "وكيل جديد",
|
||||||
setup: "يثبت",
|
setup: "يثبت",
|
||||||
workspaceSetup: "إعداد Workspace",
|
workspaceSetup: "إعداد Workspace",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
@@ -446,13 +446,15 @@ export const ar: TranslationResources = {
|
|||||||
renameAgent: "إعادة تسمية الوكيل",
|
renameAgent: "إعادة تسمية الوكيل",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "علامة تبويب الوكيل الجديد",
|
newAgent: "وكيل جديد",
|
||||||
newTerminal: "علامة تبويب طرفية جديدة",
|
newTerminal: "محطة جديدة",
|
||||||
preparingTerminal: "إعداد علامة التبويب المحطة الطرفية",
|
preparingTerminal: "إعداد علامة التبويب المحطة الطرفية",
|
||||||
preparingTerminalTooltip: "جارٍ تحضير المحطة...",
|
preparingTerminalTooltip: "جارٍ تحضير المحطة...",
|
||||||
newBrowser: "علامة تبويب متصفح جديدة",
|
newBrowser: "متصفح جديد",
|
||||||
splitRight: "تقسيم الجزء الأيمن",
|
splitRight: "تقسيم الجزء الأيمن",
|
||||||
splitDown: "تقسيم الجزء لأسفل",
|
splitDown: "تقسيم الجزء لأسفل",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "افتح المستكشف",
|
open: "افتح المستكشف",
|
||||||
@@ -1271,7 +1273,7 @@ export const ar: TranslationResources = {
|
|||||||
},
|
},
|
||||||
panels: {
|
panels: {
|
||||||
draft: {
|
draft: {
|
||||||
newAgent: "جديد Agent",
|
newAgent: "وكيل جديد",
|
||||||
creatingAgent: "وكيل الخلق",
|
creatingAgent: "وكيل الخلق",
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
@@ -1351,6 +1353,7 @@ export const ar: TranslationResources = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "مقدمي الخدمات",
|
providers: "مقدمي الخدمات",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1617,6 +1620,27 @@ export const ar: TranslationResources = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "Connect to this host to manage workspaces",
|
unavailable: "Connect to this host to manage workspaces",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "تحرير التسمية",
|
editLabel: "تحرير التسمية",
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export const en = {
|
|||||||
loadingAgentTitle: "Loading agent title",
|
loadingAgentTitle: "Loading agent title",
|
||||||
emptyPane: "No tabs in this pane.",
|
emptyPane: "No tabs in this pane.",
|
||||||
fallback: {
|
fallback: {
|
||||||
newAgent: "New Agent",
|
newAgent: "New agent",
|
||||||
setup: "Setup",
|
setup: "Setup",
|
||||||
workspaceSetup: "Workspace setup",
|
workspaceSetup: "Workspace setup",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
@@ -445,13 +445,15 @@ export const en = {
|
|||||||
renameAgent: "Rename agent",
|
renameAgent: "Rename agent",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "New agent tab",
|
newAgent: "New agent",
|
||||||
newTerminal: "New terminal tab",
|
newTerminal: "New terminal",
|
||||||
preparingTerminal: "Preparing terminal tab",
|
preparingTerminal: "Preparing terminal tab",
|
||||||
preparingTerminalTooltip: "Preparing terminal...",
|
preparingTerminalTooltip: "Preparing terminal...",
|
||||||
newBrowser: "New browser tab",
|
newBrowser: "New browser",
|
||||||
splitRight: "Split pane right",
|
splitRight: "Split pane right",
|
||||||
splitDown: "Split pane down",
|
splitDown: "Split pane down",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Open explorer",
|
open: "Open explorer",
|
||||||
@@ -1357,6 +1359,7 @@ export const en = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "Providers",
|
providers: "Providers",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1624,6 +1627,27 @@ export const en = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "Connect to this host to manage workspaces",
|
unavailable: "Connect to this host to manage workspaces",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "Edit label",
|
editLabel: "Edit label",
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export const es: TranslationResources = {
|
|||||||
loadingAgentTitle: "Título del agente de carga",
|
loadingAgentTitle: "Título del agente de carga",
|
||||||
emptyPane: "No hay pestañas en este panel.",
|
emptyPane: "No hay pestañas en este panel.",
|
||||||
fallback: {
|
fallback: {
|
||||||
newAgent: "NuevoAgent",
|
newAgent: "Nuevo agente",
|
||||||
setup: "Configuración",
|
setup: "Configuración",
|
||||||
workspaceSetup: "Configuración deWorkspace",
|
workspaceSetup: "Configuración deWorkspace",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
@@ -450,13 +450,15 @@ export const es: TranslationResources = {
|
|||||||
renameAgent: "Cambiar nombre del agente",
|
renameAgent: "Cambiar nombre del agente",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "Nueva pestaña de agente",
|
newAgent: "Nuevo agente",
|
||||||
newTerminal: "Nueva pestaña de terminal",
|
newTerminal: "Nueva terminal",
|
||||||
preparingTerminal: "Preparando la pestaña del terminal",
|
preparingTerminal: "Preparando la pestaña del terminal",
|
||||||
preparingTerminalTooltip: "Preparando terminal...",
|
preparingTerminalTooltip: "Preparando terminal...",
|
||||||
newBrowser: "Nueva pestaña del navegador",
|
newBrowser: "Nuevo navegador",
|
||||||
splitRight: "Panel dividido a la derecha",
|
splitRight: "Panel dividido a la derecha",
|
||||||
splitDown: "Dividir panel hacia abajo",
|
splitDown: "Dividir panel hacia abajo",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Explorador abierto",
|
open: "Explorador abierto",
|
||||||
@@ -1306,7 +1308,7 @@ export const es: TranslationResources = {
|
|||||||
},
|
},
|
||||||
panels: {
|
panels: {
|
||||||
draft: {
|
draft: {
|
||||||
newAgent: "NuevoAgent",
|
newAgent: "Nuevo agente",
|
||||||
creatingAgent: "Agente creador",
|
creatingAgent: "Agente creador",
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
@@ -1386,6 +1388,7 @@ export const es: TranslationResources = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "Proveedores",
|
providers: "Proveedores",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1654,6 +1657,27 @@ export const es: TranslationResources = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "Connect to this host to manage workspaces",
|
unavailable: "Connect to this host to manage workspaces",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "Editar etiqueta",
|
editLabel: "Editar etiqueta",
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export const fr: TranslationResources = {
|
|||||||
loadingAgentTitle: "Titre d'agent de chargement",
|
loadingAgentTitle: "Titre d'agent de chargement",
|
||||||
emptyPane: "Aucun onglet dans ce volet.",
|
emptyPane: "Aucun onglet dans ce volet.",
|
||||||
fallback: {
|
fallback: {
|
||||||
newAgent: "NouveauAgent",
|
newAgent: "Nouvel agent",
|
||||||
setup: "Installation",
|
setup: "Installation",
|
||||||
workspaceSetup: "ConfigurationWorkspace",
|
workspaceSetup: "ConfigurationWorkspace",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
@@ -450,13 +450,15 @@ export const fr: TranslationResources = {
|
|||||||
renameAgent: "Renommer l'agent",
|
renameAgent: "Renommer l'agent",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "Nouvel onglet agent",
|
newAgent: "Nouvel agent",
|
||||||
newTerminal: "Nouvel onglet de terminal",
|
newTerminal: "Nouveau terminal",
|
||||||
preparingTerminal: "Préparation de l'onglet du terminal",
|
preparingTerminal: "Préparation de l'onglet du terminal",
|
||||||
preparingTerminalTooltip: "Préparation du terminal...",
|
preparingTerminalTooltip: "Préparation du terminal...",
|
||||||
newBrowser: "Nouvel onglet du navigateur",
|
newBrowser: "Nouveau navigateur",
|
||||||
splitRight: "Volet divisé à droite",
|
splitRight: "Volet divisé à droite",
|
||||||
splitDown: "Diviser le volet vers le bas",
|
splitDown: "Diviser le volet vers le bas",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Ouvrir l'explorateur",
|
open: "Ouvrir l'explorateur",
|
||||||
@@ -1309,7 +1311,7 @@ export const fr: TranslationResources = {
|
|||||||
},
|
},
|
||||||
panels: {
|
panels: {
|
||||||
draft: {
|
draft: {
|
||||||
newAgent: "NouveauAgent",
|
newAgent: "Nouvel agent",
|
||||||
creatingAgent: "Agent créateur",
|
creatingAgent: "Agent créateur",
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
@@ -1389,6 +1391,7 @@ export const fr: TranslationResources = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "Fournisseurs",
|
providers: "Fournisseurs",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1660,6 +1663,27 @@ export const fr: TranslationResources = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "Connect to this host to manage workspaces",
|
unavailable: "Connect to this host to manage workspaces",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "Modifier l'étiquette",
|
editLabel: "Modifier l'étiquette",
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export const ru: TranslationResources = {
|
|||||||
loadingAgentTitle: "Название агента загрузки",
|
loadingAgentTitle: "Название агента загрузки",
|
||||||
emptyPane: "На этой панели нет вкладок.",
|
emptyPane: "На этой панели нет вкладок.",
|
||||||
fallback: {
|
fallback: {
|
||||||
newAgent: "Новый Agent",
|
newAgent: "Новый агент",
|
||||||
setup: "Настраивать",
|
setup: "Настраивать",
|
||||||
workspaceSetup: "Настройка Workspace",
|
workspaceSetup: "Настройка Workspace",
|
||||||
terminal: "Terminal",
|
terminal: "Terminal",
|
||||||
@@ -449,13 +449,15 @@ export const ru: TranslationResources = {
|
|||||||
renameAgent: "Переименовать агента",
|
renameAgent: "Переименовать агента",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "Новая вкладка агента",
|
newAgent: "Новый агент",
|
||||||
newTerminal: "Новая вкладка терминала",
|
newTerminal: "Новый терминал",
|
||||||
preparingTerminal: "Подготовка вкладки терминала",
|
preparingTerminal: "Подготовка вкладки терминала",
|
||||||
preparingTerminalTooltip: "Подготовка терминала...",
|
preparingTerminalTooltip: "Подготовка терминала...",
|
||||||
newBrowser: "Новая вкладка браузера",
|
newBrowser: "Новый браузер",
|
||||||
splitRight: "Разделить панель справа",
|
splitRight: "Разделить панель справа",
|
||||||
splitDown: "Разделить панель вниз",
|
splitDown: "Разделить панель вниз",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "Открыть проводник",
|
open: "Открыть проводник",
|
||||||
@@ -1298,7 +1300,7 @@ export const ru: TranslationResources = {
|
|||||||
},
|
},
|
||||||
panels: {
|
panels: {
|
||||||
draft: {
|
draft: {
|
||||||
newAgent: "Новый Agent",
|
newAgent: "Новый агент",
|
||||||
creatingAgent: "Создание агента",
|
creatingAgent: "Создание агента",
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
@@ -1378,6 +1380,7 @@ export const ru: TranslationResources = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "Провайдеры",
|
providers: "Провайдеры",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1648,6 +1651,27 @@ export const ru: TranslationResources = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "Connect to this host to manage workspaces",
|
unavailable: "Connect to this host to manage workspaces",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "Изменить ярлык",
|
editLabel: "Изменить ярлык",
|
||||||
|
|||||||
@@ -446,13 +446,15 @@ export const zhCN: TranslationResources = {
|
|||||||
renameAgent: "重命名 Agent",
|
renameAgent: "重命名 Agent",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
newAgent: "新建 Agent 标签",
|
newAgent: "新建 Agent",
|
||||||
newTerminal: "新建 Terminal 标签",
|
newTerminal: "新建 Terminal",
|
||||||
preparingTerminal: "正在准备 Terminal 标签",
|
preparingTerminal: "正在准备 Terminal 标签",
|
||||||
preparingTerminalTooltip: "正在准备 Terminal...",
|
preparingTerminalTooltip: "正在准备 Terminal...",
|
||||||
newBrowser: "新建浏览器标签",
|
newBrowser: "新建浏览器",
|
||||||
splitRight: "向右拆分窗格",
|
splitRight: "向右拆分窗格",
|
||||||
splitDown: "向下拆分窗格",
|
splitDown: "向下拆分窗格",
|
||||||
|
terminalProfilesMenu: "Terminal profiles",
|
||||||
|
editTerminalProfiles: "Edit profiles…",
|
||||||
},
|
},
|
||||||
explorer: {
|
explorer: {
|
||||||
open: "打开 explorer",
|
open: "打开 explorer",
|
||||||
@@ -1334,6 +1336,7 @@ export const zhCN: TranslationResources = {
|
|||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
workspaces: "Workspaces",
|
workspaces: "Workspaces",
|
||||||
providers: "Providers",
|
providers: "Providers",
|
||||||
|
terminals: "Terminals",
|
||||||
host: "Host",
|
host: "Host",
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -1599,6 +1602,27 @@ export const zhCN: TranslationResources = {
|
|||||||
workspaces: {
|
workspaces: {
|
||||||
unavailable: "连接到这个 Host 以管理 Workspace",
|
unavailable: "连接到这个 Host 以管理 Workspace",
|
||||||
},
|
},
|
||||||
|
terminalProfiles: {
|
||||||
|
unavailable: "Connect to this host to manage terminal profiles",
|
||||||
|
sectionTitle: "Terminal profiles",
|
||||||
|
editProfile: "Edit profile",
|
||||||
|
addProfileTitle: "Add terminal profile",
|
||||||
|
editProfileTitle: "Edit terminal profile",
|
||||||
|
namePlaceholder: "Claude Code",
|
||||||
|
commandPlaceholder: "claude",
|
||||||
|
argsPlaceholder: "--dangerously-skip-permissions",
|
||||||
|
nameLabel: "Name",
|
||||||
|
commandLabel: "Command",
|
||||||
|
argsLabel: "Arguments",
|
||||||
|
argsHint: "Space-separated arguments passed to the command",
|
||||||
|
remove: "Remove",
|
||||||
|
removeConfirmTitle: "Remove profile?",
|
||||||
|
removeConfirmMessage: 'Remove "{{name}}"?',
|
||||||
|
moveUp: "Move up",
|
||||||
|
moveDown: "Move down",
|
||||||
|
save: "Save",
|
||||||
|
emptyState: "No profiles yet. Add one to launch terminals with a specific command.",
|
||||||
|
},
|
||||||
daemon: {
|
daemon: {
|
||||||
rename: {
|
rename: {
|
||||||
editLabel: "编辑标签",
|
editLabel: "编辑标签",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
Puzzle,
|
Puzzle,
|
||||||
Plus,
|
Plus,
|
||||||
FolderGit2,
|
FolderGit2,
|
||||||
|
SquareTerminal,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
import { SidebarHeaderRow } from "@/components/sidebar/sidebar-header-row";
|
||||||
import { SidebarSeparator } from "@/components/sidebar/sidebar-separator";
|
import { SidebarSeparator } from "@/components/sidebar/sidebar-separator";
|
||||||
@@ -107,6 +108,7 @@ import {
|
|||||||
HostSettingsPage,
|
HostSettingsPage,
|
||||||
HostProvidersPage,
|
HostProvidersPage,
|
||||||
HostWorkspacesPage,
|
HostWorkspacesPage,
|
||||||
|
HostTerminalsPage,
|
||||||
} from "@/screens/settings/host-page";
|
} from "@/screens/settings/host-page";
|
||||||
import ProjectsScreen from "@/screens/projects-screen";
|
import ProjectsScreen from "@/screens/projects-screen";
|
||||||
import ProjectSettingsScreen from "@/screens/project-settings-screen";
|
import ProjectSettingsScreen from "@/screens/project-settings-screen";
|
||||||
@@ -173,6 +175,7 @@ const HOST_SECTION_ITEMS: HostSectionItem[] = [
|
|||||||
{ id: "agents", labelKey: "settings.hostSections.agents", icon: Bot },
|
{ id: "agents", labelKey: "settings.hostSections.agents", icon: Bot },
|
||||||
{ id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 },
|
{ id: "workspaces", labelKey: "settings.hostSections.workspaces", icon: FolderGit2 },
|
||||||
{ id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes },
|
{ id: "providers", labelKey: "settings.hostSections.providers", icon: Boxes },
|
||||||
|
{ id: "terminals", labelKey: "settings.hostSections.terminals", icon: SquareTerminal },
|
||||||
{ id: "host", labelKey: "settings.hostSections.host", icon: Server },
|
{ id: "host", labelKey: "settings.hostSections.host", icon: Server },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -189,6 +192,8 @@ function renderHostSettingsContent(
|
|||||||
return <HostWorkspacesPage serverId={view.serverId} />;
|
return <HostWorkspacesPage serverId={view.serverId} />;
|
||||||
case "providers":
|
case "providers":
|
||||||
return <HostProvidersPage serverId={view.serverId} />;
|
return <HostProvidersPage serverId={view.serverId} />;
|
||||||
|
case "terminals":
|
||||||
|
return <HostTerminalsPage serverId={view.serverId} />;
|
||||||
case "host":
|
case "host":
|
||||||
return <HostSettingsPage serverId={view.serverId} onHostRemoved={onHostRemoved} />;
|
return <HostSettingsPage serverId={view.serverId} onHostRemoved={onHostRemoved} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
import { ChevronRight, Globe, Monitor, Pencil, RotateCw, Trash2 } from "lucide-react-native";
|
import {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
ChevronRight,
|
||||||
|
Globe,
|
||||||
|
Monitor,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
RotateCw,
|
||||||
|
SquareTerminal,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react-native";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Alert, Pressable, Text, View } from "react-native";
|
import { Alert, Pressable, Text, View } from "react-native";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles";
|
||||||
|
import type { TerminalProfile } from "@getpaseo/protocol/messages";
|
||||||
|
import {
|
||||||
|
getTerminalProfileIcon,
|
||||||
|
resolveTerminalProfiles,
|
||||||
|
} from "@getpaseo/protocol/terminal-profiles";
|
||||||
|
import { AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
||||||
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
import { AdaptiveModalSheet, type SheetHeader } from "@/components/adaptive-modal-sheet";
|
||||||
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
import { AdaptiveRenameModal } from "@/components/rename-modal";
|
||||||
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
import { SettingsTextAreaCard } from "@/components/settings-textarea";
|
||||||
@@ -33,6 +50,38 @@ import type { HostConnection, HostProfile } from "@/types/host-connection";
|
|||||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||||
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
import { formatConnectionStatus, getConnectionStatusTone } from "@/utils/daemons";
|
||||||
import { formatLatency } from "@/utils/latency";
|
import { formatLatency } from "@/utils/latency";
|
||||||
|
import { ICON_SIZE } from "@/styles/theme";
|
||||||
|
import type { Theme } from "@/styles/theme";
|
||||||
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
|
|
||||||
|
const ThemedArrowUp = withUnistyles(ArrowUp);
|
||||||
|
const ThemedArrowDown = withUnistyles(ArrowDown);
|
||||||
|
const ThemedProfilePencil = withUnistyles(Pencil);
|
||||||
|
const ThemedTrash2 = withUnistyles(Trash2);
|
||||||
|
const ThemedProfileSquareTerminal = withUnistyles(SquareTerminal);
|
||||||
|
const ThemedPlus = withUnistyles(Plus);
|
||||||
|
|
||||||
|
interface DynamicProviderIconProps {
|
||||||
|
iconKey: string;
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) {
|
||||||
|
const Icon = getProviderIcon(iconKey);
|
||||||
|
return <Icon size={size} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
|
||||||
|
|
||||||
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
const destructiveColorMapping = (theme: Theme) => ({ color: theme.colors.destructive });
|
||||||
|
|
||||||
|
const moveUpIcon = <ThemedArrowUp size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
|
const moveDownIcon = <ThemedArrowDown size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
|
const editProfileIcon = <ThemedProfilePencil size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
|
const removeProfileIcon = <ThemedTrash2 size={ICON_SIZE.sm} uniProps={destructiveColorMapping} />;
|
||||||
|
const addProfileIcon = <ThemedPlus size={ICON_SIZE.sm} uniProps={mutedColorMapping} />;
|
||||||
|
|
||||||
function formatHostConnectionLabel(connection: HostConnection, t: TFunction): string {
|
function formatHostConnectionLabel(connection: HostConnection, t: TFunction): string {
|
||||||
if (connection.type === "relay") {
|
if (connection.type === "relay") {
|
||||||
@@ -1043,6 +1092,527 @@ function RemoveHostSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Terminal Profiles
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function generateProfileId(): string {
|
||||||
|
return `profile_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgsString(raw: string): string[] | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return undefined;
|
||||||
|
return trimmed
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileDraft {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
args: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_PROFILE_DRAFT: ProfileDraft = { name: "", command: "", args: "" };
|
||||||
|
|
||||||
|
interface TerminalProfileEditModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
title: string;
|
||||||
|
initialDraft: ProfileDraft;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (draft: ProfileDraft) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TerminalProfileEditModal({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
initialDraft,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
}: TerminalProfileEditModalProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [name, setName] = useState(initialDraft.name);
|
||||||
|
const [command, setCommand] = useState(initialDraft.command);
|
||||||
|
const [args, setArgs] = useState(initialDraft.args);
|
||||||
|
const sheetHeader = useMemo<SheetHeader>(() => ({ title }), [title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
setName(initialDraft.name);
|
||||||
|
setCommand(initialDraft.command);
|
||||||
|
setArgs(initialDraft.args);
|
||||||
|
}, [visible, initialDraft.name, initialDraft.command, initialDraft.args]);
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
onSave({ name, command, args });
|
||||||
|
}, [onSave, name, command, args]);
|
||||||
|
|
||||||
|
const canSave = name.trim().length > 0 && command.trim().length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AdaptiveModalSheet
|
||||||
|
visible={visible}
|
||||||
|
header={sheetHeader}
|
||||||
|
onClose={onClose}
|
||||||
|
testID="terminal-profile-edit-modal"
|
||||||
|
desktopMaxWidth={480}
|
||||||
|
>
|
||||||
|
<View style={profileModalStyles.body}>
|
||||||
|
<View style={profileModalStyles.fieldGroup}>
|
||||||
|
<Text style={profileModalStyles.fieldLabel}>
|
||||||
|
{t("settings.host.terminalProfiles.nameLabel")}
|
||||||
|
</Text>
|
||||||
|
<AdaptiveTextInput
|
||||||
|
initialValue={initialDraft.name}
|
||||||
|
resetKey={visible ? "open" : "closed"}
|
||||||
|
onChangeText={setName}
|
||||||
|
placeholder={t("settings.host.terminalProfiles.namePlaceholder")}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
testID="terminal-profile-name-input"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={profileModalStyles.fieldGroup}>
|
||||||
|
<Text style={profileModalStyles.fieldLabel}>
|
||||||
|
{t("settings.host.terminalProfiles.commandLabel")}
|
||||||
|
</Text>
|
||||||
|
<AdaptiveTextInput
|
||||||
|
initialValue={initialDraft.command}
|
||||||
|
resetKey={visible ? "open" : "closed"}
|
||||||
|
onChangeText={setCommand}
|
||||||
|
placeholder={t("settings.host.terminalProfiles.commandPlaceholder")}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
testID="terminal-profile-command-input"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={profileModalStyles.fieldGroup}>
|
||||||
|
<Text style={profileModalStyles.fieldLabel}>
|
||||||
|
{t("settings.host.terminalProfiles.argsLabel")}
|
||||||
|
</Text>
|
||||||
|
<AdaptiveTextInput
|
||||||
|
initialValue={initialDraft.args}
|
||||||
|
resetKey={visible ? "open" : "closed"}
|
||||||
|
onChangeText={setArgs}
|
||||||
|
placeholder={t("settings.host.terminalProfiles.argsPlaceholder")}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
testID="terminal-profile-args-input"
|
||||||
|
/>
|
||||||
|
<Text style={profileModalStyles.fieldHint}>
|
||||||
|
{t("settings.host.terminalProfiles.argsHint")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={profileModalStyles.actions}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
style={profileModalStyles.actionButton}
|
||||||
|
onPress={onClose}
|
||||||
|
>
|
||||||
|
{t("common.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
style={profileModalStyles.actionButton}
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={!canSave}
|
||||||
|
testID="terminal-profile-save-button"
|
||||||
|
>
|
||||||
|
{t("settings.host.terminalProfiles.save")}
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</AdaptiveModalSheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TerminalProfileRowProps {
|
||||||
|
profile: TerminalProfile;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
|
onEdit: (id: string) => void;
|
||||||
|
onRemove: (id: string) => void;
|
||||||
|
onMoveUp: (id: string) => void;
|
||||||
|
onMoveDown: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TerminalProfileRow({
|
||||||
|
profile,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
onEdit,
|
||||||
|
onRemove,
|
||||||
|
onMoveUp,
|
||||||
|
onMoveDown,
|
||||||
|
}: TerminalProfileRowProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleEdit = useCallback(() => onEdit(profile.id), [onEdit, profile.id]);
|
||||||
|
const handleRemove = useCallback(() => onRemove(profile.id), [onRemove, profile.id]);
|
||||||
|
const handleMoveUp = useCallback(() => onMoveUp(profile.id), [onMoveUp, profile.id]);
|
||||||
|
const handleMoveDown = useCallback(() => onMoveDown(profile.id), [onMoveDown, profile.id]);
|
||||||
|
|
||||||
|
const commandText =
|
||||||
|
profile.args && profile.args.length > 0
|
||||||
|
? `${profile.command} ${profile.args.join(" ")}`
|
||||||
|
: profile.command;
|
||||||
|
|
||||||
|
const rowStyle = useMemo(
|
||||||
|
() => [settingsStyles.row, !isFirst && settingsStyles.rowBorder, terminalProfileStyles.row],
|
||||||
|
[isFirst],
|
||||||
|
);
|
||||||
|
|
||||||
|
const icon = getTerminalProfileIcon(profile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={rowStyle} testID={`terminal-profile-row-${profile.id}`}>
|
||||||
|
<View style={terminalProfileStyles.iconWrapper}>
|
||||||
|
{icon ? (
|
||||||
|
<ThemedDynamicProviderIcon
|
||||||
|
iconKey={icon}
|
||||||
|
size={ICON_SIZE.md}
|
||||||
|
uniProps={mutedColorMapping}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ThemedProfileSquareTerminal size={ICON_SIZE.md} uniProps={mutedColorMapping} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={settingsStyles.rowContent}>
|
||||||
|
<Text style={settingsStyles.rowTitle} numberOfLines={1}>
|
||||||
|
{profile.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={settingsStyles.rowHint} numberOfLines={1}>
|
||||||
|
{commandText}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={terminalProfileStyles.rowActions}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={moveUpIcon}
|
||||||
|
onPress={handleMoveUp}
|
||||||
|
disabled={isFirst}
|
||||||
|
accessibilityLabel={t("settings.host.terminalProfiles.moveUp")}
|
||||||
|
testID={`terminal-profile-move-up-${profile.id}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={moveDownIcon}
|
||||||
|
onPress={handleMoveDown}
|
||||||
|
disabled={isLast}
|
||||||
|
accessibilityLabel={t("settings.host.terminalProfiles.moveDown")}
|
||||||
|
testID={`terminal-profile-move-down-${profile.id}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={editProfileIcon}
|
||||||
|
onPress={handleEdit}
|
||||||
|
accessibilityLabel={t("settings.host.terminalProfiles.editProfile")}
|
||||||
|
testID={`terminal-profile-edit-${profile.id}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={removeProfileIcon}
|
||||||
|
onPress={handleRemove}
|
||||||
|
accessibilityLabel={t("settings.host.terminalProfiles.remove")}
|
||||||
|
testID={`terminal-profile-remove-${profile.id}`}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TerminalProfilesSection({ serverId }: { serverId: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||||
|
const { config, patchConfig } = useDaemonConfig(serverId);
|
||||||
|
const [editingProfile, setEditingProfile] = useState<{
|
||||||
|
id: string;
|
||||||
|
draft: ProfileDraft;
|
||||||
|
} | null>(null);
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const profiles = useMemo(
|
||||||
|
() => (config ? resolveTerminalProfiles(config.terminalProfiles) : null),
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveProfiles = useCallback(
|
||||||
|
async (next: TerminalProfile[]) => {
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
await patchConfig({ terminalProfiles: next });
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert(
|
||||||
|
t("common.errors.unableToSave"),
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[patchConfig, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddOpen = useCallback(() => setIsAdding(true), []);
|
||||||
|
const handleAddClose = useCallback(() => setIsAdding(false), []);
|
||||||
|
|
||||||
|
const handleAddSave = useCallback(
|
||||||
|
(draft: ProfileDraft) => {
|
||||||
|
const current = profiles ? [...profiles] : [];
|
||||||
|
const next: TerminalProfile[] = [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
id: generateProfileId(),
|
||||||
|
name: draft.name.trim(),
|
||||||
|
command: draft.command.trim(),
|
||||||
|
args: parseArgsString(draft.args),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setIsAdding(false);
|
||||||
|
void saveProfiles(next);
|
||||||
|
},
|
||||||
|
[profiles, saveProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditOpen = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const profile = profiles?.find((p) => p.id === id);
|
||||||
|
if (!profile) return;
|
||||||
|
setEditingProfile({
|
||||||
|
id,
|
||||||
|
draft: {
|
||||||
|
name: profile.name,
|
||||||
|
command: profile.command,
|
||||||
|
args: profile.args ? profile.args.join(" ") : "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[profiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditClose = useCallback(() => setEditingProfile(null), []);
|
||||||
|
|
||||||
|
const handleEditSave = useCallback(
|
||||||
|
(draft: ProfileDraft) => {
|
||||||
|
if (!editingProfile || !profiles) return;
|
||||||
|
const next: TerminalProfile[] = profiles.map((p) =>
|
||||||
|
p.id === editingProfile.id
|
||||||
|
? {
|
||||||
|
...p,
|
||||||
|
name: draft.name.trim(),
|
||||||
|
command: draft.command.trim(),
|
||||||
|
args: parseArgsString(draft.args),
|
||||||
|
}
|
||||||
|
: p,
|
||||||
|
);
|
||||||
|
setEditingProfile(null);
|
||||||
|
void saveProfiles(next);
|
||||||
|
},
|
||||||
|
[editingProfile, profiles, saveProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemove = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const profile = profiles?.find((p) => p.id === id);
|
||||||
|
if (!profile) return;
|
||||||
|
void confirmDialog({
|
||||||
|
title: t("settings.host.terminalProfiles.removeConfirmTitle"),
|
||||||
|
message: t("settings.host.terminalProfiles.removeConfirmMessage", {
|
||||||
|
name: profile.name,
|
||||||
|
}),
|
||||||
|
confirmLabel: t("settings.host.terminalProfiles.remove"),
|
||||||
|
cancelLabel: t("common.actions.cancel"),
|
||||||
|
destructive: true,
|
||||||
|
}).then((confirmed) => {
|
||||||
|
if (!confirmed || !profiles) return;
|
||||||
|
void saveProfiles(profiles.filter((p) => p.id !== id));
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[profiles, saveProfiles, t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMoveUp = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!profiles) return;
|
||||||
|
const index = profiles.findIndex((p) => p.id === id);
|
||||||
|
if (index <= 0) return;
|
||||||
|
const next = [...profiles];
|
||||||
|
const [item] = next.splice(index, 1);
|
||||||
|
next.splice(index - 1, 0, item);
|
||||||
|
void saveProfiles(next);
|
||||||
|
},
|
||||||
|
[profiles, saveProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMoveDown = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!profiles) return;
|
||||||
|
const index = profiles.findIndex((p) => p.id === id);
|
||||||
|
if (index < 0 || index >= profiles.length - 1) return;
|
||||||
|
const next = [...profiles];
|
||||||
|
const [item] = next.splice(index, 1);
|
||||||
|
next.splice(index + 1, 0, item);
|
||||||
|
void saveProfiles(next);
|
||||||
|
},
|
||||||
|
[profiles, saveProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addButton = useMemo(
|
||||||
|
() => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
leftIcon={addProfileIcon}
|
||||||
|
onPress={handleAddOpen}
|
||||||
|
disabled={isSaving || !isConnected || !profiles}
|
||||||
|
testID="terminal-profiles-add-button"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[handleAddOpen, isSaving, isConnected, profiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return (
|
||||||
|
<View style={settingsStyles.card} testID="terminal-profiles-unavailable">
|
||||||
|
<View style={terminalProfileStyles.emptyCard}>
|
||||||
|
<Text style={terminalProfileStyles.emptyText}>
|
||||||
|
{t("settings.host.terminalProfiles.unavailable")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsSection
|
||||||
|
title={t("settings.host.terminalProfiles.sectionTitle")}
|
||||||
|
trailing={addButton}
|
||||||
|
testID="terminal-profiles-section"
|
||||||
|
>
|
||||||
|
<View style={settingsStyles.card} testID="terminal-profiles-card">
|
||||||
|
{profiles && profiles.length > 0 ? (
|
||||||
|
profiles.map((profile, index) => (
|
||||||
|
<TerminalProfileRow
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
isFirst={index === 0}
|
||||||
|
isLast={index === profiles.length - 1}
|
||||||
|
onEdit={handleEditOpen}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onMoveUp={handleMoveUp}
|
||||||
|
onMoveDown={handleMoveDown}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<View style={terminalProfileStyles.emptyCard}>
|
||||||
|
<Text style={terminalProfileStyles.emptyText}>
|
||||||
|
{t("settings.host.terminalProfiles.emptyState")}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
|
<TerminalProfileEditModal
|
||||||
|
visible={isAdding}
|
||||||
|
title={t("settings.host.terminalProfiles.addProfileTitle")}
|
||||||
|
initialDraft={EMPTY_PROFILE_DRAFT}
|
||||||
|
onClose={handleAddClose}
|
||||||
|
onSave={handleAddSave}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{editingProfile ? (
|
||||||
|
<TerminalProfileEditModal
|
||||||
|
visible
|
||||||
|
title={t("settings.host.terminalProfiles.editProfileTitle")}
|
||||||
|
initialDraft={editingProfile.draft}
|
||||||
|
onClose={handleEditClose}
|
||||||
|
onSave={handleEditSave}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HostTerminalsPage({ serverId }: { serverId: string }) {
|
||||||
|
const host = useHostProfile(serverId);
|
||||||
|
|
||||||
|
if (!host) {
|
||||||
|
return <HostNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TerminalProfilesSection serverId={serverId} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalProfileStyles = StyleSheet.create((theme) => ({
|
||||||
|
row: {
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
minHeight: 56,
|
||||||
|
},
|
||||||
|
iconWrapper: {
|
||||||
|
width: theme.iconSize.md,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
rowActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0,
|
||||||
|
},
|
||||||
|
emptyCard: {
|
||||||
|
padding: theme.spacing[4],
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const profileModalStyles = StyleSheet.create((theme) => ({
|
||||||
|
body: {
|
||||||
|
gap: theme.spacing[4],
|
||||||
|
paddingBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
|
fieldGroup: {
|
||||||
|
gap: theme.spacing[1.5],
|
||||||
|
},
|
||||||
|
fieldLabel: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
},
|
||||||
|
fieldHint: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
marginTop: theme.spacing[2],
|
||||||
|
},
|
||||||
|
actionButton: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const styles = StyleSheet.create((theme) => ({
|
const styles = StyleSheet.create((theme) => ({
|
||||||
identityEditButton: {
|
identityEditButton: {
|
||||||
padding: theme.spacing[1],
|
padding: theme.spacing[1],
|
||||||
|
|||||||
@@ -16,10 +16,19 @@ import {
|
|||||||
upsertCreatedTerminalPayload,
|
upsertCreatedTerminalPayload,
|
||||||
} from "@/screens/workspace/terminals/state";
|
} from "@/screens/workspace/terminals/state";
|
||||||
|
|
||||||
|
interface TerminalProfileInput {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
args?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
interface PendingTerminalCreateInput {
|
interface PendingTerminalCreateInput {
|
||||||
paneId?: string;
|
paneId?: string;
|
||||||
|
profile?: TerminalProfileInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { TerminalProfileInput };
|
||||||
|
|
||||||
interface UseWorkspaceTerminalsInput {
|
interface UseWorkspaceTerminalsInput {
|
||||||
client: DaemonClient | null;
|
client: DaemonClient | null;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
@@ -110,6 +119,10 @@ export function useWorkspaceTerminals(input: UseWorkspaceTerminalsInput) {
|
|||||||
if (!client || !workspaceDirectory) {
|
if (!client || !workspaceDirectory) {
|
||||||
throw new Error(t("workspace.terminal.hostDisconnected"));
|
throw new Error(t("workspace.terminal.hostDisconnected"));
|
||||||
}
|
}
|
||||||
|
if (_input?.profile) {
|
||||||
|
const { name, command, args } = _input.profile;
|
||||||
|
return await client.createTerminal(workspaceDirectory, name, undefined, { command, args });
|
||||||
|
}
|
||||||
return await client.createTerminal(workspaceDirectory);
|
return await client.createTerminal(workspaceDirectory);
|
||||||
},
|
},
|
||||||
onSuccess: (payload, createInput) => {
|
onSuccess: (payload, createInput) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
CopyX,
|
CopyX,
|
||||||
ArrowLeftToLine,
|
ArrowLeftToLine,
|
||||||
ArrowRightToLine,
|
ArrowRightToLine,
|
||||||
|
ChevronDown,
|
||||||
Columns2,
|
Columns2,
|
||||||
Copy,
|
Copy,
|
||||||
Pencil,
|
Pencil,
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRouter, type Href } from "expo-router";
|
||||||
import { SortableInlineList } from "@/components/sortable-inline-list";
|
import { SortableInlineList } from "@/components/sortable-inline-list";
|
||||||
import type {
|
import type {
|
||||||
DraggableListDragHandleProps,
|
DraggableListDragHandleProps,
|
||||||
@@ -46,6 +48,14 @@ import {
|
|||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Shortcut } from "@/components/ui/shortcut";
|
import { Shortcut } from "@/components/ui/shortcut";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
|
||||||
@@ -67,6 +77,14 @@ import {
|
|||||||
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
||||||
import type { Theme } from "@/styles/theme";
|
import type { Theme } from "@/styles/theme";
|
||||||
import { RenderProfile } from "@/utils/render-profiler";
|
import { RenderProfile } from "@/utils/render-profiler";
|
||||||
|
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||||
|
import {
|
||||||
|
getTerminalProfileIcon,
|
||||||
|
resolveTerminalProfiles,
|
||||||
|
} from "@getpaseo/protocol/terminal-profiles";
|
||||||
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
|
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
||||||
|
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
|
||||||
|
|
||||||
const DROPDOWN_WIDTH = 220;
|
const DROPDOWN_WIDTH = 220;
|
||||||
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
|
||||||
@@ -82,14 +100,33 @@ const ThemedCopyX = withUnistyles(CopyX);
|
|||||||
const ThemedPencil = withUnistyles(Pencil);
|
const ThemedPencil = withUnistyles(Pencil);
|
||||||
const ThemedSquarePen = withUnistyles(SquarePen);
|
const ThemedSquarePen = withUnistyles(SquarePen);
|
||||||
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
|
||||||
|
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||||
const ThemedGlobe = withUnistyles(Globe);
|
const ThemedGlobe = withUnistyles(Globe);
|
||||||
const ThemedColumns2 = withUnistyles(Columns2);
|
const ThemedColumns2 = withUnistyles(Columns2);
|
||||||
const ThemedRows2 = withUnistyles(Rows2);
|
const ThemedRows2 = withUnistyles(Rows2);
|
||||||
const ThemedPlus = withUnistyles(Plus);
|
const ThemedPlus = withUnistyles(Plus);
|
||||||
|
const ThemedProfileSquareTerminal = withUnistyles(SquareTerminal);
|
||||||
|
|
||||||
|
interface DynamicProviderIconProps {
|
||||||
|
iconKey: string;
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) {
|
||||||
|
const Icon = getProviderIcon(iconKey);
|
||||||
|
return <Icon size={size} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
|
||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|
||||||
|
const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
|
||||||
|
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
|
||||||
|
const BROWSER_ICON = <ThemedGlobe size={14} uniProps={mutedColorMapping} />;
|
||||||
|
|
||||||
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
|
||||||
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
|
||||||
}
|
}
|
||||||
@@ -103,38 +140,161 @@ function updateMeasuredWidth(setWidth: Dispatch<SetStateAction<number>>, event:
|
|||||||
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DropdownMenuProfileItemProps {
|
||||||
|
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
|
||||||
|
disabled?: boolean;
|
||||||
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuProfileItem({
|
||||||
|
profile,
|
||||||
|
disabled,
|
||||||
|
onCreateTerminalWithProfile,
|
||||||
|
}: DropdownMenuProfileItemProps) {
|
||||||
|
const handleSelect = useCallback(() => {
|
||||||
|
onCreateTerminalWithProfile({
|
||||||
|
name: profile.name,
|
||||||
|
command: profile.command,
|
||||||
|
args: profile.args,
|
||||||
|
});
|
||||||
|
}, [onCreateTerminalWithProfile, profile]);
|
||||||
|
|
||||||
|
const icon = getTerminalProfileIcon(profile);
|
||||||
|
|
||||||
|
const leading = useMemo(() => {
|
||||||
|
if (!icon) {
|
||||||
|
return (
|
||||||
|
<View style={styles.terminalProfileIconWrapper}>
|
||||||
|
<ThemedProfileSquareTerminal size={14} uniProps={mutedColorMapping} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.terminalProfileIconWrapper}>
|
||||||
|
<ThemedDynamicProviderIcon iconKey={icon} size={14} uniProps={mutedColorMapping} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [icon]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
|
||||||
|
{profile.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkspaceInlineAddTabButtonProps {
|
interface WorkspaceInlineAddTabButtonProps {
|
||||||
shortcutKeys: ShortcutKey[][] | null;
|
shortcutKeys: ShortcutKey[][] | null;
|
||||||
onPress: () => void;
|
onCreateAgentTab: () => void;
|
||||||
|
onCreateTerminal: () => void;
|
||||||
|
onCreateBrowser: () => void;
|
||||||
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
|
onEditProfiles: () => void;
|
||||||
|
normalizedServerId: string;
|
||||||
|
showCreateBrowserTab: boolean;
|
||||||
|
terminalDisabled: boolean;
|
||||||
onLayout: (event: LayoutChangeEvent) => void;
|
onLayout: (event: LayoutChangeEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceInlineAddTabButton({
|
function WorkspaceInlineAddTabButton({
|
||||||
shortcutKeys,
|
shortcutKeys,
|
||||||
onPress,
|
onCreateAgentTab,
|
||||||
|
onCreateTerminal,
|
||||||
|
onCreateBrowser,
|
||||||
|
onCreateTerminalWithProfile,
|
||||||
|
onEditProfiles,
|
||||||
|
normalizedServerId,
|
||||||
|
showCreateBrowserTab,
|
||||||
|
terminalDisabled,
|
||||||
onLayout,
|
onLayout,
|
||||||
}: WorkspaceInlineAddTabButtonProps) {
|
}: WorkspaceInlineAddTabButtonProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { config } = useDaemonConfig(normalizedServerId);
|
||||||
|
const profiles = useMemo(
|
||||||
|
() => resolveTerminalProfiles(config?.terminalProfiles),
|
||||||
|
[config?.terminalProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tooltipText = t("workspace.tabs.actions.newAgent");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.inlineAddButton} onLayout={onLayout}>
|
<View style={styles.inlineAddButton} onLayout={onLayout}>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
testID="workspace-new-agent-tab-inline"
|
testID="workspace-new-agent-tab-inline"
|
||||||
onPress={onPress}
|
onPress={onCreateAgentTab}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel="New agent tab"
|
accessibilityLabel={tooltipText}
|
||||||
style={inlineAddActionButtonStyle}
|
style={inlineAddActionButtonStyle}
|
||||||
>
|
>
|
||||||
<ThemedPlus size={16} uniProps={mutedColorMapping} />
|
<ThemedPlus size={14} uniProps={mutedColorMapping} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
<TooltipContent side="bottom" align="center" offset={8}>
|
||||||
<View style={styles.newTabTooltipRow}>
|
<View style={styles.newTabTooltipRow}>
|
||||||
<Text style={styles.newTabTooltipText}>New agent tab</Text>
|
<Text style={styles.newTabTooltipText}>{tooltipText}</Text>
|
||||||
{shortcutKeys ? (
|
{shortcutKeys ? (
|
||||||
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<DropdownMenu>
|
||||||
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
|
<TooltipTrigger asChild triggerRefProp="triggerRef">
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
testID="workspace-new-tab-menu-trigger"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t("workspace.tabs.actions.moreActions")}
|
||||||
|
style={inlineAddActionButtonStyle}
|
||||||
|
>
|
||||||
|
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="center" offset={8}>
|
||||||
|
<Text style={styles.newTabTooltipText}>{t("workspace.tabs.actions.moreActions")}</Text>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<DropdownMenuContent side="bottom" align="end" offset={4} minWidth={200}>
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="workspace-new-tab-menu-agent"
|
||||||
|
leading={AGENT_ICON}
|
||||||
|
onSelect={onCreateAgentTab}
|
||||||
|
>
|
||||||
|
{t("workspace.tabs.actions.newAgent")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="workspace-new-tab-menu-terminal"
|
||||||
|
leading={TERMINAL_ICON}
|
||||||
|
disabled={terminalDisabled}
|
||||||
|
onSelect={terminalDisabled ? undefined : onCreateTerminal}
|
||||||
|
>
|
||||||
|
{t("workspace.tabs.actions.newTerminal")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{showCreateBrowserTab ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="workspace-new-tab-menu-browser"
|
||||||
|
leading={BROWSER_ICON}
|
||||||
|
onSelect={onCreateBrowser}
|
||||||
|
>
|
||||||
|
{t("workspace.tabs.actions.newBrowser")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
|
||||||
|
{profiles.map((profile) => (
|
||||||
|
<DropdownMenuProfileItem
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
disabled={terminalDisabled}
|
||||||
|
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem testID="workspace-new-tab-menu-edit-profiles" onSelect={onEditProfiles}>
|
||||||
|
{t("workspace.tabs.actions.editTerminalProfiles")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,6 +354,40 @@ export interface WorkspaceDesktopTabRowItem {
|
|||||||
isClosingTab: boolean;
|
isClosingTab: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SplitActionButtonProps {
|
||||||
|
onPress: () => void;
|
||||||
|
label: string;
|
||||||
|
shortcutKeys: ShortcutKey[][] | null;
|
||||||
|
icon: "split-right" | "split-down";
|
||||||
|
}
|
||||||
|
|
||||||
|
function SplitActionButton({ onPress, label, shortcutKeys, icon }: SplitActionButtonProps) {
|
||||||
|
return (
|
||||||
|
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
||||||
|
<TooltipTrigger
|
||||||
|
onPress={onPress}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={label}
|
||||||
|
style={newTabActionButtonStyle}
|
||||||
|
>
|
||||||
|
{icon === "split-right" ? (
|
||||||
|
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
|
||||||
|
) : (
|
||||||
|
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
|
||||||
|
)}
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" align="center" offset={8}>
|
||||||
|
<View style={styles.newTabTooltipRow}>
|
||||||
|
<Text style={styles.newTabTooltipText}>{label}</Text>
|
||||||
|
{shortcutKeys ? (
|
||||||
|
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface WorkspaceDesktopTabsRowProps {
|
interface WorkspaceDesktopTabsRowProps {
|
||||||
paneId?: string;
|
paneId?: string;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
@@ -211,7 +405,7 @@ interface WorkspaceDesktopTabsRowProps {
|
|||||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||||
onCreateDraftTab: (input: { paneId?: string }) => void;
|
onCreateDraftTab: (input: { paneId?: string }) => void;
|
||||||
onCreateTerminalTab: (input: { paneId?: string }) => void;
|
onCreateTerminalTab: (input: { paneId?: string; profile?: TerminalProfileInput }) => void;
|
||||||
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
onCreateBrowserTab: (input: { paneId?: string }) => void;
|
||||||
showCreateBrowserTab?: boolean;
|
showCreateBrowserTab?: boolean;
|
||||||
disableCreateTerminal?: boolean;
|
disableCreateTerminal?: boolean;
|
||||||
@@ -539,8 +733,8 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
showPaneSplitActions = true,
|
showPaneSplitActions = true,
|
||||||
}: WorkspaceDesktopTabsRowProps) {
|
}: WorkspaceDesktopTabsRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
const newTabKeys = useShortcutKeys("workspace-tab-new");
|
||||||
const newTerminalKeys = useShortcutKeys("workspace-terminal-new");
|
|
||||||
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
|
||||||
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
|
||||||
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
|
||||||
@@ -641,19 +835,22 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onCreateTerminalTab({ paneId });
|
onCreateTerminalTab({ paneId });
|
||||||
}, [onCreateTerminalTab, paneId]);
|
}, [onCreateTerminalTab, paneId]);
|
||||||
|
|
||||||
|
const handleCreateTerminalWithProfile = useCallback(
|
||||||
|
(profile: TerminalProfileInput) => {
|
||||||
|
onCreateTerminalTab({ paneId, profile });
|
||||||
|
},
|
||||||
|
[onCreateTerminalTab, paneId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditProfiles = useCallback(() => {
|
||||||
|
router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href);
|
||||||
|
}, [normalizedServerId, router]);
|
||||||
|
|
||||||
const handleCreateBrowser = useCallback(() => {
|
const handleCreateBrowser = useCallback(() => {
|
||||||
onCreateBrowserTab({ paneId });
|
onCreateBrowserTab({ paneId });
|
||||||
}, [onCreateBrowserTab, paneId]);
|
}, [onCreateBrowserTab, paneId]);
|
||||||
|
|
||||||
const terminalDisabled = disableCreateTerminal || isWaitingOnTerminalReadiness;
|
const terminalDisabled = disableCreateTerminal || isWaitingOnTerminalReadiness;
|
||||||
const newTerminalActionButtonStyle = useCallback(
|
|
||||||
({ hovered, pressed }: PressableStateCallbackType) => [
|
|
||||||
styles.newTabActionButton,
|
|
||||||
terminalDisabled && styles.newTabActionButtonDisabled,
|
|
||||||
(hovered || pressed) && styles.newTabActionButtonHovered,
|
|
||||||
],
|
|
||||||
[terminalDisabled],
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderTab = useCallback(
|
const renderTab = useCallback(
|
||||||
({
|
({
|
||||||
@@ -762,120 +959,32 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
/>
|
/>
|
||||||
<WorkspaceInlineAddTabButton
|
<WorkspaceInlineAddTabButton
|
||||||
shortcutKeys={newTabKeys}
|
shortcutKeys={newTabKeys}
|
||||||
onPress={handleCreateAgentTab}
|
onCreateAgentTab={handleCreateAgentTab}
|
||||||
|
onCreateTerminal={handleCreateTerminal}
|
||||||
|
onCreateBrowser={handleCreateBrowser}
|
||||||
|
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
|
||||||
|
onEditProfiles={handleEditProfiles}
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
|
showCreateBrowserTab={showCreateBrowserTab}
|
||||||
|
terminalDisabled={terminalDisabled}
|
||||||
onLayout={handleInlineAddButtonLayout}
|
onLayout={handleInlineAddButtonLayout}
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
|
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
testID="workspace-new-agent-tab"
|
|
||||||
onPress={handleCreateAgentTab}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={t("workspace.tabs.actions.newAgent")}
|
|
||||||
style={newTabActionButtonStyle}
|
|
||||||
>
|
|
||||||
<ThemedSquarePen size={14} uniProps={mutedColorMapping} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<View style={styles.newTabTooltipRow}>
|
|
||||||
<Text style={styles.newTabTooltipText}>{t("workspace.tabs.actions.newAgent")}</Text>
|
|
||||||
{newTabKeys ? (
|
|
||||||
<Shortcut chord={newTabKeys} style={styles.newTabTooltipShortcut} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
testID="workspace-new-terminal"
|
|
||||||
onPress={handleCreateTerminal}
|
|
||||||
disabled={terminalDisabled}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
isWaitingOnTerminalReadiness
|
|
||||||
? t("workspace.tabs.actions.preparingTerminal")
|
|
||||||
: t("workspace.tabs.actions.newTerminal")
|
|
||||||
}
|
|
||||||
style={newTerminalActionButtonStyle}
|
|
||||||
>
|
|
||||||
<ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<View style={styles.newTabTooltipRow}>
|
|
||||||
<Text style={styles.newTabTooltipText}>
|
|
||||||
{isWaitingOnTerminalReadiness
|
|
||||||
? t("workspace.tabs.actions.preparingTerminalTooltip")
|
|
||||||
: t("workspace.tabs.actions.newTerminal")}
|
|
||||||
</Text>
|
|
||||||
{newTerminalKeys ? (
|
|
||||||
<Shortcut chord={newTerminalKeys} style={styles.newTabTooltipShortcut} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
{showCreateBrowserTab ? (
|
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
testID="workspace-new-browser"
|
|
||||||
onPress={handleCreateBrowser}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={t("workspace.tabs.actions.newBrowser")}
|
|
||||||
style={newTabActionButtonStyle}
|
|
||||||
>
|
|
||||||
<ThemedGlobe size={14} uniProps={mutedColorMapping} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<View style={styles.newTabTooltipRow}>
|
|
||||||
<Text style={styles.newTabTooltipText}>
|
|
||||||
{t("workspace.tabs.actions.newBrowser")}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
) : null}
|
|
||||||
{showPaneSplitActions ? (
|
{showPaneSplitActions ? (
|
||||||
<>
|
<>
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
<SplitActionButton
|
||||||
<TooltipTrigger
|
icon="split-right"
|
||||||
onPress={onSplitRight}
|
onPress={onSplitRight}
|
||||||
accessibilityRole="button"
|
label={t("workspace.tabs.actions.splitRight")}
|
||||||
accessibilityLabel={t("workspace.tabs.actions.splitRight")}
|
shortcutKeys={splitRightKeys}
|
||||||
style={newTabActionButtonStyle}
|
/>
|
||||||
>
|
<SplitActionButton
|
||||||
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
|
icon="split-down"
|
||||||
</TooltipTrigger>
|
onPress={onSplitDown}
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
label={t("workspace.tabs.actions.splitDown")}
|
||||||
<View style={styles.newTabTooltipRow}>
|
shortcutKeys={splitDownKeys}
|
||||||
<Text style={styles.newTabTooltipText}>
|
/>
|
||||||
{t("workspace.tabs.actions.splitRight")}
|
|
||||||
</Text>
|
|
||||||
{splitRightKeys ? (
|
|
||||||
<Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
|
|
||||||
<TooltipTrigger
|
|
||||||
onPress={onSplitDown}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={t("workspace.tabs.actions.splitDown")}
|
|
||||||
style={newTabActionButtonStyle}
|
|
||||||
>
|
|
||||||
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" align="center" offset={8}>
|
|
||||||
<View style={styles.newTabTooltipRow}>
|
|
||||||
<Text style={styles.newTabTooltipText}>
|
|
||||||
{t("workspace.tabs.actions.splitDown")}
|
|
||||||
</Text>
|
|
||||||
{splitDownKeys ? (
|
|
||||||
<Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} />
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
@@ -1039,9 +1148,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[2],
|
paddingHorizontal: theme.spacing[2],
|
||||||
},
|
},
|
||||||
inlineAddButton: {
|
inlineAddButton: {
|
||||||
paddingHorizontal: theme.spacing[1],
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
paddingHorizontal: theme.spacing[1],
|
||||||
},
|
},
|
||||||
tab: {
|
tab: {
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
@@ -1179,6 +1288,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
|
terminalProfileIconWrapper: {
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore];
|
const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore];
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
@@ -172,9 +173,22 @@ import { isAbsolutePath } from "@/utils/path";
|
|||||||
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
|
||||||
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
|
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
|
||||||
import { useContainerWidthBelow } from "@/hooks/use-container-width";
|
import { useContainerWidthBelow } from "@/hooks/use-container-width";
|
||||||
import { buildHostRootRoute, buildSettingsHostRoute } from "@/utils/host-routes";
|
import {
|
||||||
|
buildHostRootRoute,
|
||||||
|
buildSettingsHostRoute,
|
||||||
|
buildSettingsHostSectionRoute,
|
||||||
|
} from "@/utils/host-routes";
|
||||||
import { canCreateWorkspaceTerminal } from "@/screens/workspace/terminals/state";
|
import { canCreateWorkspaceTerminal } from "@/screens/workspace/terminals/state";
|
||||||
import { useWorkspaceTerminals } from "@/screens/workspace/terminals/use-workspace-terminals";
|
import {
|
||||||
|
useWorkspaceTerminals,
|
||||||
|
type TerminalProfileInput,
|
||||||
|
} from "@/screens/workspace/terminals/use-workspace-terminals";
|
||||||
|
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
||||||
|
import {
|
||||||
|
getTerminalProfileIcon,
|
||||||
|
resolveTerminalProfiles,
|
||||||
|
} from "@getpaseo/protocol/terminal-profiles";
|
||||||
|
import { getProviderIcon } from "@/components/provider-icons";
|
||||||
import {
|
import {
|
||||||
createWorkspaceFileTabTarget,
|
createWorkspaceFileTabTarget,
|
||||||
normalizeWorkspaceFileLocation,
|
normalizeWorkspaceFileLocation,
|
||||||
@@ -241,6 +255,19 @@ const ThemedSettings = withUnistyles(Settings);
|
|||||||
const ThemedPanelRight = withUnistyles(PanelRight);
|
const ThemedPanelRight = withUnistyles(PanelRight);
|
||||||
const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon);
|
const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon);
|
||||||
|
|
||||||
|
interface DynamicProviderIconProps {
|
||||||
|
iconKey: string;
|
||||||
|
size: number;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) {
|
||||||
|
const Icon = getProviderIcon(iconKey);
|
||||||
|
return <Icon size={size} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
|
||||||
|
|
||||||
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||||
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||||
|
|
||||||
@@ -927,6 +954,7 @@ function useCloseTabs(): UseCloseTabsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WorkspaceHeaderMenuProps {
|
interface WorkspaceHeaderMenuProps {
|
||||||
|
normalizedServerId: string;
|
||||||
normalizedWorkspaceId: string;
|
normalizedWorkspaceId: string;
|
||||||
currentBranchName: string | null;
|
currentBranchName: string | null;
|
||||||
showWorkspaceSetup: boolean;
|
showWorkspaceSetup: boolean;
|
||||||
@@ -942,12 +970,55 @@ interface WorkspaceHeaderMenuProps {
|
|||||||
menuSettingsIcon: ReactElement;
|
menuSettingsIcon: ReactElement;
|
||||||
onCreateDraftTab: () => void;
|
onCreateDraftTab: () => void;
|
||||||
onCreateTerminal: () => void;
|
onCreateTerminal: () => void;
|
||||||
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
onCreateBrowser: () => void;
|
onCreateBrowser: () => void;
|
||||||
onOpenImportSheet: () => void;
|
onOpenImportSheet: () => void;
|
||||||
onCopyWorkspacePath: () => void;
|
onCopyWorkspacePath: () => void;
|
||||||
onCopyBranchName: () => void;
|
onCopyBranchName: () => void;
|
||||||
onOpenSetupTab: () => void;
|
onOpenSetupTab: () => void;
|
||||||
}
|
}
|
||||||
|
interface HeaderMenuProfileItemProps {
|
||||||
|
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
|
||||||
|
disabled: boolean;
|
||||||
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderMenuProfileItem({
|
||||||
|
profile,
|
||||||
|
disabled,
|
||||||
|
onCreateTerminalWithProfile,
|
||||||
|
}: HeaderMenuProfileItemProps) {
|
||||||
|
const handleSelect = useCallback(() => {
|
||||||
|
onCreateTerminalWithProfile({
|
||||||
|
name: profile.name,
|
||||||
|
command: profile.command,
|
||||||
|
args: profile.args,
|
||||||
|
});
|
||||||
|
}, [onCreateTerminalWithProfile, profile]);
|
||||||
|
|
||||||
|
const icon = getTerminalProfileIcon(profile);
|
||||||
|
|
||||||
|
const leading = useMemo(() => {
|
||||||
|
if (!icon) {
|
||||||
|
return (
|
||||||
|
<View style={styles.headerMenuProfileIconWrapper}>
|
||||||
|
<ThemedSquareTerminal size={16} uniProps={mutedColorMapping} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<View style={styles.headerMenuProfileIconWrapper}>
|
||||||
|
<ThemedDynamicProviderIcon iconKey={icon} size={16} uniProps={mutedColorMapping} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, [icon]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
|
||||||
|
{profile.name}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function WorkspaceHeaderMenuTriggerIcon({
|
function WorkspaceHeaderMenuTriggerIcon({
|
||||||
hovered,
|
hovered,
|
||||||
@@ -964,6 +1035,7 @@ function WorkspaceHeaderMenuTriggerIcon({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WorkspaceHeaderMenu({
|
function WorkspaceHeaderMenu({
|
||||||
|
normalizedServerId,
|
||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
currentBranchName,
|
currentBranchName,
|
||||||
showWorkspaceSetup,
|
showWorkspaceSetup,
|
||||||
@@ -979,6 +1051,7 @@ function WorkspaceHeaderMenu({
|
|||||||
menuSettingsIcon,
|
menuSettingsIcon,
|
||||||
onCreateDraftTab,
|
onCreateDraftTab,
|
||||||
onCreateTerminal,
|
onCreateTerminal,
|
||||||
|
onCreateTerminalWithProfile,
|
||||||
onCreateBrowser,
|
onCreateBrowser,
|
||||||
onOpenImportSheet,
|
onOpenImportSheet,
|
||||||
onCopyWorkspacePath,
|
onCopyWorkspacePath,
|
||||||
@@ -986,6 +1059,17 @@ function WorkspaceHeaderMenu({
|
|||||||
onOpenSetupTab,
|
onOpenSetupTab,
|
||||||
}: WorkspaceHeaderMenuProps) {
|
}: WorkspaceHeaderMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const router = useRouter();
|
||||||
|
const { config } = useDaemonConfig(normalizedServerId);
|
||||||
|
const profiles = useMemo(
|
||||||
|
() => resolveTerminalProfiles(config?.terminalProfiles),
|
||||||
|
[config?.terminalProfiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEditProfiles = useCallback(() => {
|
||||||
|
router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href);
|
||||||
|
}, [normalizedServerId, router]);
|
||||||
|
|
||||||
const renderTriggerIcon = useCallback(
|
const renderTriggerIcon = useCallback(
|
||||||
({ hovered, open }: { hovered: boolean; open: boolean }) => (
|
({ hovered, open }: { hovered: boolean; open: boolean }) => (
|
||||||
<WorkspaceHeaderMenuTriggerIcon hovered={hovered} open={open} isMobile={isMobile} />
|
<WorkspaceHeaderMenuTriggerIcon hovered={hovered} open={open} isMobile={isMobile} />
|
||||||
@@ -1011,6 +1095,7 @@ function WorkspaceHeaderMenu({
|
|||||||
>
|
>
|
||||||
{t("workspace.header.actions.newAgent")}
|
{t("workspace.header.actions.newAgent")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-header-new-terminal"
|
testID="workspace-header-new-terminal"
|
||||||
leading={menuNewTerminalIcon}
|
leading={menuNewTerminalIcon}
|
||||||
@@ -1019,6 +1104,22 @@ function WorkspaceHeaderMenu({
|
|||||||
>
|
>
|
||||||
{t("workspace.header.actions.newTerminal")}
|
{t("workspace.header.actions.newTerminal")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{profiles.map((profile) => (
|
||||||
|
<HeaderMenuProfileItem
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
disabled={createTerminalDisabled}
|
||||||
|
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="workspace-header-edit-terminal-profiles"
|
||||||
|
onSelect={handleEditProfiles}
|
||||||
|
>
|
||||||
|
{t("workspace.tabs.actions.editTerminalProfiles")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
{showCreateBrowserTab ? (
|
{showCreateBrowserTab ? (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID="workspace-header-new-browser"
|
testID="workspace-header-new-browser"
|
||||||
@@ -1094,6 +1195,7 @@ interface WorkspaceHeaderTitleBarProps {
|
|||||||
menuSettingsIcon: ReactElement;
|
menuSettingsIcon: ReactElement;
|
||||||
onCreateDraftTab: () => void;
|
onCreateDraftTab: () => void;
|
||||||
onCreateTerminal: () => void;
|
onCreateTerminal: () => void;
|
||||||
|
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
|
||||||
onCreateBrowser: () => void;
|
onCreateBrowser: () => void;
|
||||||
onOpenImportSheet: () => void;
|
onOpenImportSheet: () => void;
|
||||||
onCopyWorkspacePath: () => void;
|
onCopyWorkspacePath: () => void;
|
||||||
@@ -1128,6 +1230,7 @@ function WorkspaceHeaderTitleBar({
|
|||||||
menuSettingsIcon,
|
menuSettingsIcon,
|
||||||
onCreateDraftTab,
|
onCreateDraftTab,
|
||||||
onCreateTerminal,
|
onCreateTerminal,
|
||||||
|
onCreateTerminalWithProfile,
|
||||||
onCreateBrowser,
|
onCreateBrowser,
|
||||||
onOpenImportSheet,
|
onOpenImportSheet,
|
||||||
onCopyWorkspacePath,
|
onCopyWorkspacePath,
|
||||||
@@ -1165,6 +1268,7 @@ function WorkspaceHeaderTitleBar({
|
|||||||
)}
|
)}
|
||||||
<View style={styles.compactHeaderMenuCluster}>
|
<View style={styles.compactHeaderMenuCluster}>
|
||||||
<WorkspaceHeaderMenu
|
<WorkspaceHeaderMenu
|
||||||
|
normalizedServerId={normalizedServerId}
|
||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
currentBranchName={currentBranchName}
|
currentBranchName={currentBranchName}
|
||||||
showWorkspaceSetup={showWorkspaceSetup}
|
showWorkspaceSetup={showWorkspaceSetup}
|
||||||
@@ -1180,6 +1284,7 @@ function WorkspaceHeaderTitleBar({
|
|||||||
menuSettingsIcon={menuSettingsIcon}
|
menuSettingsIcon={menuSettingsIcon}
|
||||||
onCreateDraftTab={onCreateDraftTab}
|
onCreateDraftTab={onCreateDraftTab}
|
||||||
onCreateTerminal={onCreateTerminal}
|
onCreateTerminal={onCreateTerminal}
|
||||||
|
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
|
||||||
onCreateBrowser={onCreateBrowser}
|
onCreateBrowser={onCreateBrowser}
|
||||||
onOpenImportSheet={onOpenImportSheet}
|
onOpenImportSheet={onOpenImportSheet}
|
||||||
onCopyWorkspacePath={onCopyWorkspacePath}
|
onCopyWorkspacePath={onCopyWorkspacePath}
|
||||||
@@ -2416,6 +2521,13 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
const handleCreateTerminal = useStableEvent(createTerminal);
|
const handleCreateTerminal = useStableEvent(createTerminal);
|
||||||
|
|
||||||
|
const handleCreateTerminalWithProfile = useCallback(
|
||||||
|
(profile: TerminalProfileInput) => {
|
||||||
|
createTerminal({ profile });
|
||||||
|
},
|
||||||
|
[createTerminal],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateBrowserTab = useCallback(
|
const handleCreateBrowserTab = useCallback(
|
||||||
(input?: { paneId?: string }) => {
|
(input?: { paneId?: string }) => {
|
||||||
if (!persistenceKey || !getIsElectron()) {
|
if (!persistenceKey || !getIsElectron()) {
|
||||||
@@ -3508,6 +3620,7 @@ function WorkspaceScreenContent({
|
|||||||
menuSettingsIcon={menuSettingsIcon}
|
menuSettingsIcon={menuSettingsIcon}
|
||||||
onCreateDraftTab={handleCreateDraftTab}
|
onCreateDraftTab={handleCreateDraftTab}
|
||||||
onCreateTerminal={handleCreateTerminal}
|
onCreateTerminal={handleCreateTerminal}
|
||||||
|
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
|
||||||
onCreateBrowser={handleCreateBrowserTab}
|
onCreateBrowser={handleCreateBrowserTab}
|
||||||
onOpenImportSheet={openImportSheet}
|
onOpenImportSheet={openImportSheet}
|
||||||
onCopyWorkspacePath={handleCopyWorkspacePath}
|
onCopyWorkspacePath={handleCopyWorkspacePath}
|
||||||
@@ -3845,6 +3958,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
|
headerMenuProfileIconWrapper: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
tabsContainer: {
|
tabsContainer: {
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ export const HOST_SECTION_SLUGS = [
|
|||||||
"agents",
|
"agents",
|
||||||
"workspaces",
|
"workspaces",
|
||||||
"providers",
|
"providers",
|
||||||
|
"terminals",
|
||||||
"host",
|
"host",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,18 @@ const MutableMetadataGenerationConfigSchema = z
|
|||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
|
export const TerminalProfileSchema = z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
command: z.string(),
|
||||||
|
args: z.array(z.string()).optional(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export type TerminalProfile = z.infer<typeof TerminalProfileSchema>;
|
||||||
|
|
||||||
export const MutableDaemonConfigSchema = z
|
export const MutableDaemonConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
mcp: z
|
mcp: z
|
||||||
@@ -127,6 +139,7 @@ export const MutableDaemonConfigSchema = z
|
|||||||
metadataGeneration: MutableMetadataGenerationConfigSchema.default({ providers: [] }),
|
metadataGeneration: MutableMetadataGenerationConfigSchema.default({ providers: [] }),
|
||||||
autoArchiveAfterMerge: z.boolean().default(false),
|
autoArchiveAfterMerge: z.boolean().default(false),
|
||||||
appendSystemPrompt: z.string().default(""),
|
appendSystemPrompt: z.string().default(""),
|
||||||
|
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -139,6 +152,7 @@ export const MutableDaemonConfigPatchSchema = z
|
|||||||
metadataGeneration: MutableMetadataGenerationConfigSchema.partial().optional(),
|
metadataGeneration: MutableMetadataGenerationConfigSchema.partial().optional(),
|
||||||
autoArchiveAfterMerge: z.boolean().optional(),
|
autoArchiveAfterMerge: z.boolean().optional(),
|
||||||
appendSystemPrompt: z.string().optional(),
|
appendSystemPrompt: z.string().optional(),
|
||||||
|
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
||||||
})
|
})
|
||||||
.partial()
|
.partial()
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
export const BUILTIN_PROVIDER_ICON_NAMES = [
|
||||||
|
"claude",
|
||||||
|
"codex",
|
||||||
|
"copilot",
|
||||||
|
"kiro",
|
||||||
|
"omp",
|
||||||
|
"opencode",
|
||||||
|
"pi",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ACP_PROVIDER_ICON_NAMES = [
|
||||||
|
"agoragentic-acp",
|
||||||
|
"amp-acp",
|
||||||
|
"auggie",
|
||||||
|
"autohand",
|
||||||
|
"cline",
|
||||||
|
"codebuddy-code",
|
||||||
|
"codewhale",
|
||||||
|
"cortex-code",
|
||||||
|
"corust-agent",
|
||||||
|
"crow-cli",
|
||||||
|
"cursor",
|
||||||
|
"deepagents",
|
||||||
|
"dimcode",
|
||||||
|
"dirac",
|
||||||
|
"factory-droid",
|
||||||
|
"fast-agent",
|
||||||
|
"gemini",
|
||||||
|
"glm-acp-agent",
|
||||||
|
"goose",
|
||||||
|
"grok",
|
||||||
|
"junie",
|
||||||
|
"kilo",
|
||||||
|
"kimi",
|
||||||
|
"minion-code",
|
||||||
|
"mistral-vibe",
|
||||||
|
"nova",
|
||||||
|
"poolside",
|
||||||
|
"qoder",
|
||||||
|
"qwen-code",
|
||||||
|
"sigit",
|
||||||
|
"stakpak",
|
||||||
|
"vtcode",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TERMINAL_PROFILE_ICON_NAMES = ["agy"];
|
||||||
|
|
||||||
|
export const KNOWN_PROVIDER_ICON_NAMES = [
|
||||||
|
...BUILTIN_PROVIDER_ICON_NAMES,
|
||||||
|
...ACP_PROVIDER_ICON_NAMES,
|
||||||
|
...TERMINAL_PROFILE_ICON_NAMES,
|
||||||
|
];
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DEFAULT_TERMINAL_PROFILES,
|
||||||
|
getTerminalProfileIcon,
|
||||||
|
guessTerminalProfileIcon,
|
||||||
|
resolveTerminalProfiles,
|
||||||
|
} from "./terminal-profiles.js";
|
||||||
|
|
||||||
|
describe("resolveTerminalProfiles", () => {
|
||||||
|
it("returns defaults when undefined", () => {
|
||||||
|
const result = resolveTerminalProfiles(undefined);
|
||||||
|
expect(result).toBe(DEFAULT_TERMINAL_PROFILES);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array as-is when defined as empty", () => {
|
||||||
|
const result = resolveTerminalProfiles([]);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a custom array as-is", () => {
|
||||||
|
const custom = [{ id: "zsh", name: "Zsh", command: "zsh" }];
|
||||||
|
const result = resolveTerminalProfiles(custom);
|
||||||
|
expect(result).toBe(custom);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default profiles include claude, codex, opencode", () => {
|
||||||
|
const ids = DEFAULT_TERMINAL_PROFILES.map((p) => p.id);
|
||||||
|
expect(ids).toContain("claude");
|
||||||
|
expect(ids).toContain("codex");
|
||||||
|
expect(ids).toContain("opencode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("claude profile has the correct icon", () => {
|
||||||
|
const claude = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "claude");
|
||||||
|
expect(claude?.icon).toBe("claude");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("codex profile has the correct icon", () => {
|
||||||
|
const codex = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "codex");
|
||||||
|
expect(codex?.icon).toBe("codex");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opencode profile has the correct icon", () => {
|
||||||
|
const opencode = DEFAULT_TERMINAL_PROFILES.find((p) => p.id === "opencode");
|
||||||
|
expect(opencode?.icon).toBe("opencode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("guessTerminalProfileIcon", () => {
|
||||||
|
it.each([
|
||||||
|
["claude", "claude"],
|
||||||
|
["codex", "codex"],
|
||||||
|
["opencode", "opencode"],
|
||||||
|
["copilot", "copilot"],
|
||||||
|
["kiro", "kiro"],
|
||||||
|
["omp", "omp"],
|
||||||
|
["pi", "pi"],
|
||||||
|
["gemini", "gemini"],
|
||||||
|
["cursor", "cursor"],
|
||||||
|
["goose", "goose"],
|
||||||
|
["grok", "grok"],
|
||||||
|
["kimi", "kimi"],
|
||||||
|
["Claude", "claude"],
|
||||||
|
["CODEX", "codex"],
|
||||||
|
["/usr/local/bin/claude", "claude"],
|
||||||
|
["C:\\\\Program Files\\\\Codex\\\\codex.exe", "codex"],
|
||||||
|
["/usr/local/bin/gemini", "gemini"],
|
||||||
|
["agy", "agy"],
|
||||||
|
["/usr/local/bin/agy", "agy"],
|
||||||
|
])("guesses %s -> %s", (command, expected) => {
|
||||||
|
expect(guessTerminalProfileIcon(command)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([["zsh"], ["bash"], ["fish"], [""], ["/usr/bin/foo"], ["cursor-agent"]])(
|
||||||
|
"returns undefined for unknown command %s",
|
||||||
|
(command) => {
|
||||||
|
expect(guessTerminalProfileIcon(command)).toBeUndefined();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getTerminalProfileIcon", () => {
|
||||||
|
it("returns the explicit icon when set", () => {
|
||||||
|
const profile = { id: "1", name: "Foo", command: "zsh", icon: "claude" };
|
||||||
|
expect(getTerminalProfileIcon(profile)).toBe("claude");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("guesses the icon when none is set", () => {
|
||||||
|
const profile = { id: "1", name: "Foo", command: "claude" };
|
||||||
|
expect(getTerminalProfileIcon(profile)).toBe("claude");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when no icon is set and command is unknown", () => {
|
||||||
|
const profile = { id: "1", name: "Foo", command: "zsh" };
|
||||||
|
expect(getTerminalProfileIcon(profile)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { TerminalProfile } from "./messages.js";
|
||||||
|
import { KNOWN_PROVIDER_ICON_NAMES } from "./provider-icon-names.js";
|
||||||
|
|
||||||
|
export const DEFAULT_TERMINAL_PROFILES: readonly TerminalProfile[] = [
|
||||||
|
{ id: "claude", name: "Claude Code", command: "claude", icon: "claude" },
|
||||||
|
{ id: "codex", name: "Codex", command: "codex", icon: "codex" },
|
||||||
|
{ id: "opencode", name: "OpenCode", command: "opencode", icon: "opencode" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const WELL_KNOWN_COMMAND_ICONS = new Map(KNOWN_PROVIDER_ICON_NAMES.map((name) => [name, name]));
|
||||||
|
|
||||||
|
function getCommandBaseName(command: string): string {
|
||||||
|
const lastSlash = command.lastIndexOf("/");
|
||||||
|
const lastBackslash = command.lastIndexOf("\\");
|
||||||
|
const start = Math.max(lastSlash, lastBackslash) + 1;
|
||||||
|
const base = command.slice(start).toLowerCase();
|
||||||
|
const dotIndex = base.indexOf(".");
|
||||||
|
return dotIndex > 0 ? base.slice(0, dotIndex) : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guessTerminalProfileIcon(command: string): string | undefined {
|
||||||
|
return WELL_KNOWN_COMMAND_ICONS.get(getCommandBaseName(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTerminalProfileIcon(profile: TerminalProfile): string | undefined {
|
||||||
|
return profile.icon ?? guessTerminalProfileIcon(profile.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTerminalProfiles(
|
||||||
|
terminalProfiles: TerminalProfile[] | undefined,
|
||||||
|
): readonly TerminalProfile[] {
|
||||||
|
if (terminalProfiles === undefined) {
|
||||||
|
return DEFAULT_TERMINAL_PROFILES;
|
||||||
|
}
|
||||||
|
return terminalProfiles;
|
||||||
|
}
|
||||||
@@ -121,6 +121,7 @@ import type { PushNotificationSender } from "./push/notifications.js";
|
|||||||
import { getOrCreateServerId } from "./server-id.js";
|
import { getOrCreateServerId } from "./server-id.js";
|
||||||
import { resolveDaemonVersion } from "./daemon-version.js";
|
import { resolveDaemonVersion } from "./daemon-version.js";
|
||||||
import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
|
import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
|
||||||
|
import type { TerminalProfile } from "@getpaseo/protocol/messages";
|
||||||
import type {
|
import type {
|
||||||
AgentProviderRuntimeSettingsMap,
|
AgentProviderRuntimeSettingsMap,
|
||||||
ProviderOverride,
|
ProviderOverride,
|
||||||
@@ -235,6 +236,7 @@ export interface PaseoDaemonConfig {
|
|||||||
mcpInjectIntoAgents?: boolean;
|
mcpInjectIntoAgents?: boolean;
|
||||||
autoArchiveAfterMerge?: boolean;
|
autoArchiveAfterMerge?: boolean;
|
||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
|
terminalProfiles?: TerminalProfile[];
|
||||||
staticDir: string;
|
staticDir: string;
|
||||||
mcpDebug: boolean;
|
mcpDebug: boolean;
|
||||||
isDev?: boolean;
|
isDev?: boolean;
|
||||||
@@ -310,6 +312,9 @@ export async function createPaseoDaemon(
|
|||||||
},
|
},
|
||||||
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
|
autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
|
||||||
appendSystemPrompt: config.appendSystemPrompt ?? "",
|
appendSystemPrompt: config.appendSystemPrompt ?? "",
|
||||||
|
...(config.terminalProfiles !== undefined
|
||||||
|
? { terminalProfiles: config.terminalProfiles }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ function resolveStaticLoadConfigSettings(
|
|||||||
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
|
cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
|
||||||
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
|
autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
|
||||||
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
|
appendSystemPrompt: resolveAppendSystemPrompt(persisted),
|
||||||
|
terminalProfiles: persisted.daemon?.terminalProfiles,
|
||||||
hostnames: mergeHostnames([
|
hostnames: mergeHostnames([
|
||||||
persisted.daemon?.hostnames,
|
persisted.daemon?.hostnames,
|
||||||
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
|
parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
|
||||||
@@ -352,6 +353,7 @@ export function loadConfig(
|
|||||||
mcpInjectIntoAgents,
|
mcpInjectIntoAgents,
|
||||||
autoArchiveAfterMerge,
|
autoArchiveAfterMerge,
|
||||||
appendSystemPrompt,
|
appendSystemPrompt,
|
||||||
|
terminalProfiles,
|
||||||
hostnames,
|
hostnames,
|
||||||
appBaseUrl,
|
appBaseUrl,
|
||||||
} = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
|
} = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
|
||||||
@@ -385,6 +387,7 @@ export function loadConfig(
|
|||||||
mcpInjectIntoAgents,
|
mcpInjectIntoAgents,
|
||||||
autoArchiveAfterMerge,
|
autoArchiveAfterMerge,
|
||||||
appendSystemPrompt,
|
appendSystemPrompt,
|
||||||
|
terminalProfiles,
|
||||||
mcpDebug: env.MCP_DEBUG === "1",
|
mcpDebug: env.MCP_DEBUG === "1",
|
||||||
isDev: resolvePaseoNodeEnv(env) === "development",
|
isDev: resolvePaseoNodeEnv(env) === "development",
|
||||||
agentStoragePath: path.join(paseoHome, "agents"),
|
agentStoragePath: path.join(paseoHome, "agents"),
|
||||||
|
|||||||
@@ -210,6 +210,9 @@ function mergeMutableConfigIntoPersistedConfig(params: {
|
|||||||
},
|
},
|
||||||
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
|
autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
|
||||||
appendSystemPrompt: mutable.appendSystemPrompt,
|
appendSystemPrompt: mutable.appendSystemPrompt,
|
||||||
|
...(mutable.terminalProfiles !== undefined
|
||||||
|
? { terminalProfiles: mutable.terminalProfiles }
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
agents: nextAgents,
|
agents: nextAgents,
|
||||||
} as PersistedConfig;
|
} as PersistedConfig;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "./agent/provider-launch-config.js";
|
} from "./agent/provider-launch-config.js";
|
||||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||||
import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js";
|
import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js";
|
||||||
|
import { TerminalProfileSchema } from "@getpaseo/protocol/messages";
|
||||||
|
|
||||||
export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
|
||||||
export const LogFormatSchema = z.enum(["pretty", "json"]);
|
export const LogFormatSchema = z.enum(["pretty", "json"]);
|
||||||
@@ -223,6 +224,7 @@ export const PersistedConfigSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
autoArchiveAfterMerge: z.boolean().optional(),
|
autoArchiveAfterMerge: z.boolean().optional(),
|
||||||
appendSystemPrompt: z.string().optional(),
|
appendSystemPrompt: z.string().optional(),
|
||||||
|
terminalProfiles: z.array(TerminalProfileSchema).optional(),
|
||||||
cors: z
|
cors: z
|
||||||
.object({
|
.object({
|
||||||
allowedOrigins: z.array(z.string()).optional(),
|
allowedOrigins: z.array(z.string()).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user