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
183 changes: 89 additions & 94 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,6 @@
<button class="ac-close" type="button" data-action="close-console">Close</button>
</div>
<div id="ac-config" class="ac-config"></div>
<div class="ac-files">
<div class="ac-files-head">
<span class="ac-files-label">Files</span>
<span id="ac-files-title" class="ac-files-title muted"></span>
</div>
<div class="ac-files-body">
<div id="ac-files-list" class="ac-files-list"></div>
<div id="ac-files-viewer" class="ac-files-viewer"></div>
</div>
</div>
<div class="ac-chat chat-wrap">
<div class="chat-head">
<span class="chat-title">Chat</span>
Expand All @@ -82,92 +72,98 @@
</div>
</section>
</section>
</div>
<section id="chat" class="chat-wrap">
<div class="chat-head">
<span class="chat-title">Agent chat</span>
<span class="chat-conn" id="chat-conn">disconnected</span>
</div>
<div id="chat-log" class="chat-log"></div>
<form id="chat-form" class="chat-input">
<textarea id="chat-text" class="chat-text" rows="2" placeholder="Message the agent… (Enter to send · Shift+Enter for a newline)"></textarea>
<div class="chat-actions">
<button type="button" id="chat-stop" class="chat-btn chat-stop" hidden>Stop</button>
<button type="submit" id="chat-send" class="chat-btn chat-send">Send</button>
<!-- ADR #83 slice 5: `[+ New fleet]` (7.2) / `[+ Add instance]` (7.3)
land here — an action, not a standing tab (Part C). Both flows share
the same compose→preview→deploy engine; `[+ New fleet]` runs the
net-new fleet-identity step first (7.5.1), `[+ Add instance]` skips
straight to Compose, inheriting the fleet it was opened from
(7.5.2). Sits alongside `#config`/`#fleet-detail`, replacing
whichever is showing — the persistent side column (identity +
Agent chat) stays visible, same as every other drill-down depth. -->
<section id="deploy-wrap" class="deploy-wrap" hidden>
<div class="deploy-head">
<span class="deploy-title" id="deploy-title">Deploy</span>
<span class="deploy-spacer"></span>
<button class="cfg-btn cfg-btn-ghost" id="deploy-cancel" type="button">Cancel</button>
</div>
</form>
</section>
</div>
<!-- ADR #83 slice 5: `[+ New fleet]` (7.2) / `[+ Add instance]` (7.3)
land here — an action, not a standing tab (Part C). Both flows share
the same compose→preview→deploy engine; `[+ New fleet]` runs the
net-new fleet-identity step first (7.5.1), `[+ Add instance]` skips
straight to Compose, inheriting the fleet it was opened from
(7.5.2). Replaces `.drilldown-row` while open. -->
<section id="deploy-wrap" class="deploy-wrap" hidden>
<div class="deploy-head">
<span class="deploy-title" id="deploy-title">Deploy</span>
<span class="deploy-spacer"></span>
<button class="cfg-btn cfg-btn-ghost" id="deploy-cancel" type="button">Cancel</button>
</div>
<form id="deploy-identity-form" class="deploy-identity" autocomplete="off" hidden>
<label
>Fleet name
<input id="deploy-fleet-name" class="compose-input" type="text" placeholder="e.g. support-fleet" spellcheck="false" />
</label>
<label
>Region
<input id="deploy-region" class="compose-input" type="text" placeholder="e.g. ap-east-2" spellcheck="false" />
</label>
<label
>Credential profile
<input id="deploy-profile" class="compose-input" type="text" placeholder="e.g. oab-fleet" spellcheck="false" />
</label>
<label
>Principal (optional)
<input id="deploy-principal" class="compose-input" type="text" placeholder="arn:aws:iam::…" spellcheck="false" />
</label>
<div class="compose-actions">
<button type="submit">Next: first instance &rarr;</button>
<span class="compose-status" id="deploy-identity-status"></span>
</div>
</form>
<div id="deploy-compose" class="deploy-compose" hidden>
<div class="deploy-compose-head" id="deploy-compose-heading">Compose</div>
<form id="deploy-form" class="compose-form" autocomplete="off">
<label
>Template
<select id="deploy-template" class="compose-select"></select>
</label>
<label
>Overlay
<select id="deploy-overlay" class="compose-select">
<option value="">— none (bare template) —</option>
</select>
</label>
<div class="compose-actions">
<button type="submit" id="deploy-preview-btn">Preview bundle</button>
<span class="compose-status" id="deploy-status"></span>
<form id="deploy-identity-form" class="deploy-identity" autocomplete="off" hidden>
<label
>Fleet name
<input id="deploy-fleet-name" class="compose-input" type="text" placeholder="e.g. support-fleet" spellcheck="false" />
</label>
<label
>Region
<input id="deploy-region" class="compose-input" type="text" placeholder="e.g. ap-east-2" spellcheck="false" />
</label>
<label
>Credential profile
<input id="deploy-profile" class="compose-input" type="text" placeholder="e.g. oab-fleet" spellcheck="false" />
</label>
<label
>Principal (optional)
<input id="deploy-principal" class="compose-input" type="text" placeholder="arn:aws:iam::…" spellcheck="false" />
</label>
<div class="compose-actions">
<button type="submit">Next: first instance &rarr;</button>
<span class="compose-status" id="deploy-identity-status"></span>
</div>
</form>
<div id="deploy-compose" class="deploy-compose" hidden>
<div class="deploy-compose-head" id="deploy-compose-heading">Compose</div>
<form id="deploy-form" class="compose-form" autocomplete="off">
<label
>Template
<select id="deploy-template" class="compose-select"></select>
</label>
<label
>Overlay
<select id="deploy-overlay" class="compose-select">
<option value="">— none (bare template) —</option>
</select>
</label>
<div class="compose-actions">
<button type="submit" id="deploy-preview-btn">Preview bundle</button>
<span class="compose-status" id="deploy-status"></span>
</div>
</form>
<div id="deploy-preview" class="compose-preview"></div>
<form id="deploy-deploy-form" class="compose-deploy" autocomplete="off" hidden>
<div class="compose-deploy-head">Deploy to ECS</div>
<label
>Agent name
<input id="deploy-name" class="compose-input" type="text" placeholder="e.g. agent-3" spellcheck="false" />
</label>
<label
>Image tag
<input id="deploy-image" class="compose-input" type="text" placeholder="(bundle default)" spellcheck="false" />
</label>
<div class="compose-actions">
<button type="submit" id="deploy-deploy-btn">Deploy</button>
<span class="compose-status" id="deploy-deploy-status"></span>
</div>
</form>
</div>
</form>
<div id="deploy-preview" class="compose-preview"></div>
<form id="deploy-deploy-form" class="compose-deploy" autocomplete="off" hidden>
<div class="compose-deploy-head">Deploy to ECS</div>
<label
>Agent name
<input id="deploy-name" class="compose-input" type="text" placeholder="e.g. agent-3" spellcheck="false" />
</label>
<label
>Image tag
<input id="deploy-image" class="compose-input" type="text" placeholder="(bundle default)" spellcheck="false" />
</label>
<div class="compose-actions">
<button type="submit" id="deploy-deploy-btn">Deploy</button>
<span class="compose-status" id="deploy-deploy-status"></span>
</section>
</div>
<div class="drilldown-resizer" id="drilldown-resizer" title="Drag to resize"></div>
<div class="drilldown-side" id="drilldown-side">
<section id="identity" class="identity-wrap"></section>
<section id="chat" class="chat-wrap">
<div class="chat-head">
<span class="chat-title">Agent chat</span>
<span class="chat-conn" id="chat-conn">disconnected</span>
</div>
</form>
<div id="chat-log" class="chat-log"></div>
<form id="chat-form" class="chat-input">
<textarea id="chat-text" class="chat-text" rows="2" placeholder="Message the agent… (Enter to send · Shift+Enter for a newline)"></textarea>
<div class="chat-actions">
<button type="button" id="chat-stop" class="chat-btn chat-stop" hidden>Stop</button>
<button type="submit" id="chat-send" class="chat-btn chat-send">Send</button>
</div>
</form>
</section>
</div>
</section>
</div>
<section id="config-editor" class="cfg-editor-wrap" hidden>
<div class="cfg-editor-head">
<span class="cfg-label" id="cfg-editor-title">edit fleets.toml</span>
Expand All @@ -179,15 +175,14 @@
<div id="cfg-editor-mount" class="cfg-editor-mount"></div>
<div class="cfg-editor-error" id="cfg-editor-error" hidden></div>
</section>
<section id="identity" class="identity-wrap"></section>
<section id="remote" class="remote-wrap"></section>
<!-- Compose (agent-deployment-templates.md: author library → preview →
deploy_provision) is the one surface Part C didn't fully relocate —
`[+ New fleet]`/`[+ Add instance]` (slice 5) reach the same engine as
an action, but where template/overlay *authoring* moves to once this
standing section goes away isn't decided in the ADR. Left in place;
flagged for the slice 7 self-review. Not part of the Debug drawer. -->
<section class="compose-standalone">
<section id="compose-standalone" class="compose-standalone">
<div class="compose-standalone-head">
<span class="compose-label">Compose</span>
<span class="compose-lib-path">template ⊕ overlay authoring — golden bundle library</span>
Expand Down
44 changes: 23 additions & 21 deletions console/src/agentConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
// It stays out of the transcript/turn machinery — that is `chatPanel.ts`. Its
// job is selection, the dial/teardown, the read-only config header, and
// registering the mounted panel in the shared event-router map so `main.ts` can
// route this agent's `agent-update` / `remote-status` events to it. The remote
// file editor (view/edit/apply) is a later slice; config is read-only here.
// route this agent's `agent-update` / `remote-status` events to it. Config is
// read-only here.

import type { Source } from "./source";
import type { AgentEndpointView } from "./types";
import { createChatPanel, type ChatPanel } from "./chatPanel";
import { createFileBrowser, type FileBrowser } from "./fileBrowser";
import { renderAgentList, agentConsoleHeaderHtml } from "./render";

export interface AgentConsoleConfig {
Expand All @@ -27,6 +26,13 @@ export interface AgentConsoleConfig {
// The management endpoint's name (resolved async in `main.ts`) — read lazily so
// the console never tries to re-key a name the management console owns.
managementName: () => string | null;
// The persistent management connection's active dial target (`remote.toml`'s
// `url`, only while it's connecting/connected/erroring — `null` once fully
// disconnected). `agents.toml`'s own `management` flag and this url are two
// independent sources of truth; a roster entry can reach the exact same
// physical agent as the management connection without being flagged, so both
// checks are needed to avoid dialing a second ACP session against it.
managementUrl: () => string | null;
}

export interface AgentConsole {
Expand All @@ -47,9 +53,6 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
const send = document.getElementById("ac-chat-send") as HTMLButtonElement | null;
const stop = document.getElementById("ac-chat-stop") as HTMLButtonElement | null;
const conn = document.getElementById("ac-chat-conn");
const fbList = document.getElementById("ac-files-list");
const fbViewer = document.getElementById("ac-files-viewer");
const fbTitle = document.getElementById("ac-files-title");

const noop: AgentConsole = {
refresh: async () => {},
Expand All @@ -61,7 +64,6 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
let agents: AgentEndpointView[] = [];
let openName: string | null = null;
let panel: ChatPanel | null = null;
let fileBrowser: FileBrowser | null = null;
const ac = new AbortController();
const { signal } = ac;

Expand All @@ -74,7 +76,7 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
}

function renderList(): void {
if (listEl) renderAgentList(listEl, agents, openName);
if (listEl) renderAgentList(listEl, agents, openName, cfg.managementUrl());
}

function renderHeader(status: string): void {
Expand Down Expand Up @@ -103,8 +105,6 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
openName = null;
panel?.dispose();
panel = null;
fileBrowser?.dispose();
fileBrowser = null;
cfg.panels.delete(name);
if (consoleEl) consoleEl.hidden = true;
// Fire-and-forget teardown; a failed disconnect is logged, not fatal.
Expand All @@ -118,12 +118,23 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
// Open a console for a named endpoint: teardown any current one, mount a chat
// panel bound to this agent, render its read-only config, and dial the
// endpoint. The management endpoint is never opened here (it has its own
// console); unconfigured endpoints are not dialable.
// console) — by name (agents.toml's own flag) or by url (the same physical
// agent reached under an un-flagged roster entry, e.g. this very fleet's
// management agent also showing up as a plain roster member); unconfigured
// endpoints are not dialable.
async function open(name: string): Promise<void> {
if (name === openName) return;
if (name === cfg.managementName()) return; // has its own top-level console
if (name === cfg.managementName()) return;
const a = find(name);
if (!a || !a.configured) return;
const mgmtUrl = cfg.managementUrl();
if (mgmtUrl && a.url === mgmtUrl) {
cfg.note(
"info",
`agents: "${name}" is the management agent's own endpoint — use Agent chat above instead of a second console`,
);
return;
}
close();
openName = name;
if (consoleEl) consoleEl.hidden = false;
Expand All @@ -143,15 +154,6 @@ export function initAgentConsole(cfg: AgentConsoleConfig): AgentConsole {
);
cfg.panels.set(name, panel);
}
// Mount the read-only file browser for this agent (Part D). It probes fs
// capability itself and shows a "pending the fs MCP files server" placeholder
// when the endpoint has no fs support — which is every real endpoint today.
if (fbList && fbViewer && fbTitle) {
fileBrowser = createFileBrowser(
{ list: fbList, viewer: fbViewer, title: fbTitle },
{ agent: name, source: cfg.source, note: cfg.note },
);
}
try {
await cfg.source.remoteConnect(name);
cfg.note("info", `agents: opened console for "${name}" — dialing ${a.url}`);
Expand Down
18 changes: 13 additions & 5 deletions console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,25 @@ export interface DeployedInfo {
export interface DeployPanelDeps {
source: Source;
onDeployed(info: DeployedInfo): void | Promise<void>;
// Re-run the #config/#fleet-detail visibility logic on close — main.ts owns
// which of the two `activeFleet` selects, this panel doesn't need to know.
restoreScreen(): void;
}

export interface DeployPanelHandle {
open(mode: DeployMode): void;
close(): void;
}

// Wires the `#deploy-wrap` panel declared in index.html. `null` if the DOM
// isn't present (mirrors the rest of the console's init* functions).
// Wires the `#deploy-wrap` panel declared in index.html. It lives inside
// `.drilldown-main` (a sibling of `#config`/`#fleet-detail`) so it takes over
// just the main column while open — the persistent side column (identity +
// Agent chat) stays put, same as every other depth of the drill-down. `null`
// if the DOM isn't present (mirrors the rest of the console's init* functions).
export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null {
const wrap = document.getElementById("deploy-wrap");
const drilldownRow = document.getElementById("drilldown-row");
const configEl = document.getElementById("config");
const fleetDetailEl = document.getElementById("fleet-detail");
const titleEl = document.getElementById("deploy-title");
const cancelBtn = document.getElementById("deploy-cancel") as HTMLButtonElement | null;
const identityForm = document.getElementById("deploy-identity-form") as HTMLFormElement | null;
Expand Down Expand Up @@ -154,15 +161,16 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
composeHeading.textContent =
m.kind === "new-fleet" ? "Step 2 — first instance" : "Compose";
}
if (drilldownRow) drilldownRow.hidden = true;
if (configEl) configEl.hidden = true;
if (fleetDetailEl) fleetDetailEl.hidden = true;
wrap.hidden = false;
void loadLibraryAndPickers();
};

const close = (): void => {
mode = null;
wrap.hidden = true;
if (drilldownRow) drilldownRow.hidden = false;
deps.restoreScreen();
reset();
};

Expand Down
Loading
Loading