fix(cli): replace all tmux attach output with dashboard URLs (#947)
Replace raw `tmux attach -t` commands in CLI output with web dashboard URLs (`http://localhost:{port}/sessions/{sessionId}`) across all four CLI commands: spawn, session restore, open, and start. Add `stripHashPrefix` helper in session-utils to extract AO session IDs from hash-prefixed tmux session names. Remove unused `tmuxTarget` variable from start command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5322220f6e
commit
24ecfc2926
|
|
@ -167,7 +167,7 @@ describe("open command", () => {
|
|||
await program.parseAsync(["node", "test", "open", "app-1"]);
|
||||
|
||||
const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(output).toContain("tmux attach");
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-1");
|
||||
});
|
||||
|
||||
it("shows 'No sessions to open' when none exist", async () => {
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ describe("spawn command", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("shows ao session attach command instead of raw tmux attach", async () => {
|
||||
it("shows dashboard URL instead of raw tmux attach", async () => {
|
||||
const fakeSession: Session = {
|
||||
id: "app-7",
|
||||
projectId: "my-app",
|
||||
|
|
@ -307,10 +307,10 @@ describe("spawn command", () => {
|
|||
|
||||
await program.parseAsync(["node", "test", "spawn"]);
|
||||
|
||||
const succeedMsg = String(mockSpinner.succeed.mock.calls[0]?.[0] ?? "");
|
||||
expect(succeedMsg).toContain("ao session attach app-7");
|
||||
expect(succeedMsg).not.toContain("tmux attach");
|
||||
expect(succeedMsg).not.toContain("8474d6f29887-app-7");
|
||||
const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-7");
|
||||
expect(output).not.toContain("tmux attach");
|
||||
expect(output).not.toContain("8474d6f29887-app-7");
|
||||
});
|
||||
|
||||
it("passes --agent flag to sessionManager.spawn()", async () => {
|
||||
|
|
@ -439,7 +439,8 @@ describe("spawn command", () => {
|
|||
|
||||
const succeedMsg = String(mockSpinner.succeed.mock.calls[0]?.[0] ?? "");
|
||||
expect(succeedMsg).toContain("https://github.com/org/repo/pull/123");
|
||||
expect(succeedMsg).toContain("ao session attach app-1");
|
||||
const output = consoleSpy.mock.calls.map((c) => String(c[0])).join("\n");
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-1");
|
||||
});
|
||||
|
||||
it("passes GitHub assignment flag through to claimPR", async () => {
|
||||
|
|
|
|||
|
|
@ -853,12 +853,12 @@ describe("start command — orchestrator session strategy display", () => {
|
|||
await program.parseAsync(["node", "test", "start", "--no-dashboard"]);
|
||||
|
||||
const output = getLoggedOutput();
|
||||
expect(output).toContain("tmux attach -t tmux-session-1");
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-orchestrator");
|
||||
expect(output).not.toContain("reused existing session");
|
||||
});
|
||||
|
||||
it.each(["delete", "ignore", "delete-new", "ignore-new", "kill-previous"] as const)(
|
||||
"uses attach messaging when strategy is %s",
|
||||
"uses session URL messaging when strategy is %s",
|
||||
async (orchestratorSessionStrategy) => {
|
||||
mockConfigRef.current = makeConfig({
|
||||
"my-app": makeProject({ orchestratorSessionStrategy }),
|
||||
|
|
@ -877,7 +877,7 @@ describe("start command — orchestrator session strategy display", () => {
|
|||
await program.parseAsync(["node", "test", "start", "--no-dashboard"]);
|
||||
|
||||
const output = getLoggedOutput();
|
||||
expect(output).toContain("tmux attach -t tmux-session-1");
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-orchestrator");
|
||||
expect(output).not.toContain("reused existing session");
|
||||
},
|
||||
);
|
||||
|
|
@ -900,8 +900,8 @@ describe("start command — orchestrator session strategy display", () => {
|
|||
|
||||
const output = getLoggedOutput();
|
||||
// When --no-dashboard is used, auto-selects the most recent orchestrator
|
||||
// and shows the tmux attach command (not the dashboard selection message)
|
||||
expect(output).toContain("tmux attach -t tmux-session-existing");
|
||||
// and shows the session URL (not the dashboard selection message)
|
||||
expect(output).toContain("http://localhost:3000/sessions/app-orchestrator");
|
||||
expect(output).not.toContain("existing sessions found — select one in the dashboard");
|
||||
|
||||
// Should NOT spawn a new orchestrator when existing ones exist
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
|
|||
import {
|
||||
escapeRegex,
|
||||
matchesPrefix,
|
||||
stripHashPrefix,
|
||||
findProjectForSession,
|
||||
isOrchestratorSessionName,
|
||||
} from "../../src/lib/session-utils.js";
|
||||
|
|
@ -29,6 +30,24 @@ describe("escapeRegex", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("stripHashPrefix", () => {
|
||||
it("strips 12-char hex hash prefix", () => {
|
||||
expect(stripHashPrefix("1686e4aaaeaa-ao-145")).toBe("ao-145");
|
||||
});
|
||||
|
||||
it("returns plain session ID unchanged", () => {
|
||||
expect(stripHashPrefix("ao-145")).toBe("ao-145");
|
||||
});
|
||||
|
||||
it("returns orchestrator session name unchanged", () => {
|
||||
expect(stripHashPrefix("app-orchestrator")).toBe("app-orchestrator");
|
||||
});
|
||||
|
||||
it("handles hash prefix with orchestrator name", () => {
|
||||
expect(stripHashPrefix("abcdef012345-app-orchestrator")).toBe("app-orchestrator");
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesPrefix", () => {
|
||||
it("matches prefix-1", () => {
|
||||
expect(matchesPrefix("app-1", "app")).toBe(true);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import chalk from "chalk";
|
|||
import type { Command } from "commander";
|
||||
import { loadConfig } from "@composio/ao-core";
|
||||
import { exec, getTmuxSessions } from "../lib/shell.js";
|
||||
import { matchesPrefix } from "../lib/session-utils.js";
|
||||
import { matchesPrefix, stripHashPrefix } from "../lib/session-utils.js";
|
||||
|
||||
async function openInTerminal(sessionName: string, newWindow?: boolean): Promise<boolean> {
|
||||
try {
|
||||
|
|
@ -60,13 +60,15 @@ export function registerOpen(program: Command): void {
|
|||
),
|
||||
);
|
||||
|
||||
const port = config.port ?? 3000;
|
||||
for (const session of sessionsToOpen.sort()) {
|
||||
const opened = await openInTerminal(session, opts.newWindow);
|
||||
if (opened) {
|
||||
console.log(chalk.green(` Opened: ${session}`));
|
||||
} else {
|
||||
const sessionId = stripHashPrefix(session);
|
||||
console.log(
|
||||
` ${chalk.yellow(session)} — attach with: ${chalk.dim(`tmux attach -t ${session}`)}`,
|
||||
` ${chalk.yellow(session)} — view at: ${chalk.dim(`http://localhost:${port}/sessions/${sessionId}`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,8 +301,8 @@ export function registerSession(program: Command): void {
|
|||
if (restored.branch) {
|
||||
console.log(chalk.dim(` Branch: ${restored.branch}`));
|
||||
}
|
||||
const tmuxTarget = restored.runtimeHandle?.id ?? sessionName;
|
||||
console.log(chalk.dim(` Attach: tmux attach -t ${tmuxTarget}`));
|
||||
const port = config.port ?? 3000;
|
||||
console.log(chalk.dim(` View: http://localhost:${port}/sessions/${sessionName}`));
|
||||
} catch (err) {
|
||||
if (err instanceof SessionNotRestorableError) {
|
||||
console.error(chalk.red(`Cannot restore: ${err.reason}`));
|
||||
|
|
|
|||
|
|
@ -121,10 +121,11 @@ async function spawnSession(
|
|||
|
||||
const issueLabel = issueId ? ` for issue #${issueId}` : "";
|
||||
const claimLabel = claimedPrUrl ? ` (claimed ${claimedPrUrl})` : "";
|
||||
const port = config.port ?? 3000;
|
||||
spinner.succeed(
|
||||
`Session ${chalk.green(session.id)} spawned${issueLabel}${claimLabel}. ` +
|
||||
`View in the dashboard (if running) or attach with: ${chalk.cyan(`ao session attach ${session.id}`)}`,
|
||||
`Session ${chalk.green(session.id)} spawned${issueLabel}${claimLabel}`,
|
||||
);
|
||||
console.log(` View: ${chalk.dim(`http://localhost:${port}/sessions/${session.id}`)}`);
|
||||
|
||||
// Warn if prompt delivery failed (for post-launch agents like Claude Code)
|
||||
const promptDelivered = session.metadata?.promptDelivered;
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,6 @@ async function runStartup(
|
|||
}
|
||||
|
||||
// Create orchestrator session (unless --no-orchestrator or existing orchestrators found)
|
||||
let tmuxTarget = sessionId;
|
||||
let hasExistingOrchestrators = false;
|
||||
let selectedOrchestratorId: string | null = null;
|
||||
|
||||
|
|
@ -1050,8 +1049,6 @@ async function runStartup(
|
|||
);
|
||||
const selected = sortedOrchestrators[0];
|
||||
selectedOrchestratorId = selected.id;
|
||||
// Use runtimeHandle.id if available, otherwise fall back to the session ID
|
||||
tmuxTarget = selected.runtimeHandle?.id ?? selected.id;
|
||||
if (opts?.dashboard !== false && existingOrchestrators.length > 1) {
|
||||
hasExistingOrchestrators = true;
|
||||
}
|
||||
|
|
@ -1067,9 +1064,6 @@ async function runStartup(
|
|||
const systemPrompt = generateOrchestratorPrompt({ config, projectId, project });
|
||||
const session = await sm.spawnOrchestrator({ projectId, systemPrompt });
|
||||
selectedOrchestratorId = session.id;
|
||||
if (session.runtimeHandle?.id) {
|
||||
tmuxTarget = session.runtimeHandle.id;
|
||||
}
|
||||
reused =
|
||||
orchestratorSessionStrategy === "reuse" &&
|
||||
session.metadata?.["orchestratorSessionReused"] === "true";
|
||||
|
|
@ -1115,7 +1109,10 @@ async function runStartup(
|
|||
`http://localhost:${port}/sessions/${orchSessionId}`,
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.cyan("Orchestrator:"), `tmux attach -t ${tmuxTarget}`);
|
||||
console.log(
|
||||
chalk.cyan("Orchestrator:"),
|
||||
`http://localhost:${port}/sessions/${orchSessionId}`,
|
||||
);
|
||||
}
|
||||
} else if (reused) {
|
||||
console.log(chalk.cyan("Orchestrator:"), `reused existing session (${sessionId})`);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@ export function escapeRegex(str: string): string {
|
|||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip optional 12-char hex hash prefix from a tmux session name.
|
||||
* "1686e4aaaeaa-ao-145" → "ao-145"
|
||||
* "ao-145" → "ao-145" (no-op if no hash prefix)
|
||||
*/
|
||||
export function stripHashPrefix(name: string): string {
|
||||
const match = name.match(/^[a-f0-9]{12}-(.+)$/);
|
||||
return match ? match[1] : name;
|
||||
}
|
||||
|
||||
/** Check whether a session name matches a project prefix (strict: prefix-\d+ only). */
|
||||
export function matchesPrefix(sessionName: string, prefix: string): boolean {
|
||||
return new RegExp(`^${escapeRegex(prefix)}-\\d+$`).test(sessionName);
|
||||
|
|
|
|||
Loading…
Reference in New Issue