bosun 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module task/task-executor-pipeline
|
|
3
|
+
* @description Pipeline orchestration helpers extracted from task-executor.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FanoutPipeline, RacePipeline, SequentialPipeline } from "./pipeline.mjs";
|
|
7
|
+
|
|
8
|
+
function toAgentList(agents) {
|
|
9
|
+
if (Array.isArray(agents)) return agents.filter(Boolean);
|
|
10
|
+
return [agents].filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeMode(mode) {
|
|
14
|
+
return String(mode || "single").trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildStageRunner(agent, stageIndex, runner) {
|
|
18
|
+
const id = String(agent?.id || agent?.name || `agent-${stageIndex + 1}`);
|
|
19
|
+
const name = String(agent?.name || agent?.id || id);
|
|
20
|
+
return {
|
|
21
|
+
...agent,
|
|
22
|
+
id,
|
|
23
|
+
name,
|
|
24
|
+
async run(input, context) {
|
|
25
|
+
return runner(agent, input, context);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createExecutionPipeline(mode = "single", agents = [], options = {}) {
|
|
31
|
+
const list = toAgentList(agents);
|
|
32
|
+
if (list.length === 0) {
|
|
33
|
+
throw new Error("At least one pipeline agent is required");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const runAgent =
|
|
37
|
+
typeof options.agentRunner === "function" ? options.agentRunner : null;
|
|
38
|
+
if (!runAgent) {
|
|
39
|
+
throw new Error("Execution pipeline requires options.agentRunner");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizedMode = normalizeMode(mode);
|
|
43
|
+
const stages = list.map((agent, stageIndex) =>
|
|
44
|
+
buildStageRunner(agent, stageIndex, runAgent));
|
|
45
|
+
const pipelineOptions = {
|
|
46
|
+
id: options.id || `${options.task?.id || "task"}-${normalizedMode}`,
|
|
47
|
+
name: options.name || `${normalizedMode}-execution`,
|
|
48
|
+
metadata: {
|
|
49
|
+
taskId: options.task?.id || null,
|
|
50
|
+
mode: normalizedMode,
|
|
51
|
+
source: "task-executor",
|
|
52
|
+
...(options.metadata && typeof options.metadata === "object" ? options.metadata : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (["parallel", "fanout", "parallel-slots"].includes(normalizedMode)) {
|
|
57
|
+
return FanoutPipeline(stages, pipelineOptions);
|
|
58
|
+
}
|
|
59
|
+
if (["failover", "race"].includes(normalizedMode)) {
|
|
60
|
+
return RacePipeline(stages, pipelineOptions);
|
|
61
|
+
}
|
|
62
|
+
return SequentialPipeline(stages, pipelineOptions);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runExecutionPipeline(mode, agents, input, options = {}) {
|
|
66
|
+
const pipeline = createExecutionPipeline(mode, agents, options);
|
|
67
|
+
return pipeline.run(input, {
|
|
68
|
+
metadata: options.metadata || {},
|
|
69
|
+
signal: options.signal || null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runExecutionPipelineAgent(
|
|
74
|
+
agent,
|
|
75
|
+
input,
|
|
76
|
+
context,
|
|
77
|
+
options = {},
|
|
78
|
+
) {
|
|
79
|
+
const runWithRetry = options.execWithRetry;
|
|
80
|
+
if (typeof runWithRetry !== "function") {
|
|
81
|
+
throw new Error("runExecutionPipelineAgent requires options.execWithRetry");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prompt =
|
|
85
|
+
typeof agent?.prompt === "string" && agent.prompt.trim()
|
|
86
|
+
? agent.prompt
|
|
87
|
+
: [
|
|
88
|
+
"Task execution pipeline",
|
|
89
|
+
`Mode: ${context?.options?.metadata?.mode || context?.pipelineType || "sequential"}`,
|
|
90
|
+
`Agent role: ${agent?.role || agent?.name || agent?.id || "agent"}`,
|
|
91
|
+
"Use only the structured input below; do not rely on prior conversation state.",
|
|
92
|
+
typeof input === "string" ? input : JSON.stringify(input, null, 2),
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join("\n\n");
|
|
96
|
+
|
|
97
|
+
const stageIndex = Number.isInteger(context?.stageIndex)
|
|
98
|
+
? context.stageIndex
|
|
99
|
+
: Number.isInteger(context?.agentIndex)
|
|
100
|
+
? context.agentIndex
|
|
101
|
+
: 0;
|
|
102
|
+
const pipelineId = String(
|
|
103
|
+
context?.options?.id || context?.runId || context?.pipeline?.id || "pipeline",
|
|
104
|
+
);
|
|
105
|
+
const taskKey = `${pipelineId}-${agent?.id || agent?.name || "agent"}-${stageIndex + 1}`;
|
|
106
|
+
const result = await runWithRetry(prompt, {
|
|
107
|
+
taskKey,
|
|
108
|
+
cwd: options.repoRoot || process.cwd(),
|
|
109
|
+
timeoutMs: Number(agent?.timeoutMs || options.timeoutMs || 0) || undefined,
|
|
110
|
+
maxRetries: Number(agent?.maxRetries || 1) || 1,
|
|
111
|
+
sdk: agent?.sdk || agent?.executor || undefined,
|
|
112
|
+
model: agent?.model || undefined,
|
|
113
|
+
sessionType: "task-pipeline",
|
|
114
|
+
signal: context?.signal || null,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!result?.success) {
|
|
118
|
+
throw new Error(result?.error || `Pipeline agent ${agent?.id || "agent"} failed`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
success: true,
|
|
123
|
+
output: {
|
|
124
|
+
text: result.output,
|
|
125
|
+
sdk: result.sdk,
|
|
126
|
+
taskKey,
|
|
127
|
+
},
|
|
128
|
+
text: result.output,
|
|
129
|
+
meta: {
|
|
130
|
+
taskKey,
|
|
131
|
+
sdk: result.sdk,
|
|
132
|
+
},
|
|
133
|
+
tokensUsed:
|
|
134
|
+
Number(result.tokensUsed || result.usage?.totalTokens || result.usage?.total_tokens || 0) ||
|
|
135
|
+
0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default {
|
|
140
|
+
createExecutionPipeline,
|
|
141
|
+
runExecutionPipeline,
|
|
142
|
+
runExecutionPipelineAgent,
|
|
143
|
+
};
|
package/task/task-executor.mjs
CHANGED
|
@@ -102,6 +102,11 @@ import {
|
|
|
102
102
|
} from "./task-claims.mjs";
|
|
103
103
|
import { initPresence, getPresenceState } from "../infra/presence.mjs";
|
|
104
104
|
import { getSharedState } from "../workspace/shared-state-manager.mjs";
|
|
105
|
+
import {
|
|
106
|
+
createExecutionPipeline as createTaskExecutionPipeline,
|
|
107
|
+
runExecutionPipeline as runTaskExecutionPipeline,
|
|
108
|
+
runExecutionPipelineAgent,
|
|
109
|
+
} from "./task-executor-pipeline.mjs";
|
|
105
110
|
|
|
106
111
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
107
112
|
|
|
@@ -3981,38 +3986,18 @@ class TaskExecutor {
|
|
|
3981
3986
|
const isFreshEnough =
|
|
3982
3987
|
ageMs === 0 || ageMs <= INPROGRESS_RECOVERY_MAX_AGE_MS;
|
|
3983
3988
|
|
|
3984
|
-
// In workflow-owned mode,
|
|
3985
|
-
//
|
|
3986
|
-
//
|
|
3987
|
-
//
|
|
3988
|
-
//
|
|
3989
|
-
// • Active thread → workflow is still managing it; leave it alone.
|
|
3990
|
-
// • No active thread but fresh → agent died; reset to todo so
|
|
3991
|
-
// trigger.task_available re-dispatches cleanly, without double-dispatch.
|
|
3989
|
+
// In workflow-owned mode, executor thread presence is not a reliable
|
|
3990
|
+
// liveness signal because workflow nodes can be actively running before
|
|
3991
|
+
// any executor thread key is observable. Resetting "fresh" in-progress
|
|
3992
|
+
// tasks here causes live runs to churn todo↔inprogress. Keep fresh tasks
|
|
3993
|
+
// in-progress and let stale/unstarted guards above handle true stranding.
|
|
3992
3994
|
if (this.workflowOwnsTaskLifecycle) {
|
|
3993
3995
|
if (hasThread) {
|
|
3994
3996
|
skippedForActiveClaim++;
|
|
3995
3997
|
continue;
|
|
3996
3998
|
}
|
|
3997
3999
|
if (isFreshEnough) {
|
|
3998
|
-
|
|
3999
|
-
await transitionTaskStatus(id, "todo", {
|
|
4000
|
-
source: "task-executor-recovery-workflow-owned",
|
|
4001
|
-
});
|
|
4002
|
-
} catch {
|
|
4003
|
-
/* best effort */
|
|
4004
|
-
}
|
|
4005
|
-
try {
|
|
4006
|
-
transitionInternalTaskStatus(
|
|
4007
|
-
id,
|
|
4008
|
-
"todo",
|
|
4009
|
-
"task-executor-recovery-workflow-owned",
|
|
4010
|
-
);
|
|
4011
|
-
} catch {
|
|
4012
|
-
/* best effort */
|
|
4013
|
-
}
|
|
4014
|
-
this._removeRuntimeSlot(id);
|
|
4015
|
-
resetToTodo++;
|
|
4000
|
+
skippedForActiveClaim++;
|
|
4016
4001
|
continue;
|
|
4017
4002
|
}
|
|
4018
4003
|
}
|
|
@@ -5041,6 +5026,30 @@ class TaskExecutor {
|
|
|
5041
5026
|
// See workflow-templates/task-lifecycle.mjs
|
|
5042
5027
|
}
|
|
5043
5028
|
|
|
5029
|
+
createExecutionPipeline(mode = "single", agents = [], options = {}) {
|
|
5030
|
+
return createTaskExecutionPipeline(mode, agents, {
|
|
5031
|
+
...options,
|
|
5032
|
+
agentRunner:
|
|
5033
|
+
options.agentRunner || this._runExecutionPipelineAgent.bind(this),
|
|
5034
|
+
});
|
|
5035
|
+
}
|
|
5036
|
+
|
|
5037
|
+
async runExecutionPipeline(mode, agents, input, options = {}) {
|
|
5038
|
+
return await runTaskExecutionPipeline(mode, agents, input, {
|
|
5039
|
+
...options,
|
|
5040
|
+
agentRunner:
|
|
5041
|
+
options.agentRunner || this._runExecutionPipelineAgent.bind(this),
|
|
5042
|
+
});
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
async _runExecutionPipelineAgent(agent, input, context) {
|
|
5046
|
+
return runExecutionPipelineAgent(agent, input, context, {
|
|
5047
|
+
execWithRetry,
|
|
5048
|
+
repoRoot: this.repoRoot || process.cwd(),
|
|
5049
|
+
timeoutMs: this.timeoutMs || 0,
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5044
5053
|
// ── Task Execution ────────────────────────────────────────────────────────
|
|
5045
5054
|
|
|
5046
5055
|
_getWorktreeManager(repoRoot) {
|
|
@@ -5491,7 +5500,7 @@ export function loadExecutorOptionsFromConfig() {
|
|
|
5491
5500
|
return {
|
|
5492
5501
|
mode: envMode || configExec.mode || "internal",
|
|
5493
5502
|
maxParallel: Number(
|
|
5494
|
-
process.env.INTERNAL_EXECUTOR_PARALLEL || configExec.maxParallel ||
|
|
5503
|
+
process.env.INTERNAL_EXECUTOR_PARALLEL || configExec.maxParallel || 5,
|
|
5495
5504
|
),
|
|
5496
5505
|
baseBranchParallelLimit: Number(
|
|
5497
5506
|
process.env.INTERNAL_EXECUTOR_BASE_BRANCH_PARALLEL ||
|
|
@@ -1,71 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
if (!token) {
|
|
6
|
-
console.error("Missing TELEGRAM_BOT_TOKEN environment variable.");
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const url = `https://api.telegram.org/bot${token}/getUpdates`;
|
|
11
|
-
|
|
12
|
-
async function main() {
|
|
13
|
-
let res;
|
|
14
|
-
try {
|
|
15
|
-
res = await fetch(url);
|
|
16
|
-
} catch (err) {
|
|
17
|
-
console.error(`Fetch error: ${err.message}`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!res.ok) {
|
|
22
|
-
const body = await res.text();
|
|
23
|
-
console.error(`Request failed: ${res.status} ${body}`);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const data = await res.json();
|
|
28
|
-
if (!data.result || data.result.length === 0) {
|
|
29
|
-
console.log(
|
|
30
|
-
"No updates found. Send a message to the bot first, then retry.",
|
|
31
|
-
);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
34
5
|
|
|
6
|
+
export function extractTelegramChatsFromUpdates(updates) {
|
|
35
7
|
const chats = new Map();
|
|
36
|
-
for (const update of
|
|
37
|
-
const message =
|
|
38
|
-
|
|
39
|
-
if (!
|
|
8
|
+
for (const update of Array.isArray(updates) ? updates : []) {
|
|
9
|
+
const message = update?.message || update?.channel_post || update?.edited_message;
|
|
10
|
+
const chat = message?.chat;
|
|
11
|
+
if (!chat?.id) {
|
|
40
12
|
continue;
|
|
41
13
|
}
|
|
42
|
-
const chat = message.chat;
|
|
43
14
|
if (!chats.has(chat.id)) {
|
|
44
15
|
chats.set(chat.id, {
|
|
45
16
|
id: chat.id,
|
|
46
|
-
type: chat.type,
|
|
17
|
+
type: chat.type || "unknown",
|
|
47
18
|
title: chat.title || "",
|
|
48
19
|
username: chat.username || "",
|
|
49
20
|
});
|
|
50
21
|
}
|
|
51
22
|
}
|
|
23
|
+
return Array.from(chats.values());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function discoverTelegramChats(token, { fetchImpl = fetch } = {}) {
|
|
27
|
+
const normalizedToken = String(token || "").trim();
|
|
28
|
+
if (!normalizedToken) {
|
|
29
|
+
throw new Error("Missing TELEGRAM_BOT_TOKEN environment variable.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const url = `https://api.telegram.org/bot${normalizedToken}/getUpdates`;
|
|
33
|
+
const response = await fetchImpl(url);
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const body = await response.text().catch(() => "");
|
|
36
|
+
throw new Error(`Request failed: ${response.status}${body ? ` ${body}` : ""}`);
|
|
37
|
+
}
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const updates = data?.result;
|
|
41
|
+
const chats = extractTelegramChatsFromUpdates(updates);
|
|
42
|
+
if (Array.isArray(updates) && updates.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
chats,
|
|
45
|
+
message: "No updates found. Send a message to the bot first, then retry.",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (chats.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
chats,
|
|
51
|
+
message: "No chat IDs found in updates. Send a message to the bot first.",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { chats, message: null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
59
|
+
const { chats, message } = await discoverTelegramChats(token);
|
|
60
|
+
if (message) {
|
|
61
|
+
console.log(message);
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
console.log("Found chat IDs:");
|
|
61
|
-
for (const chat of chats
|
|
62
|
-
const titlePart = chat.title ? ` title
|
|
66
|
+
for (const chat of chats) {
|
|
67
|
+
const titlePart = chat.title ? ` title="${chat.title}"` : "";
|
|
63
68
|
const userPart = chat.username ? ` username=@${chat.username}` : "";
|
|
64
69
|
console.log(`- id=${chat.id} type=${chat.type}${userPart}${titlePart}`);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
74
|
+
if (process.argv[1] && resolve(process.argv[1]) === resolve(__filename)) {
|
|
75
|
+
try {
|
|
76
|
+
await main();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error(`Error: ${err.message || err}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -470,7 +470,7 @@ function ExecutorConfigPanel({ ws }) {
|
|
|
470
470
|
|
|
471
471
|
<${Collapse} in=${expanded}>
|
|
472
472
|
<${Stack} spacing=${1.5} sx=${{ pt: 1, pb: 0.5, px: 0.5 }}>
|
|
473
|
-
|
|
473
|
+
|
|
474
474
|
<${Box}>
|
|
475
475
|
<${Typography} variant="caption" color="text.secondary" sx=${{ mb: 0.5, display: "block" }}>
|
|
476
476
|
Max Concurrent Executors: ${maxConcurrent}
|
|
@@ -492,7 +492,7 @@ function ExecutorConfigPanel({ ws }) {
|
|
|
492
492
|
/>
|
|
493
493
|
<//>
|
|
494
494
|
|
|
495
|
-
|
|
495
|
+
|
|
496
496
|
<${Stack} direction="row" spacing=${1} alignItems="center">
|
|
497
497
|
<${Typography} variant="caption" color="text.secondary">Pool:<//>
|
|
498
498
|
<${ToggleButtonGroup}
|
|
@@ -515,7 +515,7 @@ function ExecutorConfigPanel({ ws }) {
|
|
|
515
515
|
<//>
|
|
516
516
|
<//>
|
|
517
517
|
|
|
518
|
-
|
|
518
|
+
|
|
519
519
|
${pool === "shared" && html`
|
|
520
520
|
<${Box}>
|
|
521
521
|
<${Typography} variant="caption" color="text.secondary" sx=${{ mb: 0.5, display: "block" }}>
|
|
@@ -533,7 +533,7 @@ function ExecutorConfigPanel({ ws }) {
|
|
|
533
533
|
<//>
|
|
534
534
|
`}
|
|
535
535
|
|
|
536
|
-
|
|
536
|
+
|
|
537
537
|
${hasChanges && html`
|
|
538
538
|
<${Button}
|
|
539
539
|
size="small"
|
|
@@ -630,12 +630,12 @@ function WorkspaceCard({ ws }) {
|
|
|
630
630
|
<//>
|
|
631
631
|
<//>
|
|
632
632
|
|
|
633
|
-
|
|
633
|
+
|
|
634
634
|
<${Box} sx=${{ mt: 1.5, mb: 0.5 }}>
|
|
635
635
|
<${WorkspaceStateToggle} ws=${ws} />
|
|
636
636
|
<//>
|
|
637
637
|
|
|
638
|
-
|
|
638
|
+
|
|
639
639
|
<${ExecutorConfigPanel} ws=${ws} />
|
|
640
640
|
<//>
|
|
641
641
|
|
|
@@ -780,7 +780,7 @@ export function WorkspaceManager({ open, onClose }) {
|
|
|
780
780
|
|
|
781
781
|
return html`
|
|
782
782
|
<${Modal} title="Manage Workspaces" open=${open} onClose=${onClose}>
|
|
783
|
-
|
|
783
|
+
|
|
784
784
|
${wsList.length > 0 && html`
|
|
785
785
|
<${Stack} direction="row" spacing=${1.5} sx=${{ mb: 2 }} alignItems="center">
|
|
786
786
|
${Object.entries(STATE_CONFIG).map(([key, cfg]) => html`
|