bosun 0.41.7 → 0.41.9
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/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +251 -11
- package/agent/agent-prompts.mjs +5 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +4 -1
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +25 -7
- package/infra/session-tracker.mjs +30 -3
- package/package.json +10 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +287 -258
- package/server/ui-server.mjs +218 -23
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-model-profiles.mjs +166 -29
- package/shell/codex-shell.mjs +56 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +2 -0
- package/ui/demo-defaults.js +6 -6
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/workflow/workflow-engine.mjs +16 -4
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +489 -153
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/github.mjs +106 -16
- package/workspace/worktree-manager.mjs +1 -1
package/shell/codex-shell.mjs
CHANGED
|
@@ -78,6 +78,56 @@ function normalizeTimeoutMs(value, { fallback = DEFAULT_TIMEOUT_MS, label = "tim
|
|
|
78
78
|
return normalized;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function isAzureOpenAIBaseUrl(value) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(String(value || ""));
|
|
84
|
+
const host = String(parsed.hostname || "").toLowerCase();
|
|
85
|
+
return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCodexSdkRuntime(streamProviderOverrides, envInput = process.env) {
|
|
92
|
+
const { env: resolvedEnv } = resolveCodexProfileRuntime(envInput);
|
|
93
|
+
const baseUrl = resolvedEnv.OPENAI_BASE_URL || "";
|
|
94
|
+
const isAzure = isAzureOpenAIBaseUrl(baseUrl);
|
|
95
|
+
const env = { ...resolvedEnv };
|
|
96
|
+
|
|
97
|
+
delete env.OPENAI_BASE_URL;
|
|
98
|
+
|
|
99
|
+
if (isAzure && env.OPENAI_API_KEY && !env.AZURE_OPENAI_API_KEY) {
|
|
100
|
+
env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const providerName = isAzure ? "azure" : "openai";
|
|
104
|
+
const config = {
|
|
105
|
+
model_providers: {
|
|
106
|
+
[providerName]: isAzure
|
|
107
|
+
? {
|
|
108
|
+
name: "Azure OpenAI",
|
|
109
|
+
base_url: baseUrl,
|
|
110
|
+
env_key: "AZURE_OPENAI_API_KEY",
|
|
111
|
+
wire_api: "responses",
|
|
112
|
+
...streamProviderOverrides,
|
|
113
|
+
}
|
|
114
|
+
: streamProviderOverrides,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (isAzure && env.CODEX_MODEL) {
|
|
119
|
+
config.model_provider = "azure";
|
|
120
|
+
config.model = env.CODEX_MODEL;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
env,
|
|
125
|
+
config,
|
|
126
|
+
providerName,
|
|
127
|
+
streamIdleTimeoutMs: streamProviderOverrides.stream_idle_timeout_ms,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
81
131
|
function getInternalExecutorStreamConfig() {
|
|
82
132
|
try {
|
|
83
133
|
const cfg = loadConfig();
|
|
@@ -443,9 +493,6 @@ async function getThread() {
|
|
|
443
493
|
if (activeThread) return activeThread;
|
|
444
494
|
const threadOptions = buildThreadOptions();
|
|
445
495
|
|
|
446
|
-
const { env: resolvedEnv } = resolveCodexProfileRuntime(process.env);
|
|
447
|
-
Object.assign(process.env, resolvedEnv);
|
|
448
|
-
|
|
449
496
|
if (!codexInstance) {
|
|
450
497
|
const Cls = await loadCodexSdk();
|
|
451
498
|
if (!Cls) throw new Error("Codex SDK not available");
|
|
@@ -454,23 +501,16 @@ async function getThread() {
|
|
|
454
501
|
// even if config.toml hasn't been patched by codex-config.mjs yet.
|
|
455
502
|
// This is the most reliable path for Azure/Foundry deployments where
|
|
456
503
|
// dropped SSE streams ("response.failed") are the dominant failure mode.
|
|
457
|
-
const providerName = (() => {
|
|
458
|
-
try {
|
|
459
|
-
const parsed = new URL(String(resolvedEnv.OPENAI_BASE_URL || ""));
|
|
460
|
-
const host = String(parsed.hostname || "").toLowerCase();
|
|
461
|
-
return host === "openai.azure.com" || host.endsWith(".openai.azure.com")
|
|
462
|
-
? "azure"
|
|
463
|
-
: "openai";
|
|
464
|
-
} catch {
|
|
465
|
-
return "openai";
|
|
466
|
-
}
|
|
467
|
-
})();
|
|
468
504
|
const STREAM_IDLE_TIMEOUT_MS = 3_600_000; // 60 min — matches Azure max stream lifetime
|
|
469
505
|
const streamProviderOverrides = {
|
|
470
506
|
stream_idle_timeout_ms: STREAM_IDLE_TIMEOUT_MS,
|
|
471
507
|
stream_max_retries: 15,
|
|
472
508
|
request_max_retries: 6,
|
|
473
509
|
};
|
|
510
|
+
const runtime = buildCodexSdkRuntime(streamProviderOverrides, process.env);
|
|
511
|
+
|
|
512
|
+
Object.assign(process.env, runtime.env);
|
|
513
|
+
delete process.env.OPENAI_BASE_URL;
|
|
474
514
|
|
|
475
515
|
codexInstance = new Cls({
|
|
476
516
|
config: {
|
|
@@ -481,13 +521,11 @@ async function getThread() {
|
|
|
481
521
|
undo: true,
|
|
482
522
|
steer: true,
|
|
483
523
|
},
|
|
484
|
-
|
|
485
|
-
[providerName]: streamProviderOverrides,
|
|
486
|
-
},
|
|
524
|
+
...runtime.config,
|
|
487
525
|
},
|
|
488
526
|
});
|
|
489
527
|
|
|
490
|
-
console.log(`[codex-shell] created Codex instance (provider=${providerName}, stream_idle_timeout=${
|
|
528
|
+
console.log(`[codex-shell] created Codex instance (provider=${runtime.providerName}, stream_idle_timeout=${runtime.streamIdleTimeoutMs}ms, stream_max_retries=${streamProviderOverrides.stream_max_retries})`);
|
|
491
529
|
}
|
|
492
530
|
|
|
493
531
|
const transport = resolveCodexTransport();
|
|
@@ -91,22 +91,33 @@ async function discoverViaSDK(existingClient = null) {
|
|
|
91
91
|
// Try to connect to an already-running server
|
|
92
92
|
try {
|
|
93
93
|
const port = Number(process.env.OPENCODE_PORT || "4096");
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
try {
|
|
95
|
+
client = sdk.createOpencodeClient({
|
|
96
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
97
|
+
timeout: 5_000,
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
client = sdk.createOpencodeClient({
|
|
101
|
+
hostname: "127.0.0.1",
|
|
102
|
+
port,
|
|
103
|
+
timeout: 5_000,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
100
106
|
} catch {
|
|
101
107
|
return null;
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
try {
|
|
112
|
+
const directory = process.cwd();
|
|
113
|
+
const requestOptions = directory
|
|
114
|
+
? { query: { directory } }
|
|
115
|
+
: undefined;
|
|
116
|
+
|
|
106
117
|
// Fetch provider list + auth methods in parallel
|
|
107
118
|
const [providerRes, authRes] = await Promise.all([
|
|
108
|
-
client.provider.list().catch(() => null),
|
|
109
|
-
client.provider.auth().catch(() => null),
|
|
119
|
+
client.provider.list(requestOptions).catch(() => null),
|
|
120
|
+
client.provider.auth(requestOptions).catch(() => null),
|
|
110
121
|
]);
|
|
111
122
|
|
|
112
123
|
if (!providerRes?.data) return null;
|
|
@@ -481,3 +492,4 @@ export function buildExecutorEntry(providerID, modelFullId, overrides = {}) {
|
|
|
481
492
|
export function invalidateCache() {
|
|
482
493
|
_providerCache = { data: null, ts: 0 };
|
|
483
494
|
}
|
|
495
|
+
|
package/task/task-executor.mjs
CHANGED
|
@@ -4278,6 +4278,34 @@ class TaskExecutor {
|
|
|
4278
4278
|
};
|
|
4279
4279
|
}
|
|
4280
4280
|
|
|
4281
|
+
resetTaskThrottleState(taskId, options = {}) {
|
|
4282
|
+
const key = normalizeTaskIdKey(taskId);
|
|
4283
|
+
if (!key) return false;
|
|
4284
|
+
|
|
4285
|
+
let changed = false;
|
|
4286
|
+
if (options.clearNoCommit !== false && this._noCommitCounts.delete(key)) {
|
|
4287
|
+
changed = true;
|
|
4288
|
+
}
|
|
4289
|
+
if (options.clearSkipUntil !== false && this._skipUntil.delete(key)) {
|
|
4290
|
+
changed = true;
|
|
4291
|
+
}
|
|
4292
|
+
if (options.clearCooldowns !== false && this._taskCooldowns.delete(key)) {
|
|
4293
|
+
changed = true;
|
|
4294
|
+
}
|
|
4295
|
+
if (this._idleContinueCounts.delete(key)) {
|
|
4296
|
+
changed = true;
|
|
4297
|
+
}
|
|
4298
|
+
if (this._repoAreaBlockedTasks.has(key)) {
|
|
4299
|
+
this._repoAreaBlockedTasks.delete(key);
|
|
4300
|
+
changed = true;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
if (changed) {
|
|
4304
|
+
this._saveNoCommitState();
|
|
4305
|
+
}
|
|
4306
|
+
return changed;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4281
4309
|
setBacklogReplenishmentConfig(patch = {}) {
|
|
4282
4310
|
if (!patch || typeof patch !== "object") {
|
|
4283
4311
|
return this.getBacklogReplenishmentConfig();
|
package/task/task-store.mjs
CHANGED
|
@@ -101,15 +101,25 @@ function resolveBosunHomeDir() {
|
|
|
101
101
|
return resolve(base, "bosun");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function resolveExplicitRepoRoot() {
|
|
105
|
+
const explicit = String(process.env.REPO_ROOT || "").trim();
|
|
106
|
+
if (!explicit) return null;
|
|
107
|
+
return resolve(explicit);
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function resolvePersistentStorePath() {
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
return resolve(
|
|
111
|
+
const explicitRepoRoot = resolveExplicitRepoRoot();
|
|
112
|
+
if (explicitRepoRoot) {
|
|
113
|
+
return resolve(explicitRepoRoot, ".bosun", ".cache", "kanban-state.json");
|
|
108
114
|
}
|
|
109
115
|
const bosunHome = resolveBosunHomeDir();
|
|
110
116
|
if (bosunHome) {
|
|
111
117
|
return resolve(bosunHome, ".cache", "kanban-state.json");
|
|
112
118
|
}
|
|
119
|
+
const repoRoot = inferRepoRoot(process.cwd());
|
|
120
|
+
if (repoRoot) {
|
|
121
|
+
return resolve(repoRoot, ".bosun", ".cache", "kanban-state.json");
|
|
122
|
+
}
|
|
113
123
|
return resolve(__dirname, "..", ".cache", "kanban-state.json");
|
|
114
124
|
}
|
|
115
125
|
|
|
@@ -2831,4 +2841,3 @@ export function getStaleInReviewTasks(maxAgeMs) {
|
|
|
2831
2841
|
(t) => t.status === "inreview" && t.lastActivityAt < cutoff,
|
|
2832
2842
|
);
|
|
2833
2843
|
}
|
|
2834
|
-
|
package/tools/list-todos.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* list-todos — Scan codebase for TODO/FIXME/HACK/XXX/BUG comment markers
|
|
4
4
|
*
|
|
5
|
-
* Usage: node list-todos.mjs [rootDir] [--json]
|
|
5
|
+
* Usage: node list-todos.mjs [rootDir] [--json] [--help]
|
|
6
6
|
* rootDir defaults to cwd
|
|
7
7
|
* --json emit JSON array instead of human-readable output
|
|
8
|
+
* --help show usage and exit without scanning
|
|
8
9
|
*
|
|
9
10
|
* Exit 0 always (informational tool).
|
|
10
11
|
*/
|
|
@@ -12,6 +13,11 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
12
13
|
import { resolve, relative, extname } from "node:path";
|
|
13
14
|
|
|
14
15
|
const args = process.argv.slice(2);
|
|
16
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
|
|
17
|
+
console.log("Usage: node list-todos.mjs [rootDir] [--json] [--help]");
|
|
18
|
+
console.log("Scans for TODO/FIXME/HACK/XXX/TEMP/BUG comment markers.");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
15
21
|
const jsonMode = args.includes("--json");
|
|
16
22
|
const ROOT = args.find((a) => !a.startsWith("-")) || process.cwd();
|
|
17
23
|
|
package/ui/app.js
CHANGED
|
@@ -314,7 +314,7 @@ import { LogsTab } from "./tabs/logs.js";
|
|
|
314
314
|
import { TelemetryTab } from "./tabs/telemetry.js";
|
|
315
315
|
import { SettingsTab } from "./tabs/settings.js";
|
|
316
316
|
import { WorkflowsTab } from "./tabs/workflows.js";
|
|
317
|
-
import { LibraryTab } from "./tabs/library.js";
|
|
317
|
+
import { LibraryTab, LibraryMarketplaceTab } from "./tabs/library.js";
|
|
318
318
|
import { ManualFlowsTab } from "./tabs/manual-flows.js";
|
|
319
319
|
|
|
320
320
|
/* ── Shared components ── */
|
|
@@ -603,6 +603,7 @@ const TAB_COMPONENTS = {
|
|
|
603
603
|
workflows: WorkflowsTab,
|
|
604
604
|
"manual-flows": ManualFlowsTab,
|
|
605
605
|
library: LibraryTab,
|
|
606
|
+
marketplace: LibraryMarketplaceTab,
|
|
606
607
|
settings: SettingsTab,
|
|
607
608
|
};
|
|
608
609
|
|
|
@@ -1228,7 +1229,7 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
1228
1229
|
* Bottom Navigation
|
|
1229
1230
|
* ═══════════════════════════════════════════════ */
|
|
1230
1231
|
const PRIMARY_NAV_TABS = ["dashboard", "chat", "tasks", "agents"];
|
|
1231
|
-
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "workflows", "manual-flows", "settings"];
|
|
1232
|
+
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "marketplace", "workflows", "manual-flows", "settings"];
|
|
1232
1233
|
|
|
1233
1234
|
function getTabsById(ids) {
|
|
1234
1235
|
return ids
|
|
@@ -53,9 +53,11 @@ export const agentMode = signal("ask"); // "ask" | "agent" | "plan" | "web" | "i
|
|
|
53
53
|
|
|
54
54
|
/** Available agents loaded from API */
|
|
55
55
|
export const availableAgents = signal([]); // Array<{ id, name, provider, available, busy, capabilities }>
|
|
56
|
+
export const manualAgents = signal([]);
|
|
56
57
|
|
|
57
58
|
/** Currently active agent adapter id */
|
|
58
59
|
export const activeAgent = signal("codex-sdk");
|
|
60
|
+
export const activeManualAgentId = signal("");
|
|
59
61
|
|
|
60
62
|
/** Whether agent data is currently loading */
|
|
61
63
|
export const agentSelectorLoading = signal(false);
|
|
@@ -71,6 +73,7 @@ try { if (typeof localStorage !== "undefined") yoloMode.value = localStorage.get
|
|
|
71
73
|
/** Selected model override — empty string means "default" */
|
|
72
74
|
export const selectedModel = signal("");
|
|
73
75
|
try { if (typeof localStorage !== "undefined") selectedModel.value = localStorage.getItem("ve-selected-model") || ""; } catch {}
|
|
76
|
+
try { if (typeof localStorage !== "undefined") activeManualAgentId.value = localStorage.getItem("ve-active-manual-agent") || ""; } catch {}
|
|
74
77
|
|
|
75
78
|
/** Computed: resolved active agent object */
|
|
76
79
|
export const activeAgentInfo = computed(() => {
|
|
@@ -91,6 +94,8 @@ const MODES = [
|
|
|
91
94
|
{ id: "instant", label: "Instant", icon: "zap", description: "Fast back-and-forth" },
|
|
92
95
|
];
|
|
93
96
|
|
|
97
|
+
const VALID_MANUAL_MODES = new Set(MODES.map((mode) => mode.id));
|
|
98
|
+
|
|
94
99
|
const AGENT_ICONS = {
|
|
95
100
|
"codex-sdk": "zap",
|
|
96
101
|
"copilot-sdk": "bot",
|
|
@@ -394,8 +399,10 @@ export async function loadAvailableAgents() {
|
|
|
394
399
|
try {
|
|
395
400
|
const res = await apiFetch("/api/agents/available", { _silent: true });
|
|
396
401
|
const agents = Array.isArray(res) ? res : (res?.agents || res?.data || []);
|
|
402
|
+
const nextManualAgents = Array.isArray(res?.manualAgents) ? res.manualAgents : [];
|
|
397
403
|
const reportedActive = String(res?.active || "").trim();
|
|
398
404
|
availableAgents.value = agents;
|
|
405
|
+
manualAgents.value = nextManualAgents;
|
|
399
406
|
// Prefer backend-reported active selection when present.
|
|
400
407
|
if (reportedActive && agents.some((a) => a.id === reportedActive)) {
|
|
401
408
|
activeAgent.value = reportedActive;
|
|
@@ -404,6 +411,10 @@ export async function loadAvailableAgents() {
|
|
|
404
411
|
const firstEnabled = agents.find((a) => a.available);
|
|
405
412
|
activeAgent.value = (firstEnabled || agents[0]).id;
|
|
406
413
|
}
|
|
414
|
+
if (activeManualAgentId.value && !nextManualAgents.some((agent) => agent.id === activeManualAgentId.value)) {
|
|
415
|
+
activeManualAgentId.value = "";
|
|
416
|
+
try { localStorage.setItem("ve-active-manual-agent", ""); } catch {}
|
|
417
|
+
}
|
|
407
418
|
} catch (err) {
|
|
408
419
|
console.warn("[agent-selector] Failed to load agents:", err);
|
|
409
420
|
// Provide sensible fallback agents for offline/dev mode
|
|
@@ -419,6 +430,12 @@ export async function loadAvailableAgents() {
|
|
|
419
430
|
}
|
|
420
431
|
}
|
|
421
432
|
|
|
433
|
+
function setActiveManualAgent(agentId) {
|
|
434
|
+
const nextId = String(agentId || "").trim();
|
|
435
|
+
activeManualAgentId.value = nextId;
|
|
436
|
+
try { localStorage.setItem("ve-active-manual-agent", nextId); } catch {}
|
|
437
|
+
}
|
|
438
|
+
|
|
422
439
|
/**
|
|
423
440
|
* Switch the active agent via API.
|
|
424
441
|
* @param {string} agentId
|
|
@@ -861,6 +878,115 @@ export function AgentPicker() {
|
|
|
861
878
|
`;
|
|
862
879
|
}
|
|
863
880
|
|
|
881
|
+
export function ManualAgentPicker() {
|
|
882
|
+
const profiles = manualAgents.value;
|
|
883
|
+
const current = activeManualAgentId.value;
|
|
884
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
885
|
+
const open = Boolean(anchorEl);
|
|
886
|
+
|
|
887
|
+
const grouped = profiles.reduce((acc, profile) => {
|
|
888
|
+
const key = String(profile?.sectionLabel || "Manual").trim() || "Manual";
|
|
889
|
+
if (!acc[key]) acc[key] = [];
|
|
890
|
+
acc[key].push(profile);
|
|
891
|
+
return acc;
|
|
892
|
+
}, {});
|
|
893
|
+
const currentProfile = profiles.find((profile) => profile.id === current) || null;
|
|
894
|
+
const currentLabel = currentProfile?.name || "Manual Agent";
|
|
895
|
+
|
|
896
|
+
const handleSelect = useCallback((profileId) => {
|
|
897
|
+
const nextId = String(profileId || "").trim();
|
|
898
|
+
const selected = profiles.find((profile) => profile.id === nextId) || null;
|
|
899
|
+
setActiveManualAgent(nextId);
|
|
900
|
+
if (selected?.interactiveMode && VALID_MANUAL_MODES.has(selected.interactiveMode)) {
|
|
901
|
+
setAgentMode(selected.interactiveMode);
|
|
902
|
+
}
|
|
903
|
+
if (selected?.model) {
|
|
904
|
+
selectedModel.value = selected.model;
|
|
905
|
+
try { localStorage.setItem("ve-selected-model", selected.model); } catch {}
|
|
906
|
+
}
|
|
907
|
+
haptic(selected ? "light" : "medium");
|
|
908
|
+
setAnchorEl(null);
|
|
909
|
+
}, [profiles]);
|
|
910
|
+
|
|
911
|
+
return html`
|
|
912
|
+
<${Tooltip} title="Select a library-backed manual chat agent" arrow>
|
|
913
|
+
<${Chip}
|
|
914
|
+
label=${currentLabel}
|
|
915
|
+
size="small"
|
|
916
|
+
variant=${currentProfile ? "filled" : "outlined"}
|
|
917
|
+
onClick=${(e) => setAnchorEl(e.currentTarget)}
|
|
918
|
+
icon=${html`<span style="font-size:13px;line-height:1">${resolveIcon("bot")}</span>`}
|
|
919
|
+
sx=${{
|
|
920
|
+
flexShrink: 0,
|
|
921
|
+
cursor: "pointer",
|
|
922
|
+
fontSize: 12,
|
|
923
|
+
fontWeight: 500,
|
|
924
|
+
color: "var(--tg-theme-text-color, #fff)",
|
|
925
|
+
borderColor: open ? "var(--tg-theme-button-color, #3b82f6)" : "rgba(255,255,255,0.08)",
|
|
926
|
+
bgcolor: currentProfile ? "rgba(59,130,246,0.12)" : "transparent",
|
|
927
|
+
"&:hover": { bgcolor: "rgba(255,255,255,0.06)" },
|
|
928
|
+
}}
|
|
929
|
+
/>
|
|
930
|
+
</${Tooltip}>
|
|
931
|
+
<${Menu}
|
|
932
|
+
anchorEl=${anchorEl}
|
|
933
|
+
open=${open}
|
|
934
|
+
onClose=${() => setAnchorEl(null)}
|
|
935
|
+
anchorOrigin=${{ vertical: "top", horizontal: "left" }}
|
|
936
|
+
transformOrigin=${{ vertical: "bottom", horizontal: "left" }}
|
|
937
|
+
slotProps=${{ paper: { sx: { ...muiDarkPaper, minWidth: 260 } } }}
|
|
938
|
+
>
|
|
939
|
+
<${MenuItem} selected=${!current} onClick=${() => handleSelect("")}>
|
|
940
|
+
<${ListItemIcon} sx=${{ minWidth: "28px !important" }}>
|
|
941
|
+
${!current ? html`<${Typography} sx=${{ color: "var(--tg-theme-button-color, #3b82f6)", fontWeight: 700, fontSize: 14 }}>✓</${Typography}>` : null}
|
|
942
|
+
</${ListItemIcon}>
|
|
943
|
+
<${ListItemText}
|
|
944
|
+
primary="No manual profile"
|
|
945
|
+
secondary="Use only the executor + mode selection"
|
|
946
|
+
primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
|
|
947
|
+
secondaryTypographyProps=${{ fontSize: 11 }}
|
|
948
|
+
/>
|
|
949
|
+
</${MenuItem}>
|
|
950
|
+
${profiles.length > 0 ? html`<${Divider} />` : null}
|
|
951
|
+
${Object.entries(grouped).map(([sectionLabel, items], sectionIndex) => html`
|
|
952
|
+
<div key=${sectionLabel}>
|
|
953
|
+
${sectionIndex > 0 ? html`<${Divider} />` : null}
|
|
954
|
+
<${MenuItem} disabled sx=${{ opacity: 0.7, fontSize: 11, textTransform: "uppercase", letterSpacing: 0.5 }}>
|
|
955
|
+
${sectionLabel}
|
|
956
|
+
</${MenuItem}>
|
|
957
|
+
${items.map((profile) => html`
|
|
958
|
+
<${MenuItem}
|
|
959
|
+
key=${profile.id}
|
|
960
|
+
selected=${profile.id === current}
|
|
961
|
+
onClick=${() => handleSelect(profile.id)}
|
|
962
|
+
>
|
|
963
|
+
<${ListItemIcon} sx=${{ minWidth: "28px !important" }}>
|
|
964
|
+
${profile.id === current ? html`<${Typography} sx=${{ color: "var(--tg-theme-button-color, #3b82f6)", fontWeight: 700, fontSize: 14 }}>✓</${Typography}>` : null}
|
|
965
|
+
</${ListItemIcon}>
|
|
966
|
+
<${ListItemText}
|
|
967
|
+
primary=${profile.name}
|
|
968
|
+
secondary=${profile.description || profile.interactiveMode || profile.agentCategory || "Manual agent"}
|
|
969
|
+
primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
|
|
970
|
+
secondaryTypographyProps=${{ fontSize: 11 }}
|
|
971
|
+
/>
|
|
972
|
+
</${MenuItem}>
|
|
973
|
+
`)}
|
|
974
|
+
</div>
|
|
975
|
+
`)}
|
|
976
|
+
${profiles.length === 0 ? html`
|
|
977
|
+
<${MenuItem} disabled>
|
|
978
|
+
<${ListItemText}
|
|
979
|
+
primary="No manual agents"
|
|
980
|
+
secondary="Mark an interactive agent as visible in chat from the Library tab."
|
|
981
|
+
primaryTypographyProps=${{ fontSize: 13, fontWeight: 500 }}
|
|
982
|
+
secondaryTypographyProps=${{ fontSize: 11 }}
|
|
983
|
+
/>
|
|
984
|
+
</${MenuItem}>
|
|
985
|
+
` : null}
|
|
986
|
+
</${Menu}>
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
|
|
864
990
|
/* ═══════════════════════════════════════════════
|
|
865
991
|
* AgentStatusBadge
|
|
866
992
|
* MUI Chip showing agent runtime state
|
|
@@ -1016,6 +1142,7 @@ export function ChatInputToolbar() {
|
|
|
1016
1142
|
<div class="chat-input-toolbar">
|
|
1017
1143
|
<${AgentPicker} />
|
|
1018
1144
|
<${AgentModeSelector} />
|
|
1145
|
+
<${ManualAgentPicker} />
|
|
1019
1146
|
<${ModelPicker} />
|
|
1020
1147
|
<${YoloToggle} />
|
|
1021
1148
|
<${Box} sx=${{ flex: 1, minWidth: 8 }} />
|
|
@@ -568,6 +568,8 @@ export async function resumeSession(id) {
|
|
|
568
568
|
const STATUS_COLOR_MAP = {
|
|
569
569
|
running: "var(--accent)",
|
|
570
570
|
active: "var(--accent)",
|
|
571
|
+
idle: "var(--color-warning)",
|
|
572
|
+
stalled: "var(--color-error)",
|
|
571
573
|
paused: "var(--text-hint)",
|
|
572
574
|
completed: "var(--color-done)",
|
|
573
575
|
done: "var(--color-done)",
|
package/ui/demo-defaults.js
CHANGED
|
@@ -970,7 +970,7 @@
|
|
|
970
970
|
"type": "action.run_command",
|
|
971
971
|
"label": "Fetch Bosun PR State",
|
|
972
972
|
"config": {
|
|
973
|
-
"command": "node -e \" const {execFileSync}=require('child_process'); const hours=Number('{{lookbackHours}}')||24; const repoScope=String('{{repoScope}}'||'auto').trim(); const since=new Date(Date.now()-hours*3600000).toISOString(); function ghJson(args){ try{const o=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return o?JSON.parse(o):[];} catch{return [];} } const
|
|
973
|
+
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const hours=Number('{{lookbackHours}}')||24; const repoScope=String('{{repoScope}}'||'auto').trim(); const since=new Date(Date.now()-hours*3600000).toISOString(); function ghJson(args){ try{const o=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return o?JSON.parse(o):[];} catch{return [];} } function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(repoScope&&repoScope!=='auto'&&repoScope!=='all'&&repoScope!=='current'){ return [...new Set(repoScope.split(',').map(v=>v.trim()).filter(Boolean))]; } if(repoScope==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } function extractTaskId(pr){ const src=String((pr.body||'')+'\\n'+(pr.title||'')); const m=src.match(/(?:Bosun-Task|VE-Task|Task-ID|task[_-]?id)[:\\s]+([a-zA-Z0-9_-]{4,64})/i); return m?m[1].trim():null; } const repoTargets=resolveRepoTargets(); const merged=[]; const open=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const mergedArgs=['pr','list','--state','merged','--label','bosun-attached','--json','number,title,body,headRefName,mergedAt,url','--limit','50']; const openArgs=['pr','list','--state','open','--label','bosun-attached','--json','number,title,body,headRefName,isDraft,url','--limit','50']; if(repo){ mergedArgs.push('--repo',repo); openArgs.push('--repo',repo); } for(const pr of ghJson(mergedArgs)){ merged.push({...pr,__repo:repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim()}); } for(const pr of ghJson(openArgs)){ open.push({...pr,__repo:repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim()}); } } const recentMerged=merged.filter(p=>!p.mergedAt||new Date(p.mergedAt)>=new Date(since)); console.log(JSON.stringify({ repoScope, reposScanned: repoTargets.length, merged:recentMerged.map(p=>({n:p.number,repo:p.__repo||'',title:p.title,branch:p.headRefName,taskId:extractTaskId(p)})), open:open.filter(p=>!p.isDraft).map(p=>({n:p.number,repo:p.__repo||'',title:p.title,branch:p.headRefName,taskId:extractTaskId(p)})), })); \"",
|
|
974
974
|
"continueOnError": true
|
|
975
975
|
},
|
|
976
976
|
"position": {
|
|
@@ -1001,7 +1001,7 @@
|
|
|
1001
1001
|
"type": "action.run_command",
|
|
1002
1002
|
"label": "Sync PR State → Kanban (Programmatic)",
|
|
1003
1003
|
"config": {
|
|
1004
|
-
"command": "node -e \" const {execFileSync}=require('child_process'); const fs=require('fs'); const raw=String(process.env.BOSUN_FETCH_PR_STATE||''); const data=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})(); const merged=Array.isArray(data.merged)?data.merged:[]; const open=Array.isArray(data.open)?data.open:[]; const updates=[]; const unresolved=[]; const taskCli=['task-cli.mjs','task/task-cli.mjs'].find(p=>fs.existsSync(p))||''; if(!
|
|
1004
|
+
"command": "node -e \" const {execFileSync}=require('child_process'); const fs=require('fs'); const raw=String(process.env.BOSUN_FETCH_PR_STATE||''); const data=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})(); const merged=Array.isArray(data.merged)?data.merged:[]; const open=Array.isArray(data.open)?data.open:[]; const updates=[]; const unresolved=[]; const maxBuffer=25*1024*1024; const cliPath=fs.existsSync('cli.mjs')?'cli.mjs':''; const taskCli=['task-cli.mjs','task/task-cli.mjs'].find(p=>fs.existsSync(p))||''; const taskRunner=cliPath?'cli':(taskCli?'task-cli':''); if(!taskRunner){ console.log(JSON.stringify({updated:0,unresolved:[{reason:'task_command_missing'}],needsAgent:true})); process.exit(0); } function runTask(args){const cmdArgs=taskRunner==='cli'?['cli.mjs','task',...args,'--config-dir','.bosun','--repo-root','.']:[taskCli,...args];return execFileSync('node',cmdArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer}).trim();} function parseJsonObject(raw){const txt=String(raw||'').trim();if(!txt)return null;try{return JSON.parse(txt);}catch{}const lines=txt.split(/\\r?\\n/);for(let start=0;start<lines.length;start++){const token=lines[start].trim();if(!(token==='['||token==='{'||token.startsWith('[{')||token.startsWith('{\"')||token.startsWith('[\"')))continue;const candidate=lines.slice(start).join('\\n').trim();try{return JSON.parse(candidate);}catch{}}const compact=lines.map(s=>s.trim()).filter(Boolean);for(let i=compact.length-1;i>=0;i--){const line=compact[i];if(!(line.startsWith('{')||line.startsWith('[')))continue;try{return JSON.parse(line);}catch{}}const start=txt.indexOf('{');const end=txt.lastIndexOf('}');if(start>=0&&end>start){try{return JSON.parse(txt.slice(start,end+1));}catch{}}return null;} let taskListCache=null; function normalizeRepo(value){return String(value||'').trim().toLowerCase();} function listTasks(){ if(Array.isArray(taskListCache)) return taskListCache; try{const raw=runTask(['list','--json']);const tasks=parseJsonObject(raw);taskListCache=Array.isArray(tasks)?tasks:[];return taskListCache;}catch{taskListCache=[];return taskListCache;} } function resolveTaskId(item){ const explicit=String(item?.taskId||'').trim(); if(explicit) return explicit; const branch=String(item?.branch||'').trim(); if(!branch) return ''; const repo=normalizeRepo(item?.repo); const matches=listTasks().filter((task)=>{ const taskBranch=String(task?.branchName||'').trim(); if(taskBranch!==branch) return false; const taskRepo=normalizeRepo(task?.repository||''); if(!repo || !taskRepo) return true; return taskRepo===repo; }); if(matches.length===1) return String(matches[0]?.id||'').trim(); const exactRepo=matches.find((task)=>normalizeRepo(task?.repository||'')===repo); return exactRepo?String(exactRepo?.id||'').trim():''; } function getTaskSnapshot(id){ try{const raw=runTask(['get',id,'--json']);const task=parseJsonObject(raw);return {status:task?.status||null,reviewStatus:task?.reviewStatus||null};}catch{return {status:null,reviewStatus:null};} } for(const item of merged){ const id=resolveTaskId(item); if(!id){unresolved.push({taskId:null,repo:String(item?.repo||''),branch:String(item?.branch||''),status:'done',reason:'task_lookup_failed'});continue;} try{runTask(['update',id,'--status','done']);updates.push({taskId:id,status:'done'});}catch(e){unresolved.push({taskId:id,status:'done',error:String(e?.message||e)});} } for(const item of open){ const id=resolveTaskId(item); if(!id){unresolved.push({taskId:null,repo:String(item?.repo||''),branch:String(item?.branch||''),status:'inreview',reason:'task_lookup_failed'});continue;} try{const snap=getTaskSnapshot(id);const current=String(snap?.status||'').trim().toLowerCase();const review=String(snap?.reviewStatus||'').toLowerCase();if(current==='inreview'||current==='done'){updates.push({taskId:id,status:current,skipped:true});continue;}runTask(['update',id,'--status','inreview']);updates.push({taskId:id,status:'inreview',fromStatus:current||null,reviewStatus:review||null});}catch(e){unresolved.push({taskId:id,status:'inreview',error:String(e?.message||e)});} } const actionableUnresolved=unresolved.filter((item)=>String(item?.taskId||'').trim()); console.log(JSON.stringify({updated:updates.length,updates,unresolved,needsAgent:actionableUnresolved.length>0})); \"",
|
|
1005
1005
|
"continueOnError": true,
|
|
1006
1006
|
"failOnError": false,
|
|
1007
1007
|
"env": {
|
|
@@ -1021,7 +1021,7 @@
|
|
|
1021
1021
|
"type": "condition.expression",
|
|
1022
1022
|
"label": "Needs Agent Sync?",
|
|
1023
1023
|
"config": {
|
|
1024
|
-
"expression": "(()=>{try{const raw=$ctx.getNodeOutput('sync-programmatic')?.output||'{}';const d=JSON.parse(raw);
|
|
1024
|
+
"expression": "(()=>{try{const raw=$ctx.getNodeOutput('sync-programmatic')?.output||'{}';const d=JSON.parse(raw);const actionable=Array.isArray(d?.unresolved)?d.unresolved.some((item)=>String(item?.taskId||'').trim()):false;return d?.needsAgent===true || actionable;}catch{return true;}})()"
|
|
1025
1025
|
},
|
|
1026
1026
|
"position": {
|
|
1027
1027
|
"x": 400,
|
|
@@ -21732,7 +21732,7 @@
|
|
|
21732
21732
|
"type": "action.run_command",
|
|
21733
21733
|
"label": "Fetch Bosun PR State",
|
|
21734
21734
|
"config": {
|
|
21735
|
-
"command": "node -e \" const {execFileSync}=require('child_process'); const hours=Number('{{lookbackHours}}')||24; const repoScope=String('{{repoScope}}'||'auto').trim(); const since=new Date(Date.now()-hours*3600000).toISOString(); function ghJson(args){ try{const o=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return o?JSON.parse(o):[];} catch{return [];} } const
|
|
21735
|
+
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const hours=Number('{{lookbackHours}}')||24; const repoScope=String('{{repoScope}}'||'auto').trim(); const since=new Date(Date.now()-hours*3600000).toISOString(); function ghJson(args){ try{const o=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return o?JSON.parse(o):[];} catch{return [];} } function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(repoScope&&repoScope!=='auto'&&repoScope!=='all'&&repoScope!=='current'){ return [...new Set(repoScope.split(',').map(v=>v.trim()).filter(Boolean))]; } if(repoScope==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } function extractTaskId(pr){ const src=String((pr.body||'')+'\\n'+(pr.title||'')); const m=src.match(/(?:Bosun-Task|VE-Task|Task-ID|task[_-]?id)[:\\s]+([a-zA-Z0-9_-]{4,64})/i); return m?m[1].trim():null; } const repoTargets=resolveRepoTargets(); const merged=[]; const open=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const mergedArgs=['pr','list','--state','merged','--label','bosun-attached','--json','number,title,body,headRefName,mergedAt,url','--limit','50']; const openArgs=['pr','list','--state','open','--label','bosun-attached','--json','number,title,body,headRefName,isDraft,url','--limit','50']; if(repo){ mergedArgs.push('--repo',repo); openArgs.push('--repo',repo); } for(const pr of ghJson(mergedArgs)){ merged.push({...pr,__repo:repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim()}); } for(const pr of ghJson(openArgs)){ open.push({...pr,__repo:repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim()}); } } const recentMerged=merged.filter(p=>!p.mergedAt||new Date(p.mergedAt)>=new Date(since)); console.log(JSON.stringify({ repoScope, reposScanned: repoTargets.length, merged:recentMerged.map(p=>({n:p.number,repo:p.__repo||'',title:p.title,branch:p.headRefName,taskId:extractTaskId(p)})), open:open.filter(p=>!p.isDraft).map(p=>({n:p.number,repo:p.__repo||'',title:p.title,branch:p.headRefName,taskId:extractTaskId(p)})), })); \"",
|
|
21736
21736
|
"continueOnError": true
|
|
21737
21737
|
},
|
|
21738
21738
|
"position": {
|
|
@@ -21763,7 +21763,7 @@
|
|
|
21763
21763
|
"type": "action.run_command",
|
|
21764
21764
|
"label": "Sync PR State → Kanban (Programmatic)",
|
|
21765
21765
|
"config": {
|
|
21766
|
-
"command": "node -e \" const {execFileSync}=require('child_process'); const fs=require('fs'); const raw=String(process.env.BOSUN_FETCH_PR_STATE||''); const data=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})(); const merged=Array.isArray(data.merged)?data.merged:[]; const open=Array.isArray(data.open)?data.open:[]; const updates=[]; const unresolved=[]; const taskCli=['task-cli.mjs','task/task-cli.mjs'].find(p=>fs.existsSync(p))||''; if(!
|
|
21766
|
+
"command": "node -e \" const {execFileSync}=require('child_process'); const fs=require('fs'); const raw=String(process.env.BOSUN_FETCH_PR_STATE||''); const data=(()=>{try{return JSON.parse(raw||'{}')}catch{return {}}})(); const merged=Array.isArray(data.merged)?data.merged:[]; const open=Array.isArray(data.open)?data.open:[]; const updates=[]; const unresolved=[]; const maxBuffer=25*1024*1024; const cliPath=fs.existsSync('cli.mjs')?'cli.mjs':''; const taskCli=['task-cli.mjs','task/task-cli.mjs'].find(p=>fs.existsSync(p))||''; const taskRunner=cliPath?'cli':(taskCli?'task-cli':''); if(!taskRunner){ console.log(JSON.stringify({updated:0,unresolved:[{reason:'task_command_missing'}],needsAgent:true})); process.exit(0); } function runTask(args){const cmdArgs=taskRunner==='cli'?['cli.mjs','task',...args,'--config-dir','.bosun','--repo-root','.']:[taskCli,...args];return execFileSync('node',cmdArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe'],maxBuffer}).trim();} function parseJsonObject(raw){const txt=String(raw||'').trim();if(!txt)return null;try{return JSON.parse(txt);}catch{}const lines=txt.split(/\\r?\\n/);for(let start=0;start<lines.length;start++){const token=lines[start].trim();if(!(token==='['||token==='{'||token.startsWith('[{')||token.startsWith('{\"')||token.startsWith('[\"')))continue;const candidate=lines.slice(start).join('\\n').trim();try{return JSON.parse(candidate);}catch{}}const compact=lines.map(s=>s.trim()).filter(Boolean);for(let i=compact.length-1;i>=0;i--){const line=compact[i];if(!(line.startsWith('{')||line.startsWith('[')))continue;try{return JSON.parse(line);}catch{}}const start=txt.indexOf('{');const end=txt.lastIndexOf('}');if(start>=0&&end>start){try{return JSON.parse(txt.slice(start,end+1));}catch{}}return null;} let taskListCache=null; function normalizeRepo(value){return String(value||'').trim().toLowerCase();} function listTasks(){ if(Array.isArray(taskListCache)) return taskListCache; try{const raw=runTask(['list','--json']);const tasks=parseJsonObject(raw);taskListCache=Array.isArray(tasks)?tasks:[];return taskListCache;}catch{taskListCache=[];return taskListCache;} } function resolveTaskId(item){ const explicit=String(item?.taskId||'').trim(); if(explicit) return explicit; const branch=String(item?.branch||'').trim(); if(!branch) return ''; const repo=normalizeRepo(item?.repo); const matches=listTasks().filter((task)=>{ const taskBranch=String(task?.branchName||'').trim(); if(taskBranch!==branch) return false; const taskRepo=normalizeRepo(task?.repository||''); if(!repo || !taskRepo) return true; return taskRepo===repo; }); if(matches.length===1) return String(matches[0]?.id||'').trim(); const exactRepo=matches.find((task)=>normalizeRepo(task?.repository||'')===repo); return exactRepo?String(exactRepo?.id||'').trim():''; } function getTaskSnapshot(id){ try{const raw=runTask(['get',id,'--json']);const task=parseJsonObject(raw);return {status:task?.status||null,reviewStatus:task?.reviewStatus||null};}catch{return {status:null,reviewStatus:null};} } for(const item of merged){ const id=resolveTaskId(item); if(!id){unresolved.push({taskId:null,repo:String(item?.repo||''),branch:String(item?.branch||''),status:'done',reason:'task_lookup_failed'});continue;} try{runTask(['update',id,'--status','done']);updates.push({taskId:id,status:'done'});}catch(e){unresolved.push({taskId:id,status:'done',error:String(e?.message||e)});} } for(const item of open){ const id=resolveTaskId(item); if(!id){unresolved.push({taskId:null,repo:String(item?.repo||''),branch:String(item?.branch||''),status:'inreview',reason:'task_lookup_failed'});continue;} try{const snap=getTaskSnapshot(id);const current=String(snap?.status||'').trim().toLowerCase();const review=String(snap?.reviewStatus||'').toLowerCase();if(current==='inreview'||current==='done'){updates.push({taskId:id,status:current,skipped:true});continue;}runTask(['update',id,'--status','inreview']);updates.push({taskId:id,status:'inreview',fromStatus:current||null,reviewStatus:review||null});}catch(e){unresolved.push({taskId:id,status:'inreview',error:String(e?.message||e)});} } const actionableUnresolved=unresolved.filter((item)=>String(item?.taskId||'').trim()); console.log(JSON.stringify({updated:updates.length,updates,unresolved,needsAgent:actionableUnresolved.length>0})); \"",
|
|
21767
21767
|
"continueOnError": true,
|
|
21768
21768
|
"failOnError": false,
|
|
21769
21769
|
"env": {
|
|
@@ -21783,7 +21783,7 @@
|
|
|
21783
21783
|
"type": "condition.expression",
|
|
21784
21784
|
"label": "Needs Agent Sync?",
|
|
21785
21785
|
"config": {
|
|
21786
|
-
"expression": "(()=>{try{const raw=$ctx.getNodeOutput('sync-programmatic')?.output||'{}';const d=JSON.parse(raw);
|
|
21786
|
+
"expression": "(()=>{try{const raw=$ctx.getNodeOutput('sync-programmatic')?.output||'{}';const d=JSON.parse(raw);const actionable=Array.isArray(d?.unresolved)?d.unresolved.some((item)=>String(item?.taskId||'').trim()):false;return d?.needsAgent===true || actionable;}catch{return true;}})()"
|
|
21787
21787
|
},
|
|
21788
21788
|
"position": {
|
|
21789
21789
|
"x": 400,
|
package/ui/modules/router.js
CHANGED
|
@@ -27,6 +27,7 @@ const ROUTE_TABS = new Set([
|
|
|
27
27
|
"infra",
|
|
28
28
|
"logs",
|
|
29
29
|
"library",
|
|
30
|
+
"marketplace",
|
|
30
31
|
"telemetry",
|
|
31
32
|
"settings",
|
|
32
33
|
]);
|
|
@@ -258,6 +259,7 @@ export const TAB_CONFIG = [
|
|
|
258
259
|
{ id: "infra", label: "Infra", icon: "server" },
|
|
259
260
|
{ id: "logs", label: "Logs", icon: "terminal" },
|
|
260
261
|
{ id: "library", label: "Library", icon: "book" },
|
|
262
|
+
{ id: "marketplace", label: "Market", icon: "box" },
|
|
261
263
|
{ id: "manual-flows", label: "Run Flows", icon: "zap" },
|
|
262
264
|
{ id: "telemetry", label: "Telemetry", icon: "chart" },
|
|
263
265
|
{ id: "benchmarks", label: "Bench", icon: "chart" },
|
package/ui/modules/state.js
CHANGED
|
@@ -88,6 +88,7 @@ export function isPlaceholderTaskDescription(value) {
|
|
|
88
88
|
if (!text) return false;
|
|
89
89
|
const normalized = text.toLowerCase();
|
|
90
90
|
return (
|
|
91
|
+
TASK_TEMPLATE_PLACEHOLDER_RE.test(text) ||
|
|
91
92
|
normalized === "internal server error" ||
|
|
92
93
|
normalized === "{\"ok\":false,\"error\":\"internal server error\"}" ||
|
|
93
94
|
normalized === "{\"error\":\"internal server error\"}"
|
|
@@ -227,6 +228,7 @@ export async function loadRetryQueue() {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
const TASK_IGNORE_LABEL = "codex:ignore";
|
|
231
|
+
const TASK_TEMPLATE_PLACEHOLDER_RE = /^\{\{\s*[\w.-]+\s*\}\}$/;
|
|
230
232
|
const TASK_TEXT_REPLACEMENTS = [
|
|
231
233
|
[/\u00D4\u00C7\u00F6/g, "-"],
|
|
232
234
|
[/\u00D4\u00C7\u00A3/g, "\""],
|
|
@@ -576,11 +578,16 @@ export async function loadTasks(options = {}) {
|
|
|
576
578
|
if (tasksSort.value) params.set("sort", tasksSort.value);
|
|
577
579
|
|
|
578
580
|
const res = await apiFetch(`/api/tasks?${params}`, { _silent: true }).catch(
|
|
579
|
-
() =>
|
|
580
|
-
data:
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
581
|
+
(err) => {
|
|
582
|
+
console.warn("[state] loadTasks fetch failed, keeping previous data:", err?.message || err);
|
|
583
|
+
return {
|
|
584
|
+
data: tasksData.value || [],
|
|
585
|
+
total: tasksTotal.value || 0,
|
|
586
|
+
totalPages: tasksTotalPages.value || 1,
|
|
587
|
+
statusCounts: tasksStatusCounts.value || {},
|
|
588
|
+
_fetchFailed: true,
|
|
589
|
+
};
|
|
590
|
+
},
|
|
584
591
|
);
|
|
585
592
|
const nextTasks = Array.isArray(res.data)
|
|
586
593
|
? res.data.map(normalizeTaskForUi)
|
|
@@ -1027,6 +1034,7 @@ const WS_CHANNEL_MAP = {
|
|
|
1027
1034
|
infra: ["worktrees", "workspaces", "presence"],
|
|
1028
1035
|
control: ["executor", "overview"],
|
|
1029
1036
|
logs: ["*"],
|
|
1037
|
+
marketplace: ["library"],
|
|
1030
1038
|
telemetry: ["*", "retry-queue"],
|
|
1031
1039
|
settings: ["overview"],
|
|
1032
1040
|
};
|