Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions webviews/src/agent-session/acpmux/ComposerContext.gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const baseProps = {

/// 21 machines, as `acpmux host ls` lists them on a Mac with a fleet.
const fleetProps = { ...baseProps, ...fleetPeers() };
/// The account's Cloud machines acpmux does not list (cloudMachines.ts): paused, ready and booting.
const accountMachines = [
{ id: "vm-build", name: "build box", stage: "paused" },
{ id: "vm-eval", name: "eval box", stage: "ready" },
{ id: "vm-fresh", name: "fresh box", stage: "booting" },
];

/// The New Tab page's saved environments (SavedEnvironmentRows), with `current` picked.
function savedProps(current?: string): ComponentProps<typeof ComposerContext> {
Expand Down Expand Up @@ -417,6 +423,27 @@ export default componentEntry<ComponentProps<typeof ComposerContext>>({
await ctx.waitFor(() => ctx.document.querySelector('.acpmux-env-popover [role="option"]'));
},
},
"environment-picker-cloud-machines": {
note: "Preview: the environment picker above, then the account's Cloud machines acpmux does not list (`cloudMachine.list`) in Cloud and VMs, each with its stage: build box is paused (a pick resumes it), eval box is ready (a pick opens it), fresh box is still booting. They take no ⌥⌘ digit and are not greyed.",
props: {
...baseProps,
environments: true,
peers: ["gpu-box", "Cloud machine"],
peerKinds: { "gpu-box": "ssh", "Cloud machine": "cloud" },
peerConnected: { "gpu-box": true, "Cloud machine": false },
cloudMachines: accountMachines,
onCloudMachine: () => undefined,
},
play: async (ctx) => {
await ctx.click({ role: "button", name: /^Environment/ });
await ctx.waitFor(() => ctx.document.querySelector('.acpmux-env-popover [role="option"]'));
},
},
"computer-menu-cloud-machines": {
note: "Previews off: the plain Computer menu lists the account's Cloud machines acpmux does not list the same way, with their stage on the second line.",
props: { ...baseProps, cloudMachines: accountMachines, onCloudMachine: () => undefined },
play: openComputer,
},
"environment-picker-fleet": {
height: 760,
note: "Preview: every machine acpmux lists (21, from `host ls`), in This Mac, My machines and Cloud and VMs sections; in each the machines a chat can start on come first, in number order, and the rest are greyed with their state (Offline, Reconnecting, Connecting) and are no pick.",
Expand Down
59 changes: 51 additions & 8 deletions webviews/src/agent-session/acpmux/ComposerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import {
type SavedEnvironmentRows,
} from "./EnvironmentPicker";
import { machineColor } from "./machineColor";
import {
isCloudMachineRow,
refreshCloudMachines,
runCloudMachine,
useCloudMachines,
withCloudMachines,
type CloudMachine,
} from "./cloudMachines";
import {
environmentReady,
environmentSections,
Expand Down Expand Up @@ -60,7 +68,7 @@ type Location = {
section?: string;
disabled?: boolean;
};
type Computer = Location & { kind: EnvironmentKind; connected?: boolean; state?: string };
type Computer = Location & { kind: EnvironmentKind; connected?: boolean; state?: string; cloudMachine?: CloudMachine };
const NO_PEER_KINDS: Record<string, "ssh" | "cloud"> = {};
const NO_PEER_CONNECTED: Record<string, boolean> = {};
const NO_PEER_STATES: Record<string, string> = {};
Expand Down Expand Up @@ -93,9 +101,11 @@ export function ComposerContext({
onOffload,
offloads = NO_OFFLOADS,
saved,
onRefreshMachines,
onRefreshMachines: onRefreshPeers,
folderNote,
onComputer,
cloudMachines,
onCloudMachine = (machine) => void runCloudMachine(machine),
}: {
summary?: Summary;
sessions?: Session[];
Expand Down Expand Up @@ -148,14 +158,36 @@ export function ComposerContext({
/// A new chat's machine was picked (`local` for this Mac) where no folder of it is known; with a
/// known folder the pick is `onProject(folder, peer)`.
onComputer?(computer: string): void;
/// The account's Cloud machines (cloudMachines.ts); by default the app's list, read again as the
/// machine list opens. Only a new chat lists them.
cloudMachines?: readonly CloudMachine[];
/// A pick of one acpmux does not list: it resumes (paused) or opens; by default through the app.
onCloudMachine?(machine: CloudMachine): void;
}) {
const pickerEnabled = environments || started;
const newChat = !started && onProject !== undefined;
const accountMachines = useCloudMachines();
const shownMachines = cloudMachines ?? accountMachines;
// Every machine acpmux lists (`_acpmux/status` peers) and every host its chats ran on, with each
// one's transport and state, in both the environment picker and the plain Computer menu.
const computers = useMemo(
() => availableComputers(summary, sessions, peers, peerKinds, localName, peerConnected, peerStates),
[summary, sessions, peers, peerKinds, peerConnected, peerStates, localName],
);
// one's transport and state, in both the environment picker and the plain Computer menu; for a
// new chat, then the account's Cloud machines acpmux does not list (a paused one, say).
const computers = useMemo(() => {
const listed = availableComputers(summary, sessions, peers, peerKinds, localName, peerConnected, peerStates);
if (!newChat) return listed;
const detail = t(CONTEXT_LABELS.cloud);
return withCloudMachines(listed, shownMachines, peers, computerId(summary)).map((row) =>
isCloudMachineRow(row) ? { ...row, detail } : row,
);
}, [summary, sessions, peers, peerKinds, peerConnected, peerStates, localName, newChat, shownMachines]);
// The list opened, and every few seconds while it is open: acpmux's peers and, for a new chat,
// the app's Cloud machines are read again.
const onRefreshMachines =
onRefreshPeers || (newChat && !cloudMachines)
? () => {
onRefreshPeers?.();
if (newChat && !cloudMachines) void refreshCloudMachines();
}
: undefined;
// The plain Computer menu lists them in the picker's sections and order (environments.ts), one
// line each; a machine a chat cannot start on yet says its state on a second line.
const menuComputers = useMemo(() => {
Expand All @@ -165,7 +197,10 @@ export function ComposerContext({
return {
...computer,
section: t(SECTION_LABELS[sections.get(computer.id) ?? "fleet"]),
...(ready ? {} : { disabled: true, detail: environmentStateLabel(computer) }),
// An account Cloud machine acpmux does not list says what a pick does, and is a pick.
...(ready
? {}
: { ...(isCloudMachineRow(computer) ? {} : { disabled: true }), detail: environmentStateLabel(computer) }),
};
});
}, [computers]);
Expand Down Expand Up @@ -239,7 +274,14 @@ export function ComposerContext({
});
}
: undefined;
// An account Cloud machine acpmux does not list runs its machine action: no chat starts on it here.
const pickCloudMachine = (id: string) => {
const machine = computers.find((computer) => computer.id === id)?.cloudMachine;
if (machine && !readOnly) onCloudMachine(machine);
return machine !== undefined;
};
const pickComputer = (id: string) => {
if (pickCloudMachine(id)) return;
if (!readOnly && id !== selectedComputer) {
setSelectedComputer(id);
const folder = availableFolders(summary, sessions, id)[0]?.id;
Expand All @@ -255,6 +297,7 @@ export function ComposerContext({
const savedRows = environmentsShown && !readOnly ? saved : undefined;
const savedPick = savedRows?.environments.find((environment) => environment.id === savedRows.current);
const pickEnvironment = (id: string) => {
if (pickCloudMachine(id)) return;
if (savedRows?.current !== undefined) savedRows.onClear();
pickComputer(id);
};
Expand Down
22 changes: 19 additions & 3 deletions webviews/src/agent-session/acpmux/EnvironmentPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
numberedEnvironments,
orderEnvironments,
} from "./environments";
import { type CloudMachine, isCloudMachineRow } from "./cloudMachines";
import { useLocationOpener, useLocationPopover } from "./locationPopover";
import { machineColor } from "./machineColor";

Expand All @@ -39,8 +40,21 @@ const STATE_LABELS: Record<string, StringKey> = {
reconnecting: "header.reconnecting",
};
export function environmentStateLabel(environment: Environment): string {
if (isCloudMachineRow(environment)) return cloudMachineLabel(environment.cloudMachine);
return t(STATE_LABELS[environment.state ?? ""] ?? "composer.envNotConnected");
}
/// An account Cloud machine acpmux does not list (cloudMachines.ts): its stage, and what a pick does.
const CLOUD_STAGE_LABELS: Record<string, StringKey> = {
ready: "composer.envOpenMachine",
paused: "composer.envPausedResume",
requesting: "composer.envStarting",
creating: "composer.envStarting",
booting: "composer.envStarting",
connecting: "composer.envStarting",
};
function cloudMachineLabel(machine: CloudMachine): string {
return t(CLOUD_STAGE_LABELS[machine.stage] ?? "composer.envNotConnected");
}

/// How often an open machine list reads the machines again: acpmux sends no peer events, so a
/// machine that connects or drops while the list is open shows within this long.
Expand Down Expand Up @@ -335,7 +349,7 @@ export function EnvironmentPicker({
// A machine still connecting or down is listed, greyed, in its place, but is no pick: the list
// stays open.
const machine = positions.get(value)?.environment;
if (machine && !environmentReady(machine)) return;
if (machine && !environmentReady(machine) && !isCloudMachineRow(machine)) return;
close();
if (action) action.run();
else if (value === currentRow) focusPrompt();
Expand Down Expand Up @@ -404,15 +418,17 @@ export function EnvironmentPicker({
const { environment, index, sectionStart } = position;
const number = numbers.get(environment.id);
const ready = environmentReady(environment);
// An account Cloud machine acpmux does not list is a pick (it resumes or opens), not greyed.
const greyed = !ready && !isCloudMachineRow(environment);
return (
<span
className="acpmux-location-row acpmux-env-row"
data-kind={environment.kind}
data-section={sectionStart ? t(SECTION_LABELS[sectionOf(environment)]) : undefined}
data-first-section={sectionStart && index === 0 && savedShown.length === 0 ? "" : undefined}
data-current={environment.id === currentRow ? "" : undefined}
data-ready={ready ? undefined : "false"}
aria-disabled={ready ? undefined : true}
data-ready={greyed ? "false" : undefined}
aria-disabled={greyed ? true : undefined}
>
<EnvironmentIcon kind={environment.kind} name={environment.label} />
<span className="acpmux-menu-text acpmux-location-name">
Expand Down
3 changes: 3 additions & 0 deletions webviews/src/agent-session/acpmux/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -111831,6 +111831,9 @@
},
"composer.envMine": {"comment":"Agent pane: environment picker and Computer menu section of the person's own SSH machines that acpmux lists","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"أجهزتي"}},"bs":{"stringUnit":{"state":"needs_review","value":"Moje mašine"}},"da":{"stringUnit":{"state":"needs_review","value":"Mine maskiner"}},"de":{"stringUnit":{"state":"needs_review","value":"Meine Rechner"}},"en":{"stringUnit":{"state":"translated","value":"My machines"}},"es":{"stringUnit":{"state":"needs_review","value":"Mis máquinas"}},"fr":{"stringUnit":{"state":"needs_review","value":"Mes machines"}},"it":{"stringUnit":{"state":"needs_review","value":"Le mie macchine"}},"ja":{"stringUnit":{"state":"needs_review","value":"自分のマシン"}},"km":{"stringUnit":{"state":"needs_review","value":"ម៉ាស៊ីនរបស់ខ្ញុំ"}},"ko":{"stringUnit":{"state":"needs_review","value":"내 머신"}},"nb":{"stringUnit":{"state":"needs_review","value":"Mine maskiner"}},"pl":{"stringUnit":{"state":"needs_review","value":"Moje maszyny"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Minhas máquinas"}},"ru":{"stringUnit":{"state":"needs_review","value":"Мои машины"}},"th":{"stringUnit":{"state":"needs_review","value":"เครื่องของฉัน"}},"tr":{"stringUnit":{"state":"needs_review","value":"Makinelerim"}},"uk":{"stringUnit":{"state":"needs_review","value":"Мої машини"}},"vi":{"stringUnit":{"state":"needs_review","value":"Máy của tôi"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"我的机器"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"我的機器"}}}},
"composer.envFleet": {"comment":"Agent pane: environment picker and Computer menu section of Cloud machines and pools of numbered VMs (lane-1, lane-2) that acpmux lists","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"السحابة والأجهزة الافتراضية"}},"bs":{"stringUnit":{"state":"needs_review","value":"Cloud i VM-ovi"}},"da":{"stringUnit":{"state":"needs_review","value":"Cloud og VM'er"}},"de":{"stringUnit":{"state":"needs_review","value":"Cloud und VMs"}},"en":{"stringUnit":{"state":"translated","value":"Cloud and VMs"}},"es":{"stringUnit":{"state":"needs_review","value":"Nube y VM"}},"fr":{"stringUnit":{"state":"needs_review","value":"Cloud et VM"}},"it":{"stringUnit":{"state":"needs_review","value":"Cloud e VM"}},"ja":{"stringUnit":{"state":"needs_review","value":"クラウドとVM"}},"km":{"stringUnit":{"state":"needs_review","value":"ពពក និង VM"}},"ko":{"stringUnit":{"state":"needs_review","value":"클라우드 및 VM"}},"nb":{"stringUnit":{"state":"needs_review","value":"Sky og VM-er"}},"pl":{"stringUnit":{"state":"needs_review","value":"Chmura i maszyny wirtualne"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Nuvem e VMs"}},"ru":{"stringUnit":{"state":"needs_review","value":"Облако и ВМ"}},"th":{"stringUnit":{"state":"needs_review","value":"คลาวด์และ VM"}},"tr":{"stringUnit":{"state":"needs_review","value":"Bulut ve VM'ler"}},"uk":{"stringUnit":{"state":"needs_review","value":"Хмара та ВМ"}},"vi":{"stringUnit":{"state":"needs_review","value":"Đám mây và VM"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"云端和虚拟机"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"雲端和虛擬機"}}}},
"composer.envOpenMachine": {"comment":"Agent pane: environment picker and Computer menu, after a Cloud machine of the person's account that acpmux does not list: a pick opens the machine","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"فتح"}},"bs":{"stringUnit":{"state":"needs_review","value":"Otvori"}},"da":{"stringUnit":{"state":"needs_review","value":"Åbn"}},"de":{"stringUnit":{"state":"needs_review","value":"Öffnen"}},"en":{"stringUnit":{"state":"translated","value":"Open"}},"es":{"stringUnit":{"state":"needs_review","value":"Abrir"}},"fr":{"stringUnit":{"state":"needs_review","value":"Ouvrir"}},"it":{"stringUnit":{"state":"needs_review","value":"Apri"}},"ja":{"stringUnit":{"state":"needs_review","value":"開く"}},"km":{"stringUnit":{"state":"needs_review","value":"បើក"}},"ko":{"stringUnit":{"state":"needs_review","value":"열기"}},"nb":{"stringUnit":{"state":"needs_review","value":"Åpne"}},"pl":{"stringUnit":{"state":"needs_review","value":"Otwórz"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Abrir"}},"ru":{"stringUnit":{"state":"needs_review","value":"Открыть"}},"th":{"stringUnit":{"state":"needs_review","value":"เปิด"}},"tr":{"stringUnit":{"state":"needs_review","value":"Aç"}},"uk":{"stringUnit":{"state":"needs_review","value":"Відкрити"}},"vi":{"stringUnit":{"state":"needs_review","value":"Mở"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"打开"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"開啟"}}}},
"composer.envPausedResume": {"comment":"Agent pane: environment picker and Computer menu, after a paused Cloud machine of the person's account: it is paused, and a pick resumes it","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"متوقف مؤقتًا · استئناف"}},"bs":{"stringUnit":{"state":"needs_review","value":"Pauzirano · Nastavi"}},"da":{"stringUnit":{"state":"needs_review","value":"Sat på pause · Genoptag"}},"de":{"stringUnit":{"state":"needs_review","value":"Pausiert · Fortsetzen"}},"en":{"stringUnit":{"state":"translated","value":"Paused · Resume"}},"es":{"stringUnit":{"state":"needs_review","value":"En pausa · Reanudar"}},"fr":{"stringUnit":{"state":"needs_review","value":"En pause · Reprendre"}},"it":{"stringUnit":{"state":"needs_review","value":"In pausa · Riprendi"}},"ja":{"stringUnit":{"state":"needs_review","value":"一時停止中 · 再開"}},"km":{"stringUnit":{"state":"needs_review","value":"បានផ្អាក · បន្ត"}},"ko":{"stringUnit":{"state":"needs_review","value":"일시 중지됨 · 재개"}},"nb":{"stringUnit":{"state":"needs_review","value":"Satt på pause · Gjenoppta"}},"pl":{"stringUnit":{"state":"needs_review","value":"Wstrzymano · Wznów"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Pausado · Retomar"}},"ru":{"stringUnit":{"state":"needs_review","value":"Приостановлено · Возобновить"}},"th":{"stringUnit":{"state":"needs_review","value":"หยุดชั่วคราว · ดำเนินการต่อ"}},"tr":{"stringUnit":{"state":"needs_review","value":"Duraklatıldı · Sürdür"}},"uk":{"stringUnit":{"state":"needs_review","value":"Призупинено · Відновити"}},"vi":{"stringUnit":{"state":"needs_review","value":"Đã tạm dừng · Tiếp tục"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"已暂停 · 恢复"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"已暫停 · 繼續"}}}},
"composer.envStarting": {"comment":"Agent pane: environment picker and Computer menu, after a Cloud machine of the person's account that is being created or is booting","extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"needs_review","value":"جارٍ البدء…"}},"bs":{"stringUnit":{"state":"needs_review","value":"Pokretanje…"}},"da":{"stringUnit":{"state":"needs_review","value":"Starter…"}},"de":{"stringUnit":{"state":"needs_review","value":"Wird gestartet…"}},"en":{"stringUnit":{"state":"translated","value":"Starting…"}},"es":{"stringUnit":{"state":"needs_review","value":"Iniciando…"}},"fr":{"stringUnit":{"state":"needs_review","value":"Démarrage…"}},"it":{"stringUnit":{"state":"needs_review","value":"Avvio in corso…"}},"ja":{"stringUnit":{"state":"needs_review","value":"起動中…"}},"km":{"stringUnit":{"state":"needs_review","value":"កំពុងចាប់ផ្ដើម…"}},"ko":{"stringUnit":{"state":"needs_review","value":"시작하는 중…"}},"nb":{"stringUnit":{"state":"needs_review","value":"Starter…"}},"pl":{"stringUnit":{"state":"needs_review","value":"Uruchamianie…"}},"pt-BR":{"stringUnit":{"state":"needs_review","value":"Iniciando…"}},"ru":{"stringUnit":{"state":"needs_review","value":"Запуск…"}},"th":{"stringUnit":{"state":"needs_review","value":"กำลังเริ่ม…"}},"tr":{"stringUnit":{"state":"needs_review","value":"Başlatılıyor…"}},"uk":{"stringUnit":{"state":"needs_review","value":"Запуск…"}},"vi":{"stringUnit":{"state":"needs_review","value":"Đang khởi động…"}},"zh-Hans":{"stringUnit":{"state":"needs_review","value":"正在启动…"}},"zh-Hant":{"stringUnit":{"state":"needs_review","value":"正在啟動…"}}}},
"composer.envLocal": {
"comment": "Agent pane",
"extractionState": "manual",
Expand Down
Loading
Loading