Update UI for session/conflict controls and remove workspace dir
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user