bosun 0.35.2 → 0.35.3
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 +14 -1
- package/agent-hooks.mjs +7 -1
- package/agent-pool.mjs +16 -0
- package/agent-prompts.mjs +190 -4
- package/agent-sdk.mjs +6 -1
- package/agent-work-analyzer.mjs +48 -9
- package/autofix.mjs +32 -18
- package/bosun.schema.json +1 -1
- package/kanban-adapter.mjs +62 -12
- package/monitor.mjs +25 -6
- package/opencode-shell.mjs +881 -0
- package/package.json +5 -2
- package/primary-agent.mjs +43 -0
- package/setup.mjs +33 -4
- package/task-executor.mjs +43 -14
- package/ui/app.js +10 -7
- package/ui/components/chat-view.js +31 -9
- package/ui/components/session-list.js +20 -4
- package/ui/modules/router.js +2 -0
- package/ui/tabs/agents.js +66 -8
- package/ui-server.mjs +142 -5
- package/workflow-engine.mjs +664 -10
- package/workflow-nodes.mjs +250 -1
- package/workflow-templates/github.mjs +389 -71
- package/workflow-templates/planning.mjs +31 -11
- package/workflow-templates.mjs +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"./compat": "./compat.mjs",
|
|
69
69
|
"./task-cli": "./task-cli.mjs",
|
|
70
70
|
"./github-auth-manager": "./github-auth-manager.mjs",
|
|
71
|
-
"./git-commit-helpers": "./git-commit-helpers.mjs"
|
|
71
|
+
"./git-commit-helpers": "./git-commit-helpers.mjs",
|
|
72
|
+
"./opencode-shell": "./opencode-shell.mjs"
|
|
72
73
|
},
|
|
73
74
|
"bin": {
|
|
74
75
|
"bosun": "cli.mjs",
|
|
@@ -160,6 +161,7 @@
|
|
|
160
161
|
"maintenance.mjs",
|
|
161
162
|
"merge-strategy.mjs",
|
|
162
163
|
"monitor.mjs",
|
|
164
|
+
"opencode-shell.mjs",
|
|
163
165
|
"postinstall.mjs",
|
|
164
166
|
"pr-cleanup-daemon.mjs",
|
|
165
167
|
"preflight.mjs",
|
|
@@ -240,6 +242,7 @@
|
|
|
240
242
|
"@anthropic-ai/claude-agent-sdk": "latest",
|
|
241
243
|
"@github/copilot-sdk": "latest",
|
|
242
244
|
"@openai/codex-sdk": "latest",
|
|
245
|
+
"@opencode-ai/sdk": "latest",
|
|
243
246
|
"@preact/signals": "1.3.1",
|
|
244
247
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
245
248
|
"ajv": "^8.18.0",
|
package/primary-agent.mjs
CHANGED
|
@@ -39,6 +39,18 @@ import {
|
|
|
39
39
|
resetClaudeSession,
|
|
40
40
|
initClaudeShell,
|
|
41
41
|
} from "./claude-shell.mjs";
|
|
42
|
+
import {
|
|
43
|
+
execOpencodePrompt,
|
|
44
|
+
steerOpencodePrompt,
|
|
45
|
+
isOpencodeBusy,
|
|
46
|
+
getSessionInfo as getOpencodeSessionInfo,
|
|
47
|
+
resetSession as resetOpencodeSession,
|
|
48
|
+
initOpencodeShell,
|
|
49
|
+
getActiveSessionId as getOpencodeSessionId,
|
|
50
|
+
listSessions as listOpencodeSessions,
|
|
51
|
+
switchSession as switchOpencodeSession,
|
|
52
|
+
createSession as createOpencodeSession,
|
|
53
|
+
} from "./opencode-shell.mjs";
|
|
42
54
|
import { getModelsForExecutor, normalizeExecutorKey } from "./task-complexity.mjs";
|
|
43
55
|
|
|
44
56
|
/** Valid agent interaction modes */
|
|
@@ -179,6 +191,34 @@ const ADAPTERS = {
|
|
|
179
191
|
return execClaudePrompt(fullCmd, {});
|
|
180
192
|
},
|
|
181
193
|
},
|
|
194
|
+
"opencode-sdk": {
|
|
195
|
+
name: "opencode-sdk",
|
|
196
|
+
provider: "OPENCODE",
|
|
197
|
+
displayName: "OpenCode",
|
|
198
|
+
exec: (msg, opts) => execOpencodePrompt(msg, { persistent: true, ...opts }),
|
|
199
|
+
steer: steerOpencodePrompt,
|
|
200
|
+
isBusy: isOpencodeBusy,
|
|
201
|
+
getInfo: () => getOpencodeSessionInfo(),
|
|
202
|
+
reset: resetOpencodeSession,
|
|
203
|
+
init: async () => {
|
|
204
|
+
await initOpencodeShell();
|
|
205
|
+
return true;
|
|
206
|
+
},
|
|
207
|
+
getSessionId: getOpencodeSessionId,
|
|
208
|
+
listSessions: listOpencodeSessions,
|
|
209
|
+
switchSession: switchOpencodeSession,
|
|
210
|
+
createSession: createOpencodeSession,
|
|
211
|
+
sdkCommands: ["/status", "/model", "/sessions", "/clear"],
|
|
212
|
+
execSdkCommand: async (command, args) => {
|
|
213
|
+
const cmd = command.startsWith("/") ? command : `/${command}`;
|
|
214
|
+
if (cmd === "/clear") {
|
|
215
|
+
await resetOpencodeSession();
|
|
216
|
+
return "Session cleared.";
|
|
217
|
+
}
|
|
218
|
+
const fullCmd = args ? `${cmd} ${args}` : cmd;
|
|
219
|
+
return execOpencodePrompt(fullCmd, { persistent: true });
|
|
220
|
+
},
|
|
221
|
+
},
|
|
182
222
|
};
|
|
183
223
|
|
|
184
224
|
function envFlagEnabled(value) {
|
|
@@ -296,6 +336,8 @@ function normalizePrimaryAgent(value) {
|
|
|
296
336
|
return "copilot-sdk";
|
|
297
337
|
if (["claude", "claude-sdk", "claude_code", "claude-code"].includes(raw))
|
|
298
338
|
return "claude-sdk";
|
|
339
|
+
if (["opencode", "opencode-sdk", "open-code"].includes(raw))
|
|
340
|
+
return "opencode-sdk";
|
|
299
341
|
return raw;
|
|
300
342
|
}
|
|
301
343
|
|
|
@@ -312,6 +354,7 @@ function executorToAdapter(executor) {
|
|
|
312
354
|
const key = normalizeExecutorKey(executor);
|
|
313
355
|
if (key === "copilot") return "copilot-sdk";
|
|
314
356
|
if (key === "claude") return "claude-sdk";
|
|
357
|
+
if (key === "opencode") return "opencode-sdk";
|
|
315
358
|
return "codex-sdk";
|
|
316
359
|
}
|
|
317
360
|
|
package/setup.mjs
CHANGED
|
@@ -1592,6 +1592,31 @@ const EXECUTOR_PRESETS = {
|
|
|
1592
1592
|
role: "primary",
|
|
1593
1593
|
},
|
|
1594
1594
|
],
|
|
1595
|
+
"opencode-only": [
|
|
1596
|
+
{
|
|
1597
|
+
name: "opencode-default",
|
|
1598
|
+
executor: "OPENCODE",
|
|
1599
|
+
variant: "DEFAULT",
|
|
1600
|
+
weight: 100,
|
|
1601
|
+
role: "primary",
|
|
1602
|
+
},
|
|
1603
|
+
],
|
|
1604
|
+
"opencode-codex": [
|
|
1605
|
+
{
|
|
1606
|
+
name: "opencode-default",
|
|
1607
|
+
executor: "OPENCODE",
|
|
1608
|
+
variant: "DEFAULT",
|
|
1609
|
+
weight: 60,
|
|
1610
|
+
role: "primary",
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
name: "codex-backup",
|
|
1614
|
+
executor: "CODEX",
|
|
1615
|
+
variant: "DEFAULT",
|
|
1616
|
+
weight: 40,
|
|
1617
|
+
role: "backup",
|
|
1618
|
+
},
|
|
1619
|
+
],
|
|
1595
1620
|
triple: [
|
|
1596
1621
|
{
|
|
1597
1622
|
name: "copilot-claude",
|
|
@@ -1958,7 +1983,7 @@ function normalizeSetupConfiguration({
|
|
|
1958
1983
|
{
|
|
1959
1984
|
const primaryExec = configJson.executors.find((e) => e.role === "primary");
|
|
1960
1985
|
if (primaryExec) {
|
|
1961
|
-
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk" };
|
|
1986
|
+
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk", OPENCODE: "opencode-sdk" };
|
|
1962
1987
|
env.PRIMARY_AGENT = env.PRIMARY_AGENT ||
|
|
1963
1988
|
sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
|
|
1964
1989
|
}
|
|
@@ -2684,6 +2709,8 @@ async function main() {
|
|
|
2684
2709
|
"Copilot + Codex (50/50 split)",
|
|
2685
2710
|
"Copilot only (Claude Opus 4.6)",
|
|
2686
2711
|
"Claude only (direct API)",
|
|
2712
|
+
"OpenCode only (local OpenCode server)",
|
|
2713
|
+
"OpenCode + Codex (60/40 split)",
|
|
2687
2714
|
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
2688
2715
|
"Custom — I'll define my own executors",
|
|
2689
2716
|
]
|
|
@@ -2692,6 +2719,8 @@ async function main() {
|
|
|
2692
2719
|
"Copilot + Codex (50/50 split)",
|
|
2693
2720
|
"Copilot only (Claude Opus 4.6)",
|
|
2694
2721
|
"Claude only (direct API)",
|
|
2722
|
+
"OpenCode only (local OpenCode server)",
|
|
2723
|
+
"OpenCode + Codex (60/40 split)",
|
|
2695
2724
|
"Triple (Copilot Claude 40%, Codex 35%, Copilot GPT 25%)",
|
|
2696
2725
|
];
|
|
2697
2726
|
|
|
@@ -2702,8 +2731,8 @@ async function main() {
|
|
|
2702
2731
|
);
|
|
2703
2732
|
|
|
2704
2733
|
const presetNames = isAdvancedSetup
|
|
2705
|
-
? ["codex-only", "copilot-codex", "copilot-only", "claude-only", "triple", "custom"]
|
|
2706
|
-
: ["codex-only", "copilot-codex", "copilot-only", "claude-only", "triple"];
|
|
2734
|
+
? ["codex-only", "copilot-codex", "copilot-only", "claude-only", "opencode-only", "opencode-codex", "triple", "custom"]
|
|
2735
|
+
: ["codex-only", "copilot-codex", "copilot-only", "claude-only", "opencode-only", "opencode-codex", "triple"];
|
|
2707
2736
|
const presetKey = presetNames[presetIdx] || "codex-only";
|
|
2708
2737
|
|
|
2709
2738
|
if (presetKey === "custom") {
|
|
@@ -5183,7 +5212,7 @@ async function runNonInteractive({
|
|
|
5183
5212
|
(e) => e.role === "primary",
|
|
5184
5213
|
);
|
|
5185
5214
|
if (primaryExec) {
|
|
5186
|
-
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk" };
|
|
5215
|
+
const sdkMap = { CODEX: "codex-sdk", COPILOT: "copilot-sdk", CLAUDE: "claude-sdk", OPENCODE: "opencode-sdk" };
|
|
5187
5216
|
env.PRIMARY_AGENT = sdkMap[String(primaryExec.executor).toUpperCase()] || "codex-sdk";
|
|
5188
5217
|
}
|
|
5189
5218
|
}
|
package/task-executor.mjs
CHANGED
|
@@ -1869,6 +1869,7 @@ async function commentOnIssue(task, commentBody) {
|
|
|
1869
1869
|
* @property {Function} onTaskFailed - callback(task, error)
|
|
1870
1870
|
* @property {Function} sendTelegram - optional telegram notifier function
|
|
1871
1871
|
* @property {Object} agentPrompts - optional prompt templates loaded from config
|
|
1872
|
+
* @property {boolean} workflowOwnsTaskLifecycle - Delegate finalization/recovery to workflow automation
|
|
1872
1873
|
*/
|
|
1873
1874
|
|
|
1874
1875
|
/**
|
|
@@ -1919,6 +1920,7 @@ class TaskExecutor {
|
|
|
1919
1920
|
onTaskFailed: null,
|
|
1920
1921
|
sendTelegram: null,
|
|
1921
1922
|
agentPrompts: {},
|
|
1923
|
+
workflowOwnsTaskLifecycle: false,
|
|
1922
1924
|
};
|
|
1923
1925
|
|
|
1924
1926
|
const merged = { ...defaults, ...options };
|
|
@@ -1954,6 +1956,7 @@ class TaskExecutor {
|
|
|
1954
1956
|
this.onTaskCompleted = merged.onTaskCompleted;
|
|
1955
1957
|
this.onTaskFailed = merged.onTaskFailed;
|
|
1956
1958
|
this.sendTelegram = merged.sendTelegram;
|
|
1959
|
+
this.workflowOwnsTaskLifecycle = merged.workflowOwnsTaskLifecycle === true;
|
|
1957
1960
|
this._agentPrompts =
|
|
1958
1961
|
merged.agentPrompts && typeof merged.agentPrompts === "object"
|
|
1959
1962
|
? merged.agentPrompts
|
|
@@ -5288,6 +5291,26 @@ class TaskExecutor {
|
|
|
5288
5291
|
output: (result.output || "").slice(0, 500),
|
|
5289
5292
|
}).catch(() => {});
|
|
5290
5293
|
|
|
5294
|
+
if (result.success && this.workflowOwnsTaskLifecycle) {
|
|
5295
|
+
result.finalized = true;
|
|
5296
|
+
result.finalizationReason = null;
|
|
5297
|
+
if (!result.worktreePath) result.worktreePath = worktreePath || null;
|
|
5298
|
+
if (!result.branch) {
|
|
5299
|
+
result.branch =
|
|
5300
|
+
task.branchName ||
|
|
5301
|
+
task.meta?.branch_name ||
|
|
5302
|
+
null;
|
|
5303
|
+
}
|
|
5304
|
+
if (!result.baseBranch) {
|
|
5305
|
+
result.baseBranch = baseBranch || null;
|
|
5306
|
+
}
|
|
5307
|
+
console.log(
|
|
5308
|
+
`${tag} workflow-owned lifecycle active — delegating finalization/review handoff to workflows`,
|
|
5309
|
+
);
|
|
5310
|
+
this.onTaskCompleted?.(task, result);
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5291
5314
|
if (result.success) {
|
|
5292
5315
|
console.log(
|
|
5293
5316
|
`${tag} completed successfully (${result.attempts} attempt(s))`,
|
|
@@ -5729,21 +5752,27 @@ class TaskExecutor {
|
|
|
5729
5752
|
this.pauseFor(5 * 60 * 1000, "rate-limit");
|
|
5730
5753
|
}
|
|
5731
5754
|
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5755
|
+
if (!this.workflowOwnsTaskLifecycle) {
|
|
5756
|
+
try {
|
|
5757
|
+
await transitionTaskStatus(task.id, "todo", {
|
|
5758
|
+
source: "task-executor-failed",
|
|
5759
|
+
taskTitle,
|
|
5760
|
+
branch: result?.branch || task?.branchName || null,
|
|
5761
|
+
baseBranch: result?.baseBranch || baseBranch || null,
|
|
5762
|
+
worktreePath,
|
|
5763
|
+
error: result?.error || null,
|
|
5764
|
+
});
|
|
5765
|
+
} catch {
|
|
5766
|
+
/* best-effort */
|
|
5767
|
+
}
|
|
5768
|
+
this.sendTelegram?.(
|
|
5769
|
+
`❌ Task failed: "${task.title}" — ${(result.error || "").slice(0, 200)}`,
|
|
5770
|
+
);
|
|
5771
|
+
} else {
|
|
5772
|
+
console.log(
|
|
5773
|
+
`${tag} workflow-owned lifecycle active — delegating failure recovery to workflows`,
|
|
5774
|
+
);
|
|
5743
5775
|
}
|
|
5744
|
-
this.sendTelegram?.(
|
|
5745
|
-
`❌ Task failed: "${task.title}" — ${(result.error || "").slice(0, 200)}`,
|
|
5746
|
-
);
|
|
5747
5776
|
this.onTaskFailed?.(task, result);
|
|
5748
5777
|
}
|
|
5749
5778
|
}
|
package/ui/app.js
CHANGED
|
@@ -122,7 +122,7 @@ import {
|
|
|
122
122
|
import { DashboardTab } from "./tabs/dashboard.js";
|
|
123
123
|
import { TasksTab } from "./tabs/tasks.js";
|
|
124
124
|
import { ChatTab } from "./tabs/chat.js";
|
|
125
|
-
import { AgentsTab } from "./tabs/agents.js";
|
|
125
|
+
import { AgentsTab, FleetSessionsTab } from "./tabs/agents.js";
|
|
126
126
|
import { InfraTab } from "./tabs/infra.js";
|
|
127
127
|
import { ControlTab } from "./tabs/control.js";
|
|
128
128
|
import { LogsTab } from "./tabs/logs.js";
|
|
@@ -431,6 +431,7 @@ const TAB_COMPONENTS = {
|
|
|
431
431
|
tasks: TasksTab,
|
|
432
432
|
chat: ChatTab,
|
|
433
433
|
agents: AgentsTab,
|
|
434
|
+
"fleet-sessions": FleetSessionsTab,
|
|
434
435
|
infra: InfraTab,
|
|
435
436
|
control: ControlTab,
|
|
436
437
|
logs: LogsTab,
|
|
@@ -560,6 +561,7 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
560
561
|
${TAB_CONFIG.map((tab) => {
|
|
561
562
|
const isActive = activeTab.value === tab.id;
|
|
562
563
|
const isHome = tab.id === "dashboard";
|
|
564
|
+
const isChild = !!tab.parent;
|
|
563
565
|
let badge = 0;
|
|
564
566
|
if (tab.id === "tasks") {
|
|
565
567
|
badge = getActiveTaskCount();
|
|
@@ -569,8 +571,8 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
569
571
|
return html`
|
|
570
572
|
<button
|
|
571
573
|
key=${tab.id}
|
|
572
|
-
class="sidebar-nav-item ${isActive ? "active" : ""}"
|
|
573
|
-
style
|
|
574
|
+
class="sidebar-nav-item ${isActive ? "active" : ""} ${isChild ? "sidebar-nav-child" : ""}"
|
|
575
|
+
style=${`position:relative${isChild ? ";padding-left:28px;font-size:0.85em" : ""}`}
|
|
574
576
|
aria-label=${tab.label}
|
|
575
577
|
aria-current=${isActive ? "page" : null}
|
|
576
578
|
title=${collapsed ? tab.label : undefined}
|
|
@@ -1582,7 +1584,7 @@ function App() {
|
|
|
1582
1584
|
useEffect(() => {
|
|
1583
1585
|
const el = mainRef.current;
|
|
1584
1586
|
if (!el) return;
|
|
1585
|
-
const swipeTabs = TAB_CONFIG.filter((t) => t.id !== "settings");
|
|
1587
|
+
const swipeTabs = TAB_CONFIG.filter((t) => t.id !== "settings" && !t.parent);
|
|
1586
1588
|
let startX = 0;
|
|
1587
1589
|
let startY = 0;
|
|
1588
1590
|
let startTime = 0;
|
|
@@ -1643,11 +1645,12 @@ function App() {
|
|
|
1643
1645
|
}, []);
|
|
1644
1646
|
|
|
1645
1647
|
const CurrentTab = TAB_COMPONENTS[activeTab.value] || DashboardTab;
|
|
1646
|
-
const isChatOrAgents = activeTab.value === "chat" || activeTab.value === "agents";
|
|
1647
|
-
const
|
|
1648
|
+
const isChatOrAgents = activeTab.value === "chat" || activeTab.value === "agents" || activeTab.value === "fleet-sessions";
|
|
1649
|
+
const isChat = activeTab.value === "chat";
|
|
1650
|
+
const showSessionRail = isDesktop && isChat;
|
|
1648
1651
|
const showInspector = isDesktop && isChatOrAgents;
|
|
1649
1652
|
const showBottomNav = !isDesktop;
|
|
1650
|
-
const railSessionType =
|
|
1653
|
+
const railSessionType = "primary";
|
|
1651
1654
|
const showDrawerToggles = isTablet;
|
|
1652
1655
|
const showInspectorToggle = isTablet && isChatOrAgents;
|
|
1653
1656
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
loadSessionMessages,
|
|
17
17
|
loadSessions,
|
|
18
18
|
sessionsData,
|
|
19
|
+
sessionPagination,
|
|
19
20
|
} from "./session-list.js";
|
|
20
21
|
import {
|
|
21
22
|
pendingMessages,
|
|
@@ -444,11 +445,12 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
444
445
|
const [paused, setPaused] = useState(false);
|
|
445
446
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
446
447
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
447
|
-
const [visibleCount, setVisibleCount] = useState(
|
|
448
|
+
const [visibleCount, setVisibleCount] = useState(20);
|
|
448
449
|
const [showStreamMeta, setShowStreamMeta] = useState(false);
|
|
449
450
|
const [pendingAttachments, setPendingAttachments] = useState([]);
|
|
450
451
|
const [uploadingAttachments, setUploadingAttachments] = useState(false);
|
|
451
452
|
const [dragActive, setDragActive] = useState(false);
|
|
453
|
+
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
452
454
|
const [filters, setFilters] = useState({
|
|
453
455
|
tool: false,
|
|
454
456
|
result: false,
|
|
@@ -611,10 +613,23 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
611
613
|
const refreshMessages = useCallback(async () => {
|
|
612
614
|
if (!sessionId) return;
|
|
613
615
|
setLoading(true);
|
|
614
|
-
const res = await loadSessionMessages(sessionId).finally(() => setLoading(false));
|
|
616
|
+
const res = await loadSessionMessages(sessionId, { limit: 20 }).finally(() => setLoading(false));
|
|
615
617
|
setLoadError(res?.ok ? null : res?.error || "unavailable");
|
|
616
618
|
}, [sessionId]);
|
|
617
619
|
|
|
620
|
+
/** Load older messages (triggered by scroll-to-top or "Load older" button) */
|
|
621
|
+
const loadOlderMessages = useCallback(async () => {
|
|
622
|
+
if (!sessionId || loadingOlder) return;
|
|
623
|
+
const pag = sessionPagination.value;
|
|
624
|
+
if (!pag || !pag.hasMore) return;
|
|
625
|
+
setLoadingOlder(true);
|
|
626
|
+
const newOffset = Math.max(0, pag.offset - 20);
|
|
627
|
+
const limit = pag.offset - newOffset;
|
|
628
|
+
if (limit <= 0) { setLoadingOlder(false); return; }
|
|
629
|
+
await loadSessionMessages(sessionId, { limit, offset: newOffset, prepend: true })
|
|
630
|
+
.finally(() => setLoadingOlder(false));
|
|
631
|
+
}, [sessionId, loadingOlder]);
|
|
632
|
+
|
|
618
633
|
/* Load messages on mount; WS push via initSessionWsListener handles real-time */
|
|
619
634
|
useEffect(() => {
|
|
620
635
|
if (!sessionId) return;
|
|
@@ -624,7 +639,7 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
624
639
|
if (!active) return;
|
|
625
640
|
if (!paused) {
|
|
626
641
|
setLoading(true);
|
|
627
|
-
loadSessionMessages(sessionId).then((res) => {
|
|
642
|
+
loadSessionMessages(sessionId, { limit: 20 }).then((res) => {
|
|
628
643
|
if (active) setLoadError(res?.ok ? null : res?.error || "unavailable");
|
|
629
644
|
}).finally(() => {
|
|
630
645
|
if (active) setLoading(false);
|
|
@@ -637,7 +652,7 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
637
652
|
// Fallback: poll slowly as safety net (30s) - WS does the heavy lifting
|
|
638
653
|
const interval = setInterval(() => {
|
|
639
654
|
if (active && !paused) {
|
|
640
|
-
loadSessionMessages(sessionId).then((res) => {
|
|
655
|
+
loadSessionMessages(sessionId, { limit: 20 }).then((res) => {
|
|
641
656
|
if (active && res?.ok === false) setLoadError(res?.error || "unavailable");
|
|
642
657
|
});
|
|
643
658
|
}
|
|
@@ -673,7 +688,7 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
673
688
|
|
|
674
689
|
/* Reset visible window when session or filters change */
|
|
675
690
|
useEffect(() => {
|
|
676
|
-
setVisibleCount(
|
|
691
|
+
setVisibleCount(20);
|
|
677
692
|
setUnreadCount(0);
|
|
678
693
|
setAutoScroll(true);
|
|
679
694
|
}, [sessionId, filterKey]);
|
|
@@ -1076,16 +1091,23 @@ export function ChatView({ sessionId, readOnly = false, embedded = false }) {
|
|
|
1076
1091
|
`}
|
|
1077
1092
|
|
|
1078
1093
|
<div class="chat-messages" ref=${messagesRef}>
|
|
1079
|
-
${hasMoreMessages && html`
|
|
1094
|
+
${(hasMoreMessages || sessionPagination.value?.hasMore) && html`
|
|
1080
1095
|
<div class="chat-load-earlier">
|
|
1081
1096
|
<button
|
|
1082
1097
|
class="btn btn-ghost btn-sm"
|
|
1083
|
-
|
|
1098
|
+
disabled=${loadingOlder}
|
|
1099
|
+
onClick=${() => {
|
|
1100
|
+
if (hasMoreMessages) {
|
|
1101
|
+
setVisibleCount((prev) => prev + 20);
|
|
1102
|
+
} else if (sessionPagination.value?.hasMore) {
|
|
1103
|
+
loadOlderMessages();
|
|
1104
|
+
}
|
|
1105
|
+
}}
|
|
1084
1106
|
>
|
|
1085
|
-
Load
|
|
1107
|
+
${loadingOlder ? "Loading…" : "Load older messages"}
|
|
1086
1108
|
</button>
|
|
1087
1109
|
<span class="chat-load-count">
|
|
1088
|
-
Showing ${visibleMessages.length} of ${filteredMessages.length}
|
|
1110
|
+
Showing ${visibleMessages.length} of ${sessionPagination.value?.total || filteredMessages.length}
|
|
1089
1111
|
</span>
|
|
1090
1112
|
</div>
|
|
1091
1113
|
`}
|
|
@@ -17,6 +17,8 @@ export const sessionsData = signal([]);
|
|
|
17
17
|
export const selectedSessionId = signal(null);
|
|
18
18
|
export const sessionMessages = signal([]);
|
|
19
19
|
export const sessionsError = signal(null);
|
|
20
|
+
/** Pagination metadata from the last loadSessionMessages call */
|
|
21
|
+
export const sessionPagination = signal(null);
|
|
20
22
|
|
|
21
23
|
let _wsListenerReady = false;
|
|
22
24
|
|
|
@@ -42,20 +44,34 @@ export async function loadSessions(filter = {}) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
export async function loadSessionMessages(id) {
|
|
47
|
+
export async function loadSessionMessages(id, opts = {}) {
|
|
46
48
|
try {
|
|
47
|
-
|
|
49
|
+
let url = sessionPath(id);
|
|
48
50
|
if (!url) return { ok: false, error: "invalid" };
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (opts.limit) params.set("limit", String(opts.limit));
|
|
53
|
+
if (opts.offset != null) params.set("offset", String(opts.offset));
|
|
54
|
+
const qs = params.toString();
|
|
55
|
+
if (qs) url += `?${qs}`;
|
|
49
56
|
const res = await apiFetch(url, { _silent: true });
|
|
50
57
|
if (res?.session) {
|
|
51
58
|
const normalized = dedupeMessages(res.session.messages || []);
|
|
52
|
-
sessionMessages.value
|
|
53
|
-
|
|
59
|
+
if (opts.prepend && sessionMessages.value?.length) {
|
|
60
|
+
// Prepend older messages (loading history on scroll up)
|
|
61
|
+
const merged = dedupeMessages([...normalized, ...sessionMessages.value]);
|
|
62
|
+
sessionMessages.value = merged;
|
|
63
|
+
} else {
|
|
64
|
+
sessionMessages.value = normalized;
|
|
65
|
+
}
|
|
66
|
+
sessionPagination.value = res.pagination || null;
|
|
67
|
+
return { ok: true, messages: normalized, pagination: res.pagination || null };
|
|
54
68
|
}
|
|
55
69
|
sessionMessages.value = [];
|
|
70
|
+
sessionPagination.value = null;
|
|
56
71
|
return { ok: false, error: "empty" };
|
|
57
72
|
} catch {
|
|
58
73
|
sessionMessages.value = [];
|
|
74
|
+
sessionPagination.value = null;
|
|
59
75
|
return { ok: false, error: "unavailable" };
|
|
60
76
|
}
|
|
61
77
|
}
|
package/ui/modules/router.js
CHANGED
|
@@ -20,6 +20,7 @@ const ROUTE_TABS = new Set([
|
|
|
20
20
|
"chat",
|
|
21
21
|
"workflows",
|
|
22
22
|
"agents",
|
|
23
|
+
"fleet-sessions",
|
|
23
24
|
"control",
|
|
24
25
|
"infra",
|
|
25
26
|
"logs",
|
|
@@ -232,6 +233,7 @@ export const TAB_CONFIG = [
|
|
|
232
233
|
{ id: "chat", label: "Chat", icon: "chat" },
|
|
233
234
|
{ id: "workflows", label: "Flows", icon: "workflow" },
|
|
234
235
|
{ id: "agents", label: "Fleet", icon: "cpu" },
|
|
236
|
+
{ id: "fleet-sessions", label: "Sessions", icon: "chat", parent: "agents" },
|
|
235
237
|
{ id: "control", label: "Control", icon: "sliders" },
|
|
236
238
|
{ id: "infra", label: "Infra", icon: "server" },
|
|
237
239
|
{ id: "logs", label: "Logs", icon: "terminal" },
|
package/ui/tabs/agents.js
CHANGED
|
@@ -1503,14 +1503,7 @@ export function AgentsTab() {
|
|
|
1503
1503
|
</div>
|
|
1504
1504
|
|
|
1505
1505
|
<div class="fleet-span">
|
|
1506
|
-
|
|
1507
|
-
slots=${slots}
|
|
1508
|
-
onOpenWorkspace=${openWorkspace}
|
|
1509
|
-
onForceStop=${handleForceStop}
|
|
1510
|
-
/>
|
|
1511
|
-
</div>
|
|
1512
|
-
|
|
1513
|
-
${agents.length > 0 &&
|
|
1506
|
+
${agents.length > 0 &&
|
|
1514
1507
|
html`
|
|
1515
1508
|
<div class="fleet-span">
|
|
1516
1509
|
<${Collapsible} title="Agent Threads" defaultOpen=${false}>
|
|
@@ -1971,3 +1964,68 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1971
1964
|
<//>
|
|
1972
1965
|
`;
|
|
1973
1966
|
}
|
|
1967
|
+
|
|
1968
|
+
/* ─── Fleet Sessions Tab (standalone) ─── */
|
|
1969
|
+
export function FleetSessionsTab() {
|
|
1970
|
+
const executor = executorData.value;
|
|
1971
|
+
const execData = executor?.data;
|
|
1972
|
+
const slots = execData?.slots || [];
|
|
1973
|
+
|
|
1974
|
+
useEffect(() => {
|
|
1975
|
+
let active = true;
|
|
1976
|
+
const refreshTaskSessions = () => {
|
|
1977
|
+
if (!active) return;
|
|
1978
|
+
loadSessions({ type: "task" });
|
|
1979
|
+
};
|
|
1980
|
+
refreshTaskSessions();
|
|
1981
|
+
const interval = setInterval(refreshTaskSessions, 5000);
|
|
1982
|
+
return () => {
|
|
1983
|
+
active = false;
|
|
1984
|
+
clearInterval(interval);
|
|
1985
|
+
};
|
|
1986
|
+
}, []);
|
|
1987
|
+
|
|
1988
|
+
/* Force stop a specific agent slot */
|
|
1989
|
+
const handleForceStop = async (slot) => {
|
|
1990
|
+
const ok = await showConfirm(
|
|
1991
|
+
`Force-stop agent working on "${truncate(slot.taskTitle || slot.taskId || "task", 40)}"?`,
|
|
1992
|
+
);
|
|
1993
|
+
if (!ok) return;
|
|
1994
|
+
haptic("heavy");
|
|
1995
|
+
try {
|
|
1996
|
+
await apiFetch("/api/executor/stop-slot", {
|
|
1997
|
+
method: "POST",
|
|
1998
|
+
body: JSON.stringify({ slotIndex: slot.index, taskId: slot.taskId }),
|
|
1999
|
+
});
|
|
2000
|
+
showToast("Stop signal sent", "success");
|
|
2001
|
+
scheduleRefresh(200);
|
|
2002
|
+
} catch {
|
|
2003
|
+
/* toast via apiFetch */
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
/* Open workspace viewer for an agent */
|
|
2008
|
+
const [selectedAgent, setSelectedAgent] = useState(null);
|
|
2009
|
+
const openWorkspace = (slot, i) => {
|
|
2010
|
+
haptic();
|
|
2011
|
+
setSelectedAgent({ ...slot, index: i });
|
|
2012
|
+
};
|
|
2013
|
+
|
|
2014
|
+
return html`
|
|
2015
|
+
<div class="fleet-layout">
|
|
2016
|
+
<div class="fleet-span">
|
|
2017
|
+
<${FleetSessionsPanel}
|
|
2018
|
+
slots=${slots}
|
|
2019
|
+
onOpenWorkspace=${openWorkspace}
|
|
2020
|
+
onForceStop=${handleForceStop}
|
|
2021
|
+
/>
|
|
2022
|
+
</div>
|
|
2023
|
+
</div>
|
|
2024
|
+
${selectedAgent && html`
|
|
2025
|
+
<${WorkspaceViewer}
|
|
2026
|
+
agent=${selectedAgent}
|
|
2027
|
+
onClose=${() => setSelectedAgent(null)}
|
|
2028
|
+
/>
|
|
2029
|
+
`}
|
|
2030
|
+
`;
|
|
2031
|
+
}
|