Update UI for session/conflict controls and remove workspace dir

This commit is contained in:
2026-02-24 10:50:43 -05:00
parent 9f032d9b14
commit 6863c1da0b
5 changed files with 149 additions and 1 deletions

View File

@@ -23,6 +23,7 @@ export type LimitSettings = {
topologyMaxDepth: number; topologyMaxDepth: number;
topologyMaxRetries: number; topologyMaxRetries: number;
relationshipMaxChildren: number; relationshipMaxChildren: number;
mergeConflictMaxAttempts: number;
portBase: number; portBase: number;
portBlockSize: number; portBlockSize: number;
portBlockCount: number; portBlockCount: number;
@@ -88,6 +89,7 @@ function toLimits(config: Readonly<AppConfig>): LimitSettings {
topologyMaxDepth: config.orchestration.maxDepth, topologyMaxDepth: config.orchestration.maxDepth,
topologyMaxRetries: config.orchestration.maxRetries, topologyMaxRetries: config.orchestration.maxRetries,
relationshipMaxChildren: config.orchestration.maxChildren, relationshipMaxChildren: config.orchestration.maxChildren,
mergeConflictMaxAttempts: config.orchestration.mergeConflictMaxAttempts,
portBase: config.provisioning.portRange.basePort, portBase: config.provisioning.portRange.basePort,
portBlockSize: config.provisioning.portRange.blockSize, portBlockSize: config.provisioning.portRange.blockSize,
portBlockCount: config.provisioning.portRange.blockCount, portBlockCount: config.provisioning.portRange.blockCount,
@@ -170,6 +172,7 @@ export class UiConfigStore {
AGENT_TOPOLOGY_MAX_DEPTH: String(input.topologyMaxDepth), AGENT_TOPOLOGY_MAX_DEPTH: String(input.topologyMaxDepth),
AGENT_TOPOLOGY_MAX_RETRIES: String(input.topologyMaxRetries), AGENT_TOPOLOGY_MAX_RETRIES: String(input.topologyMaxRetries),
AGENT_RELATIONSHIP_MAX_CHILDREN: String(input.relationshipMaxChildren), AGENT_RELATIONSHIP_MAX_CHILDREN: String(input.relationshipMaxChildren),
AGENT_MERGE_CONFLICT_MAX_ATTEMPTS: String(input.mergeConflictMaxAttempts),
AGENT_PORT_BASE: String(input.portBase), AGENT_PORT_BASE: String(input.portBase),
AGENT_PORT_BLOCK_SIZE: String(input.portBlockSize), AGENT_PORT_BLOCK_SIZE: String(input.portBlockSize),
AGENT_PORT_BLOCK_COUNT: String(input.portBlockCount), AGENT_PORT_BLOCK_COUNT: String(input.portBlockCount),

View File

@@ -2,6 +2,7 @@ const state = {
config: null, config: null,
manifests: [], manifests: [],
sessions: [], sessions: [],
sessionMetadata: [],
runs: [], runs: [],
selectedSessionId: "", selectedSessionId: "",
selectedManifestPath: "", selectedManifestPath: "",
@@ -29,6 +30,11 @@ const dom = {
runValidationNodes: document.querySelector("#run-validation-nodes"), runValidationNodes: document.querySelector("#run-validation-nodes"),
killRun: document.querySelector("#kill-run"), killRun: document.querySelector("#kill-run"),
runStatus: document.querySelector("#run-status"), runStatus: document.querySelector("#run-status"),
sessionForm: document.querySelector("#session-form"),
sessionProjectPath: document.querySelector("#session-project-path"),
sessionCreate: document.querySelector("#session-create"),
sessionClose: document.querySelector("#session-close"),
sessionCloseMerge: document.querySelector("#session-close-merge"),
nodeInspector: document.querySelector("#node-inspector"), nodeInspector: document.querySelector("#node-inspector"),
eventsLimit: document.querySelector("#events-limit"), eventsLimit: document.querySelector("#events-limit"),
eventsRefresh: document.querySelector("#events-refresh"), eventsRefresh: document.querySelector("#events-refresh"),
@@ -78,6 +84,7 @@ const dom = {
cfgTopologyDepth: document.querySelector("#cfg-topology-depth"), cfgTopologyDepth: document.querySelector("#cfg-topology-depth"),
cfgTopologyRetries: document.querySelector("#cfg-topology-retries"), cfgTopologyRetries: document.querySelector("#cfg-topology-retries"),
cfgRelationshipChildren: document.querySelector("#cfg-relationship-children"), cfgRelationshipChildren: document.querySelector("#cfg-relationship-children"),
cfgMergeConflictAttempts: document.querySelector("#cfg-merge-conflict-attempts"),
cfgPortBase: document.querySelector("#cfg-port-base"), cfgPortBase: document.querySelector("#cfg-port-base"),
cfgPortBlockSize: document.querySelector("#cfg-port-block-size"), cfgPortBlockSize: document.querySelector("#cfg-port-block-size"),
cfgPortBlockCount: document.querySelector("#cfg-port-block-count"), cfgPortBlockCount: document.querySelector("#cfg-port-block-count"),
@@ -137,6 +144,8 @@ const LABEL_HELP_BY_CONTROL = Object.freeze({
"run-flags": "Optional JSON object passed in as initial run flags.", "run-flags": "Optional JSON object passed in as initial run flags.",
"run-runtime-context": "Optional JSON object of template values injected into persona prompts (for example repo or ticket).", "run-runtime-context": "Optional JSON object of template values injected into persona prompts (for example repo or ticket).",
"run-validation-nodes": "Optional comma-separated node IDs to simulate validation outcomes for.", "run-validation-nodes": "Optional comma-separated node IDs to simulate validation outcomes for.",
"session-project-path": "Absolute project path used when creating an explicit managed session.",
"session-close-merge": "When enabled, close will merge the session base branch back into the project branch.",
"events-limit": "Set how many recent runtime events are loaded per refresh.", "events-limit": "Set how many recent runtime events are loaded per refresh.",
"cfg-webhook-url": "Webhook endpoint that receives runtime event notifications.", "cfg-webhook-url": "Webhook endpoint that receives runtime event notifications.",
"cfg-webhook-severity": "Minimum severity level that triggers webhook notifications.", "cfg-webhook-severity": "Minimum severity level that triggers webhook notifications.",
@@ -152,6 +161,7 @@ const LABEL_HELP_BY_CONTROL = Object.freeze({
"cfg-topology-depth": "Maximum orchestration graph depth permitted by topology rules.", "cfg-topology-depth": "Maximum orchestration graph depth permitted by topology rules.",
"cfg-topology-retries": "Maximum retry expansions allowed by topology orchestration.", "cfg-topology-retries": "Maximum retry expansions allowed by topology orchestration.",
"cfg-relationship-children": "Maximum children each persona relationship can spawn.", "cfg-relationship-children": "Maximum children each persona relationship can spawn.",
"cfg-merge-conflict-attempts": "Maximum merge-conflict resolution attempts before emitting unresolved conflict events.",
"cfg-port-base": "Starting port number for provisioning port allocations.", "cfg-port-base": "Starting port number for provisioning port allocations.",
"cfg-port-block-size": "Number of ports reserved per allocated block.", "cfg-port-block-size": "Number of ports reserved per allocated block.",
"cfg-port-block-count": "Number of port blocks available for allocation.", "cfg-port-block-count": "Number of port blocks available for allocation.",
@@ -1036,6 +1046,7 @@ async function loadConfig() {
dom.cfgTopologyDepth.value = String(limits.topologyMaxDepth); dom.cfgTopologyDepth.value = String(limits.topologyMaxDepth);
dom.cfgTopologyRetries.value = String(limits.topologyMaxRetries); dom.cfgTopologyRetries.value = String(limits.topologyMaxRetries);
dom.cfgRelationshipChildren.value = String(limits.relationshipMaxChildren); dom.cfgRelationshipChildren.value = String(limits.relationshipMaxChildren);
dom.cfgMergeConflictAttempts.value = String(limits.mergeConflictMaxAttempts);
dom.cfgPortBase.value = String(limits.portBase); dom.cfgPortBase.value = String(limits.portBase);
dom.cfgPortBlockSize.value = String(limits.portBlockSize); dom.cfgPortBlockSize.value = String(limits.portBlockSize);
dom.cfgPortBlockCount.value = String(limits.portBlockCount); dom.cfgPortBlockCount.value = String(limits.portBlockCount);
@@ -1067,11 +1078,28 @@ function statusChipClass(status) {
return `status-chip status-${status || "unknown"}`; return `status-chip status-${status || "unknown"}`;
} }
function getSessionLifecycleStatus(sessionId) {
const metadata = state.sessionMetadata.find((entry) => entry?.sessionId === sessionId);
if (!metadata) {
return undefined;
}
const status = metadata.sessionStatus;
if (status === "active" || status === "suspended" || status === "closed" || status === "closed_with_conflicts") {
return status;
}
return undefined;
}
function renderRunsAndSessionsTable() { function renderRunsAndSessionsTable() {
const rows = []; const rows = [];
for (const session of state.sessions) { for (const session of state.sessions) {
const sessionStatus = session.status || "unknown"; const lifecycleStatus = getSessionLifecycleStatus(session.sessionId);
const sessionStatus =
lifecycleStatus === "closed" || lifecycleStatus === "closed_with_conflicts"
? lifecycleStatus
: session.status || lifecycleStatus || "unknown";
rows.push(` rows.push(`
<tr data-session-id="${escapeHtml(session.sessionId)}"> <tr data-session-id="${escapeHtml(session.sessionId)}">
<td>${escapeHtml(session.sessionId)}</td> <td>${escapeHtml(session.sessionId)}</td>
@@ -1099,6 +1127,7 @@ function renderRunsAndSessionsTable() {
async function loadSessions() { async function loadSessions() {
const payload = await apiRequest("/api/sessions"); const payload = await apiRequest("/api/sessions");
state.sessions = payload.sessions || []; state.sessions = payload.sessions || [];
state.sessionMetadata = payload.sessionMetadata || [];
state.runs = payload.runs || []; state.runs = payload.runs || [];
if (!state.selectedSessionId && state.sessions.length > 0) { if (!state.selectedSessionId && state.sessions.length > 0) {
@@ -1511,6 +1540,17 @@ async function startRun(event) {
simulateValidationNodeIds: fromCsv(dom.runValidationNodes.value), simulateValidationNodeIds: fromCsv(dom.runValidationNodes.value),
}; };
const selectedSessionMetadata = state.sessionMetadata.find(
(entry) => entry?.sessionId === state.selectedSessionId,
);
if (
selectedSessionMetadata &&
(selectedSessionMetadata.sessionStatus === "active" ||
selectedSessionMetadata.sessionStatus === "suspended")
) {
payload.sessionId = selectedSessionMetadata.sessionId;
}
if (manifestSelection === RUN_MANIFEST_EDITOR_VALUE) { if (manifestSelection === RUN_MANIFEST_EDITOR_VALUE) {
const manifestFromEditor = parseJsonSafe(dom.manifestEditor.value, null); const manifestFromEditor = parseJsonSafe(dom.manifestEditor.value, null);
if (!manifestFromEditor) { if (!manifestFromEditor) {
@@ -1566,6 +1606,64 @@ async function cancelActiveRun() {
} }
} }
async function createSessionFromUi() {
const projectPath = dom.sessionProjectPath.value.trim();
if (!projectPath) {
showRunStatus("Project path is required to create a session.", true);
return;
}
try {
const payload = await apiRequest("/api/sessions", {
method: "POST",
body: JSON.stringify({
projectPath,
}),
});
const created = payload.session;
if (created?.sessionId) {
state.selectedSessionId = created.sessionId;
showRunStatus(`Session ${created.sessionId} created.`);
} else {
showRunStatus("Session created.");
}
await loadSessions();
if (state.selectedSessionId) {
dom.sessionSelect.value = state.selectedSessionId;
await refreshGraph();
await refreshEvents();
}
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
}
}
async function closeSelectedSessionFromUi() {
const sessionId = state.selectedSessionId || dom.sessionSelect.value;
if (!sessionId) {
showRunStatus("Select a session before closing.", true);
return;
}
try {
const payload = await apiRequest(`/api/sessions/${encodeURIComponent(sessionId)}/close`, {
method: "POST",
body: JSON.stringify({
mergeToProject: dom.sessionCloseMerge.checked,
}),
});
const nextStatus = payload?.session?.sessionStatus || "closed";
showRunStatus(`Session ${sessionId} closed with status ${nextStatus}.`);
await loadSessions();
await refreshGraph();
await refreshEvents();
} catch (error) {
showRunStatus(error instanceof Error ? error.message : String(error), true);
}
}
async function saveNotifications(event) { async function saveNotifications(event) {
event.preventDefault(); event.preventDefault();
const payload = { const payload = {
@@ -1611,6 +1709,7 @@ async function saveLimits(event) {
topologyMaxDepth: Number(dom.cfgTopologyDepth.value), topologyMaxDepth: Number(dom.cfgTopologyDepth.value),
topologyMaxRetries: Number(dom.cfgTopologyRetries.value), topologyMaxRetries: Number(dom.cfgTopologyRetries.value),
relationshipMaxChildren: Number(dom.cfgRelationshipChildren.value), relationshipMaxChildren: Number(dom.cfgRelationshipChildren.value),
mergeConflictMaxAttempts: Number(dom.cfgMergeConflictAttempts.value),
portBase: Number(dom.cfgPortBase.value), portBase: Number(dom.cfgPortBase.value),
portBlockSize: Number(dom.cfgPortBlockSize.value), portBlockSize: Number(dom.cfgPortBlockSize.value),
portBlockCount: Number(dom.cfgPortBlockCount.value), portBlockCount: Number(dom.cfgPortBlockCount.value),
@@ -1737,6 +1836,12 @@ function bindUiEvents() {
dom.killRun.addEventListener("click", () => { dom.killRun.addEventListener("click", () => {
void cancelActiveRun(); void cancelActiveRun();
}); });
dom.sessionCreate.addEventListener("click", () => {
void createSessionFromUi();
});
dom.sessionClose.addEventListener("click", () => {
void closeSelectedSessionFromUi();
});
dom.notificationsForm.addEventListener("submit", (event) => { dom.notificationsForm.addEventListener("submit", (event) => {
void saveNotifications(event); void saveNotifications(event);

View File

@@ -90,6 +90,23 @@
</form> </form>
<div id="run-status" class="subtle"></div> <div id="run-status" class="subtle"></div>
<div class="divider"></div>
<h3>Session Controls</h3>
<form id="session-form" class="stacked-form">
<label>
Project Path (absolute)
<input id="session-project-path" type="text" placeholder="/abs/path/to/project" />
</label>
<label class="inline-checkbox">
<input id="session-close-merge" type="checkbox" />
Merge base into project when closing selected session
</label>
<div class="inline-actions">
<button id="session-create" type="button">Create Session</button>
<button id="session-close" type="button">Close Selected Session</button>
</div>
</form>
<div class="divider"></div> <div class="divider"></div>
<h3>Node Inspector</h3> <h3>Node Inspector</h3>
<div id="node-inspector" class="inspector empty">Select a graph node.</div> <div id="node-inspector" class="inspector empty">Select a graph node.</div>
@@ -196,6 +213,7 @@
<label>AGENT_TOPOLOGY_MAX_DEPTH<input id="cfg-topology-depth" type="number" min="1" /></label> <label>AGENT_TOPOLOGY_MAX_DEPTH<input id="cfg-topology-depth" type="number" min="1" /></label>
<label>AGENT_TOPOLOGY_MAX_RETRIES<input id="cfg-topology-retries" type="number" min="0" /></label> <label>AGENT_TOPOLOGY_MAX_RETRIES<input id="cfg-topology-retries" type="number" min="0" /></label>
<label>AGENT_RELATIONSHIP_MAX_CHILDREN<input id="cfg-relationship-children" type="number" min="1" /></label> <label>AGENT_RELATIONSHIP_MAX_CHILDREN<input id="cfg-relationship-children" type="number" min="1" /></label>
<label>AGENT_MERGE_CONFLICT_MAX_ATTEMPTS<input id="cfg-merge-conflict-attempts" type="number" min="1" /></label>
<label>AGENT_PORT_BASE<input id="cfg-port-base" type="number" min="1" /></label> <label>AGENT_PORT_BASE<input id="cfg-port-base" type="number" min="1" /></label>
<label>AGENT_PORT_BLOCK_SIZE<input id="cfg-port-block-size" type="number" min="1" /></label> <label>AGENT_PORT_BLOCK_SIZE<input id="cfg-port-block-size" type="number" min="1" /></label>
<label>AGENT_PORT_BLOCK_COUNT<input id="cfg-port-block-count" type="number" min="1" /></label> <label>AGENT_PORT_BLOCK_COUNT<input id="cfg-port-block-count" type="number" min="1" /></label>

View File

@@ -142,6 +142,12 @@ label {
letter-spacing: 0.015em; letter-spacing: 0.015em;
} }
label.inline-checkbox {
flex-direction: row;
align-items: center;
gap: 0.45rem;
}
input, input,
select, select,
textarea, textarea,
@@ -353,6 +359,22 @@ button.danger {
border-color: rgba(255, 201, 74, 0.6); border-color: rgba(255, 201, 74, 0.6);
} }
.status-active {
color: var(--accent-cool);
border-color: rgba(86, 195, 255, 0.6);
}
.status-suspended,
.status-closed_with_conflicts {
color: var(--warn);
border-color: rgba(255, 201, 74, 0.6);
}
.status-closed {
color: var(--muted);
border-color: rgba(155, 184, 207, 0.45);
}
.status-unknown { .status-unknown {
color: var(--muted); color: var(--muted);
border-color: rgba(155, 184, 207, 0.45); border-color: rgba(155, 184, 207, 0.45);

View File