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
10 changes: 10 additions & 0 deletions apps/desktop-tauri/scripts/proof-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,16 @@
{ "expectText": "%" },
{ "screenshot": "floatbar" }
]
},
{
"name": "tray-agent-sessions-hosts",
"query": "window=main&fixture=agent-sessions-hosts",
"size": [310, 1032],
"steps": [
{ "expectText": ["WORKSTATION-A — 2", "BUILD-BOX — 1", "LAB-HOST — unreachable", "ssh: connection timed out", "example-project"] },
{ "expectNoText": "LAB-HOST — 0" },
{ "screenshot": "tray-agent-sessions-hosts", "clip": "panel" }
]
}
]
}
62 changes: 62 additions & 0 deletions apps/desktop-tauri/src/components/AgentSessions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,66 @@ describe("AgentSessions", () => {

expect(await screen.findByText("AgentSessionsProviderPi")).toBeTruthy();
});

it("groups sessions under one header per host with its session count", async () => {
api.listAgentSessions.mockResolvedValue({
status: "hosts",
hosts: [
{
host: "DESKTOP",
sessions: [
session({ id: "1", host: "DESKTOP", workspace: { cwd: null, projectName: "alpha" } }),
session({ id: "2", host: "DESKTOP", workspace: { cwd: null, projectName: "beta" } }),
],
error: null,
},
{
host: "LAPTOP",
sessions: [session({ id: "3", host: "LAPTOP", workspace: { cwd: null, projectName: "gamma" } })],
error: null,
},
],
});
render(<AgentSessions />);

const desktopHeader = await screen.findByText("DESKTOP — 2");
const laptopHeader = await screen.findByText("LAPTOP — 1");
const alpha = screen.getByText("alpha");
const gamma = screen.getByText("gamma");
// Each host's rows follow its own header, and the hosts stay in order.
expect(desktopHeader.compareDocumentPosition(alpha) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(alpha.compareDocumentPosition(laptopHeader) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(laptopHeader.compareDocumentPosition(gamma) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});

it("shows an unreachable host as its own row with the error", async () => {
api.listAgentSessions.mockResolvedValue({
status: "hosts",
hosts: [
{
host: "DESKTOP",
sessions: [session({ id: "1", workspace: { cwd: null, projectName: "alpha" } })],
error: null,
},
{ host: "BUILD-BOX", sessions: [], error: "ssh: connect timed out" },
],
});
render(<AgentSessions />);

expect(await screen.findByText("BUILD-BOX — AgentSessionsHostUnreachable")).toBeTruthy();
expect(screen.getByText("ssh: connect timed out").className).toContain("agent-sessions__error");
expect(screen.queryByText("BUILD-BOX — 0")).toBeNull();
expect(screen.getByText("DESKTOP — 1")).toBeTruthy();
});

it("keeps a reachable host with no sessions visible as a zero-count header", async () => {
api.listAgentSessions.mockResolvedValue({
status: "hosts",
hosts: [{ host: "IDLE-BOX", sessions: [], error: null }],
});
render(<AgentSessions />);

expect(await screen.findByText("IDLE-BOX — 0")).toBeTruthy();
expect(screen.getByText("AgentSessionsEmpty")).toBeTruthy();
});
});
54 changes: 33 additions & 21 deletions apps/desktop-tauri/src/components/AgentSessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ export default function AgentSessions() {
if (result?.status === "disabled") return null;
const hosts = result?.status === "hosts" ? result.hosts : [];
const sessions = hosts.flatMap((host) => host.sessions);
const hostErrors = hosts.flatMap((host) =>
host.error ? [`${host.host}: ${host.error}`] : [],
);

const focus = (session: AgentSession) => {
void focusAgentSession(session).then((focusResult) => {
Expand All @@ -53,26 +50,41 @@ export default function AgentSessions() {
{result && sessions.length === 0 && (
<p>{t("AgentSessionsEmpty")}</p>
)}
{sessions.map((session) => (
<button
type="button"
className="agent-sessions__row"
key={`${session.host}:${session.provider}:${session.id}`}
onClick={() => focus(session)}
>
<span>{sessionLabel(session)}</span>
<span
className="agent-sessions__name"
title={session.sessionName ?? session.workspace.projectName ?? session.host}
{hosts.map((host) => (
<div className="agent-sessions__host" key={host.host}>
{/* Upstream agentSessionsSection: one header per host, then its rows. */}
<div
className={
host.error
? "agent-sessions__host-header agent-sessions__host-header--unreachable"
: "agent-sessions__host-header"
}
>
{session.sessionName ?? session.workspace.projectName ?? session.host}
</span>
<span>{session.state}</span>
</button>
))}
{[...hostErrors, ...(error ? [error] : [])].map((message) => (
<p className="agent-sessions__error" key={message}>{message}</p>
{host.error
? `${host.host} — ${t("AgentSessionsHostUnreachable")}`
: `${host.host} — ${host.sessions.length}`}
</div>
{host.error && <p className="agent-sessions__error">{host.error}</p>}
{host.sessions.map((session) => (
<button
type="button"
className="agent-sessions__row"
key={`${session.host}:${session.provider}:${session.id}`}
onClick={() => focus(session)}
>
<span>{sessionLabel(session)}</span>
<span
className="agent-sessions__name"
title={session.sessionName ?? session.workspace.projectName ?? session.host}
>
{session.sessionName ?? session.workspace.projectName ?? session.host}
</span>
<span>{session.state}</span>
</button>
))}
</div>
))}
{error && <p className="agent-sessions__error">{error}</p>}
</section>
);
}
1 change: 1 addition & 0 deletions apps/desktop-tauri/src/i18n/keyGroups/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ export const CORE_LOCALE_KEYS = [
"AgentSessionsSshHostsHelper",
"AgentSessionsLoading",
"AgentSessionsEmpty",
"AgentSessionsHostUnreachable",
"UpdatesTitle",
"AboutBuilt",
"SectionLinks",
Expand Down
55 changes: 55 additions & 0 deletions apps/desktop-tauri/src/proof/fixtures/scenarios.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {
AccountUsageEntryBridge,
AgentSession,
AgentSessionDiscoveryResult,
ClaudeAccount,
ClaudeSwapAccount,
ClaudeSwapAccountsState,
Expand Down Expand Up @@ -54,6 +56,7 @@ export const PROOF_SCENARIO_NAMES = [
"vercel",
"vercel-team-id-invalid",
"kimi-bonus-credits",
"agent-sessions-hosts",
] as const;

export type ProofScenarioName = (typeof PROOF_SCENARIO_NAMES)[number];
Expand Down Expand Up @@ -92,6 +95,8 @@ export interface ProofScenario {
workspaceIds?: Record<string, string>;
/** Error a workspace-value save rejects with, by provider id; absent means saves succeed. */
workspaceIdSaveErrors?: Record<string, string>;
/** Agent-session discovery served by `list_agent_sessions`; absent means disabled. */
agentSessions?: AgentSessionDiscoveryResult;
}

export interface LocalSpendFixture {
Expand Down Expand Up @@ -1147,6 +1152,55 @@ function kimiBonusCreditsScenario(now: Date): ProofScenario {
};
}

function proofAgentSession(
id: string,
host: string,
provider: AgentSession["provider"],
projectName: string,
): AgentSession {
return {
id,
provider,
source: "cli",
state: "active",
pid: null,
transcriptPath: null,
host,
workspace: { cwd: null, projectName },
activity: { startedAt: null, lastActivityAt: null },
focusTarget: { kind: "none" },
};
}

/** Two reachable hosts with sessions and one unreachable host; all names are synthetic. */
function agentSessionsHostsScenario(now: Date): ProofScenario {
const base = defaultScenario(now);
return {
...base,
name: "agent-sessions-hosts",
settings: { ...base.settings, agentSessionsEnabled: true, agentSessionsLocalOnly: false },
agentSessions: {
status: "hosts",
hosts: [
{
host: "WORKSTATION-A",
sessions: [
proofAgentSession("proof-1", "WORKSTATION-A", "codex", "example-project"),
proofAgentSession("proof-2", "WORKSTATION-A", "claude", "example-notes"),
],
error: null,
},
{
host: "BUILD-BOX",
sessions: [proofAgentSession("proof-3", "BUILD-BOX", "claude", "example-ci")],
error: null,
},
{ host: "LAB-HOST", sessions: [], error: "ssh: connection timed out" },
],
},
};
}

const BUILDERS: Record<ProofScenarioName, (now: Date) => ProofScenario> = {
default: defaultScenario,
"two-codex-accounts": twoCodexAccountsScenario,
Expand Down Expand Up @@ -1175,6 +1229,7 @@ const BUILDERS: Record<ProofScenarioName, (now: Date) => ProofScenario> = {
vercel: vercelScenario,
"vercel-team-id-invalid": vercelTeamIdInvalidScenario,
"kimi-bonus-credits": kimiBonusCreditsScenario,
"agent-sessions-hosts": agentSessionsHostsScenario,
};

export function isProofScenarioName(value: string): value is ProofScenarioName {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop-tauri/src/proof/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
CurrentSurfaceState,
HttpServerConfig,
HttpServerSnapshot,
SessionFocusResult,
SettingsSnapshot,
SettingsUpdate,
TokenAccountBridge,
Expand Down Expand Up @@ -59,7 +60,6 @@ export const INERT_ACTIONS = [
"codex_account_switch",
"download_update",
"export_preferences",
"focus_agent_session",
"get_openai_live_usage",
"get_openai_org_rate_limits",
"grok_account_add",
Expand Down Expand Up @@ -424,7 +424,8 @@ export function createProofRouter(
},
grok_accounts_list: () => [],
get_hooks_config: proofHooksConfig,
list_agent_sessions: () => ({ status: "disabled" }),
list_agent_sessions: () => scenario.agentSessions ?? { status: "disabled" },
focus_agent_session: (): SessionFocusResult => ({ status: "focused" }),
get_gemini_cli_signed_in: () => ({ signedIn: true, credentialsPath: null }),
get_vertexai_status: () => ({ hasCredentials: false, credentialsPath: null }),
list_jetbrains_detected_ides: () => [],
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop-tauri/src/styles/parity-overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,20 @@
color: var(--provider-status-error);
}

.agent-sessions__host {
display: grid;
gap: 6px;
}

.agent-sessions__host-header {
font-weight: 600;
opacity: 0.8;
}

.agent-sessions__host-header--unreachable {
color: var(--provider-status-error);
}

/* ── Per-provider accent color override (#2972) ─────────────────── */
.provider-detail-accent-color {
margin-top: 16px;
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,7 @@ locale_keys! {
AgentSessionsSshHostsHelper,
AgentSessionsLoading,
AgentSessionsEmpty,
AgentSessionsHostUnreachable,
UpdatesTitle,
AboutBuilt,
SectionLinks,
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ AgentSessionsSshHostsLabel = Additional SSH hosts
AgentSessionsSshHostsHelper = Comma-separated SSH targets. Connections use non-interactive mode and short timeouts.
AgentSessionsLoading = Looking for agent sessions...
AgentSessionsEmpty = No active agent sessions.
AgentSessionsHostUnreachable = unreachable
UpdatesTitle = Updates
AboutBuilt = Built { "{}" }
SectionLinks = Links
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/es-MX.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ AgentSessionsSshHostsLabel = Hosts SSH
AgentSessionsSshHostsHelper = Destinos SSH separados por comas. Las conexiones usan modo no interactivo y tiempos de espera cortos.
AgentSessionsLoading = Buscando sesiones de agentes...
AgentSessionsEmpty = No hay sesiones de agentes activas.
AgentSessionsHostUnreachable = inaccesible
UpdatesTitle = Actualizaciones
AboutBuilt = Compilado el { "{}" }
SectionLinks = Enlaces
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/ja-JP.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ AgentSessionsSshHostsLabel = SSH ホスト
AgentSessionsSshHostsHelper = カンマ区切りの SSH ターゲット。接続は非対話モード・短いタイムアウトを使用。
AgentSessionsLoading = エージェントセッションを検索中…
AgentSessionsEmpty = アクティブなエージェントセッションはありません。
AgentSessionsHostUnreachable = 接続できません
UpdatesTitle = アップデート
AboutBuilt = ビルド日 { "{}" }
SectionLinks = リンク
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/ko-KR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ AgentSessionsSshHostsLabel = SSH 호스트
AgentSessionsSshHostsHelper = 쉼표로 구분된 SSH 대상입니다. 연결은 비대화형 모드와 짧은 제한 시간을 사용합니다.
AgentSessionsLoading = 에이전트 세션을 찾는 중...
AgentSessionsEmpty = 활성 에이전트 세션이 없습니다.
AgentSessionsHostUnreachable = 연결할 수 없음
UpdatesTitle = 업데이트
AboutBuilt = 빌드 날짜 { "{}" }
SectionLinks = 링크
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/pt-BR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ AgentSessionsSshHostsLabel = Hosts SSH
AgentSessionsSshHostsHelper = Destinos SSH separados por vírgula. As conexões usam modo não interativo e tempos limites curtos.
AgentSessionsLoading = Procurando sessões de agente...
AgentSessionsEmpty = Nenhuma sessão de agente ativa.
AgentSessionsHostUnreachable = inacessível
UpdatesTitle = Atualizações
AboutBuilt = Compilado em { "{}" }
SectionLinks = Links
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/ru-RU.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ AgentSessionsSshHostsLabel = SSH-хосты
AgentSessionsSshHostsHelper = Цели SSH, разделенные запятыми. Соединения используют неинтерактивный режим и короткие таймауты.
AgentSessionsLoading = Ищу агентские сессии...
AgentSessionsEmpty = Нет сеансов активных агентов.
AgentSessionsHostUnreachable = недоступен
UpdatesTitle = Обновления
AboutBuilt = Сборка от { "{}" }
SectionLinks = Ссылки
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/tr-TR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ AgentSessionsSshHostsLabel = SSH sunucuları
AgentSessionsSshHostsHelper = Virgülle ayrılmış SSH hedefleri. Bağlantılar etkileşimsiz modu ve kısa zaman aşımlarını kullanır.
AgentSessionsLoading = Aracı oturumları aranıyor...
AgentSessionsEmpty = Etkin aracı oturumu yok.
AgentSessionsHostUnreachable = ulaşılamıyor
UpdatesTitle = Güncellemeler
AboutBuilt = Derleme tarihi { "{}" }
SectionLinks = Bağlantılar
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/uk-UA.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ AgentSessionsSshHostsLabel = SSH-хости
AgentSessionsSshHostsHelper = SSH-адреси через кому. З’єднання працюють у неінтерактивному режимі з короткими тайм-аутами.
AgentSessionsLoading = Пошук сеансів агентів...
AgentSessionsEmpty = Немає активних сеансів агентів.
AgentSessionsHostUnreachable = недоступний
UpdatesTitle = Оновлення
AboutBuilt = Збірка від { "{}" }
SectionLinks = Посилання
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/zh-CN.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ AgentSessionsSshHostsLabel = SSH 主机
AgentSessionsSshHostsHelper = 以逗号分隔的 SSH 目标。连接使用非交互模式和短超时。
AgentSessionsLoading = 正在查找 Agent 会话…
AgentSessionsEmpty = 没有活跃的 Agent 会话。
AgentSessionsHostUnreachable = 无法连接
UpdatesTitle = 更新
AboutBuilt = 构建于 { "{}" }
SectionLinks = 链接
Expand Down
1 change: 1 addition & 0 deletions rust/src/locale/zh-TW.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ AgentSessionsSshHostsLabel = SSH 主機
AgentSessionsSshHostsHelper = 以逗號分隔的 SSH 目標。連線使用非互動模式和短逾時。
AgentSessionsLoading = 正在查找 Agent 工作階段…
AgentSessionsEmpty = 沒有活躍的 Agent 工作階段。
AgentSessionsHostUnreachable = 無法連線
UpdatesTitle = 更新
AboutBuilt = 建置於 { "{}" }
SectionLinks = 連結
Expand Down