bosun 0.36.0 → 0.36.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 +98 -16
- package/README.md +27 -0
- package/agent-event-bus.mjs +5 -5
- package/agent-pool.mjs +129 -12
- package/agent-prompts.mjs +7 -1
- package/agent-sdk.mjs +13 -2
- package/agent-supervisor.mjs +2 -2
- package/agent-work-report.mjs +1 -1
- package/anomaly-detector.mjs +6 -6
- package/autofix.mjs +15 -15
- package/bosun-skills.mjs +4 -4
- package/bosun.schema.json +160 -4
- package/claude-shell.mjs +11 -11
- package/cli.mjs +21 -21
- package/codex-config.mjs +19 -19
- package/codex-shell.mjs +180 -29
- package/config-doctor.mjs +27 -2
- package/config.mjs +60 -7
- package/copilot-shell.mjs +4 -4
- package/error-detector.mjs +1 -1
- package/fleet-coordinator.mjs +2 -2
- package/gemini-shell.mjs +692 -0
- package/github-oauth-portal.mjs +1 -1
- package/github-reconciler.mjs +2 -2
- package/kanban-adapter.mjs +741 -168
- package/merge-strategy.mjs +25 -25
- package/monitor.mjs +123 -105
- package/opencode-shell.mjs +22 -22
- package/package.json +7 -1
- package/postinstall.mjs +22 -22
- package/pr-cleanup-daemon.mjs +6 -6
- package/prepublish-check.mjs +4 -4
- package/presence.mjs +2 -2
- package/primary-agent.mjs +85 -7
- package/publish.mjs +1 -1
- package/review-agent.mjs +1 -1
- package/session-tracker.mjs +11 -0
- package/setup-web-server.mjs +429 -21
- package/setup.mjs +367 -12
- package/shared-knowledge.mjs +1 -1
- package/startup-service.mjs +9 -9
- package/stream-resilience.mjs +58 -4
- package/sync-engine.mjs +2 -2
- package/task-assessment.mjs +9 -9
- package/task-cli.mjs +1 -1
- package/task-complexity.mjs +71 -2
- package/task-context.mjs +1 -2
- package/task-executor.mjs +104 -41
- package/telegram-bot.mjs +825 -494
- package/telegram-sentinel.mjs +28 -28
- package/ui/app.js +256 -23
- package/ui/app.monolith.js +1 -1
- package/ui/components/agent-selector.js +4 -3
- package/ui/components/chat-view.js +101 -28
- package/ui/components/diff-viewer.js +3 -3
- package/ui/components/kanban-board.js +3 -3
- package/ui/components/session-list.js +255 -35
- package/ui/components/workspace-switcher.js +3 -3
- package/ui/demo.html +209 -194
- package/ui/index.html +3 -3
- package/ui/modules/icon-utils.js +206 -142
- package/ui/modules/icons.js +2 -27
- package/ui/modules/settings-schema.js +29 -5
- package/ui/modules/streaming.js +30 -2
- package/ui/modules/vision-stream.js +275 -0
- package/ui/modules/voice-client.js +102 -9
- package/ui/modules/voice-fallback.js +62 -6
- package/ui/modules/voice-overlay.js +594 -59
- package/ui/modules/voice.js +31 -38
- package/ui/setup.html +284 -34
- package/ui/styles/components.css +47 -0
- package/ui/styles/sessions.css +75 -0
- package/ui/tabs/agents.js +73 -43
- package/ui/tabs/chat.js +37 -40
- package/ui/tabs/control.js +2 -2
- package/ui/tabs/dashboard.js +1 -1
- package/ui/tabs/infra.js +10 -10
- package/ui/tabs/library.js +8 -8
- package/ui/tabs/logs.js +10 -10
- package/ui/tabs/settings.js +20 -20
- package/ui/tabs/tasks.js +76 -47
- package/ui-server.mjs +1761 -124
- package/update-check.mjs +13 -13
- package/ve-kanban.mjs +1 -1
- package/whatsapp-channel.mjs +5 -5
- package/workflow-engine.mjs +20 -1
- package/workflow-nodes.mjs +904 -4
- package/workflow-templates/agents.mjs +321 -7
- package/workflow-templates/ci-cd.mjs +6 -6
- package/workflow-templates/github.mjs +156 -84
- package/workflow-templates/planning.mjs +8 -8
- package/workflow-templates/reliability.mjs +8 -8
- package/workflow-templates/security.mjs +3 -3
- package/workflow-templates.mjs +15 -9
- package/workspace-manager.mjs +85 -1
- package/workspace-monitor.mjs +2 -2
- package/workspace-registry.mjs +2 -2
- package/worktree-manager.mjs +1 -1
package/ui/styles/components.css
CHANGED
|
@@ -710,6 +710,53 @@ select.input {
|
|
|
710
710
|
line-height: 1.5;
|
|
711
711
|
}
|
|
712
712
|
|
|
713
|
+
.repo-select-group {
|
|
714
|
+
display: flex;
|
|
715
|
+
flex-direction: column;
|
|
716
|
+
gap: 6px;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.repo-auto-label {
|
|
720
|
+
font-size: 13px;
|
|
721
|
+
color: var(--text-secondary, #aaa);
|
|
722
|
+
padding: 4px 2px;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.repo-auto-label strong {
|
|
726
|
+
color: var(--text-primary, #fff);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.repo-checkboxes {
|
|
730
|
+
display: flex;
|
|
731
|
+
flex-direction: column;
|
|
732
|
+
gap: 4px;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.repo-checkboxes-label {
|
|
736
|
+
font-size: 12px;
|
|
737
|
+
text-transform: uppercase;
|
|
738
|
+
letter-spacing: 0.05em;
|
|
739
|
+
color: var(--text-hint, #888);
|
|
740
|
+
margin-bottom: 2px;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.repo-checkbox-item {
|
|
744
|
+
display: flex;
|
|
745
|
+
align-items: center;
|
|
746
|
+
gap: 8px;
|
|
747
|
+
font-size: 13px;
|
|
748
|
+
color: var(--text-primary, #fff);
|
|
749
|
+
cursor: pointer;
|
|
750
|
+
padding: 3px 2px;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.repo-checkbox-item input[type="checkbox"] {
|
|
754
|
+
accent-color: var(--accent, #7c6af7);
|
|
755
|
+
width: 15px;
|
|
756
|
+
height: 15px;
|
|
757
|
+
flex-shrink: 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
713
760
|
.create-task-advanced-toggle {
|
|
714
761
|
font-size: 12px;
|
|
715
762
|
letter-spacing: 0.02em;
|
package/ui/styles/sessions.css
CHANGED
|
@@ -1218,6 +1218,81 @@
|
|
|
1218
1218
|
background: rgba(255, 255, 255, 0.2);
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
/* ─── Thinking Group — collapsed view of consecutive trace events ─── */
|
|
1222
|
+
.thinking-group {
|
|
1223
|
+
align-self: stretch;
|
|
1224
|
+
border: 1px solid var(--border);
|
|
1225
|
+
border-radius: 12px;
|
|
1226
|
+
background: rgba(255, 255, 255, 0.02);
|
|
1227
|
+
margin: 2px 0 4px;
|
|
1228
|
+
overflow: hidden;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.thinking-group.has-errors {
|
|
1232
|
+
border-color: rgba(239, 68, 68, 0.35);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.thinking-group-head {
|
|
1236
|
+
display: flex;
|
|
1237
|
+
align-items: center;
|
|
1238
|
+
gap: 8px;
|
|
1239
|
+
padding: 7px 10px;
|
|
1240
|
+
width: 100%;
|
|
1241
|
+
border: 0;
|
|
1242
|
+
background: transparent;
|
|
1243
|
+
color: inherit;
|
|
1244
|
+
cursor: pointer;
|
|
1245
|
+
text-align: left;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
.thinking-group-head:hover {
|
|
1249
|
+
background: rgba(255, 255, 255, 0.03);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.thinking-group-badge {
|
|
1253
|
+
display: inline-flex;
|
|
1254
|
+
align-items: center;
|
|
1255
|
+
gap: 4px;
|
|
1256
|
+
padding: 2px 7px;
|
|
1257
|
+
border-radius: 999px;
|
|
1258
|
+
border: 1px solid rgba(245, 158, 11, 0.35);
|
|
1259
|
+
background: rgba(245, 158, 11, 0.12);
|
|
1260
|
+
font-size: 9px;
|
|
1261
|
+
font-weight: 700;
|
|
1262
|
+
letter-spacing: 0.07em;
|
|
1263
|
+
text-transform: uppercase;
|
|
1264
|
+
color: #fde68a;
|
|
1265
|
+
flex-shrink: 0;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
.thinking-group-badge svg {
|
|
1269
|
+
width: 10px;
|
|
1270
|
+
height: 10px;
|
|
1271
|
+
opacity: 0.85;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.thinking-group-label {
|
|
1275
|
+
flex: 1;
|
|
1276
|
+
min-width: 0;
|
|
1277
|
+
font-size: 11px;
|
|
1278
|
+
font-weight: 400;
|
|
1279
|
+
color: var(--text-secondary);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.thinking-group-chevron {
|
|
1283
|
+
flex-shrink: 0;
|
|
1284
|
+
font-size: 13px;
|
|
1285
|
+
color: var(--text-hint);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.thinking-group-body {
|
|
1289
|
+
border-top: 1px solid var(--border);
|
|
1290
|
+
padding: 6px;
|
|
1291
|
+
display: flex;
|
|
1292
|
+
flex-direction: column;
|
|
1293
|
+
gap: 2px;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1221
1296
|
/* Subtle entrance for bubbles — only the last few get the animation
|
|
1222
1297
|
(content-visibility: auto on older bubbles skips them for free) */
|
|
1223
1298
|
.chat-bubble:last-child,
|
package/ui/tabs/agents.js
CHANGED
|
@@ -87,6 +87,28 @@ function formatTaskOptionLabel(task) {
|
|
|
87
87
|
return `#${numberToken} ${task?.title || "(untitled task)"}`;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function normalizeDispatchTaskChoices(tasks) {
|
|
91
|
+
if (!Array.isArray(tasks)) return [];
|
|
92
|
+
const deduped = [];
|
|
93
|
+
const seenTaskIds = new Set();
|
|
94
|
+
for (const task of tasks) {
|
|
95
|
+
if (!task || typeof task !== "object") continue;
|
|
96
|
+
const status = String(task?.status || "").toLowerCase();
|
|
97
|
+
const dispatchable =
|
|
98
|
+
task?.draft === true || status === "draft" || status === "todo";
|
|
99
|
+
if (!dispatchable) continue;
|
|
100
|
+
const taskId = String(task?.id ?? task?.taskId ?? "").trim();
|
|
101
|
+
if (!taskId || seenTaskIds.has(taskId)) continue;
|
|
102
|
+
seenTaskIds.add(taskId);
|
|
103
|
+
deduped.push({ ...task, id: taskId });
|
|
104
|
+
}
|
|
105
|
+
return deduped.sort((a, b) => taskSortScore(b) - taskSortScore(a));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fleetSlotKey(index) {
|
|
109
|
+
return `slot-${index}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
/* ─── Workspace Viewer Modal ─── */
|
|
91
113
|
function WorkspaceViewer({ agent, onClose }) {
|
|
92
114
|
const [logText, setLogText] = useState("Loading…");
|
|
@@ -445,7 +467,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
445
467
|
${sessionInfo.preview &&
|
|
446
468
|
html`<div class="meta-text mt-xs">${truncate(sessionInfo.preview, 120)}</div>`}
|
|
447
469
|
<button class="btn btn-ghost btn-sm mt-sm" onClick=${() => setActiveTab("stream")}>
|
|
448
|
-
${iconText("
|
|
470
|
+
${iconText(":chat: View Stream")}
|
|
449
471
|
</button>
|
|
450
472
|
</div>
|
|
451
473
|
`}
|
|
@@ -565,7 +587,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
565
587
|
setStreamSnapshot({ events: [], fileAccess: null, capturedAt: null });
|
|
566
588
|
}
|
|
567
589
|
}}>
|
|
568
|
-
${streamPaused ? "
|
|
590
|
+
${streamPaused ? ":play: Resume" : ":pause: Pause"}
|
|
569
591
|
</button>
|
|
570
592
|
<button
|
|
571
593
|
class="btn btn-ghost btn-sm"
|
|
@@ -618,7 +640,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
618
640
|
`}
|
|
619
641
|
${filteredEvents.length === 0 &&
|
|
620
642
|
html`<div class="stream-empty">
|
|
621
|
-
<div class="stream-empty-icon">${resolveIcon("
|
|
643
|
+
<div class="stream-empty-icon">${resolveIcon(":server:")}</div>
|
|
622
644
|
<div class="stream-empty-text">
|
|
623
645
|
${toolEvents.length === 0 ? "No tool events yet" : "No events match filters"}
|
|
624
646
|
</div>
|
|
@@ -751,7 +773,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
751
773
|
html`<div class="meta-text mt-xs">Paused at ${snapshotMeta}</div>`}
|
|
752
774
|
${filteredFiles.length === 0 &&
|
|
753
775
|
html`<div class="stream-empty">
|
|
754
|
-
<div class="stream-empty-icon">${resolveIcon("
|
|
776
|
+
<div class="stream-empty-icon">${resolveIcon(":folder:")}</div>
|
|
755
777
|
<div class="stream-empty-text">
|
|
756
778
|
${summaryFiles.length === 0 ? "No file access recorded" : "No files match filters"}
|
|
757
779
|
</div>
|
|
@@ -819,7 +841,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
819
841
|
${sessionInfo.preview &&
|
|
820
842
|
html`<div class="meta-text mt-xs">${truncate(sessionInfo.preview, 140)}</div>`}
|
|
821
843
|
<button class="btn btn-ghost btn-sm mt-sm" onClick=${() => setActiveTab("stream")}>
|
|
822
|
-
${iconText("
|
|
844
|
+
${iconText(":chat: View Stream")}
|
|
823
845
|
</button>
|
|
824
846
|
</div>
|
|
825
847
|
`}
|
|
@@ -902,15 +924,15 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
902
924
|
<button
|
|
903
925
|
class="session-detail-tab ${activeTab === "stream" ? "active" : ""}"
|
|
904
926
|
onClick=${() => setActiveTab("stream")}
|
|
905
|
-
>${iconText("
|
|
927
|
+
>${iconText(":chat: Stream")}</button>
|
|
906
928
|
<button
|
|
907
929
|
class="session-detail-tab ${activeTab === "changes" ? "active" : ""}"
|
|
908
930
|
onClick=${() => setActiveTab("changes")}
|
|
909
|
-
>${iconText("
|
|
931
|
+
>${iconText(":edit: Changes")}</button>
|
|
910
932
|
<button
|
|
911
933
|
class="session-detail-tab ${activeTab === "logs" ? "active" : ""}"
|
|
912
934
|
onClick=${() => setActiveTab("logs")}
|
|
913
|
-
>${iconText("
|
|
935
|
+
>${iconText(":file: Logs")}</button>
|
|
914
936
|
</div>
|
|
915
937
|
|
|
916
938
|
<div class="workspace-body">
|
|
@@ -920,7 +942,7 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
920
942
|
? html`<${ChatView} sessionId=${sessionId} readOnly=${true} />`
|
|
921
943
|
: html`
|
|
922
944
|
<div class="chat-view chat-empty-state">
|
|
923
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
945
|
+
<div class="session-empty-icon">${resolveIcon(":chat:")}</div>
|
|
924
946
|
<div class="session-empty-text">No session stream available</div>
|
|
925
947
|
</div>
|
|
926
948
|
`}
|
|
@@ -938,12 +960,12 @@ function WorkspaceViewer({ agent, onClose }) {
|
|
|
938
960
|
onInput=${(e) => setSteerInput(e.target.value)}
|
|
939
961
|
onKeyDown=${(e) => { if (e.key === "Enter") { e.preventDefault(); handleSteer(); } }}
|
|
940
962
|
/>
|
|
941
|
-
<button class="btn btn-primary btn-sm" onClick=${handleSteer}>${resolveIcon("
|
|
963
|
+
<button class="btn btn-primary btn-sm" onClick=${handleSteer}>${resolveIcon(":target:")}</button>
|
|
942
964
|
<button
|
|
943
965
|
class="btn btn-danger btn-sm"
|
|
944
966
|
disabled=${agent.index == null}
|
|
945
967
|
onClick=${handleStop}
|
|
946
|
-
>${iconText("
|
|
968
|
+
>${iconText(":ban: Stop")}</button>
|
|
947
969
|
</div>
|
|
948
970
|
</div>
|
|
949
971
|
</div>
|
|
@@ -958,26 +980,34 @@ function DispatchSection({ freeSlots, inputRef, className = "" }) {
|
|
|
958
980
|
const [dispatching, setDispatching] = useState(false);
|
|
959
981
|
const [taskChoices, setTaskChoices] = useState([]);
|
|
960
982
|
const [tasksLoading, setTasksLoading] = useState(false);
|
|
983
|
+
const latestTaskRequestRef = useRef(0);
|
|
984
|
+
const mountedRef = useRef(true);
|
|
961
985
|
|
|
962
986
|
const canDispatch = Boolean(taskId.trim() || prompt.trim());
|
|
963
987
|
|
|
988
|
+
useEffect(() => {
|
|
989
|
+
mountedRef.current = true;
|
|
990
|
+
return () => {
|
|
991
|
+
mountedRef.current = false;
|
|
992
|
+
};
|
|
993
|
+
}, []);
|
|
994
|
+
|
|
964
995
|
const loadDispatchTasks = useCallback(() => {
|
|
996
|
+
const requestId = latestTaskRequestRef.current + 1;
|
|
997
|
+
latestTaskRequestRef.current = requestId;
|
|
965
998
|
setTasksLoading(true);
|
|
966
999
|
apiFetch("/api/tasks?limit=1000", { _silent: true })
|
|
967
1000
|
.then((res) => {
|
|
968
|
-
|
|
969
|
-
const choices =
|
|
970
|
-
.filter((task) => {
|
|
971
|
-
const status = String(task?.status || "").toLowerCase();
|
|
972
|
-
return task?.draft === true || status === "draft" || status === "todo";
|
|
973
|
-
})
|
|
974
|
-
.sort((a, b) => taskSortScore(b) - taskSortScore(a));
|
|
1001
|
+
if (!mountedRef.current || latestTaskRequestRef.current !== requestId) return;
|
|
1002
|
+
const choices = normalizeDispatchTaskChoices(res?.data);
|
|
975
1003
|
setTaskChoices(choices);
|
|
976
1004
|
})
|
|
977
1005
|
.catch(() => {
|
|
1006
|
+
if (!mountedRef.current || latestTaskRequestRef.current !== requestId) return;
|
|
978
1007
|
setTaskChoices([]);
|
|
979
1008
|
})
|
|
980
1009
|
.finally(() => {
|
|
1010
|
+
if (!mountedRef.current || latestTaskRequestRef.current !== requestId) return;
|
|
981
1011
|
setTasksLoading(false);
|
|
982
1012
|
});
|
|
983
1013
|
}, []);
|
|
@@ -1045,8 +1075,8 @@ function DispatchSection({ freeSlots, inputRef, className = "" }) {
|
|
|
1045
1075
|
<option value="">
|
|
1046
1076
|
${tasksLoading ? "Loading tasks…" : "Select backlog or draft task"}
|
|
1047
1077
|
</option>
|
|
1048
|
-
${taskChoices.map((task) => html`
|
|
1049
|
-
<option key=${task.id} value=${task.id}>
|
|
1078
|
+
${taskChoices.map((task, i) => html`
|
|
1079
|
+
<option key=${`${task.id}-${i}`} value=${task.id}>
|
|
1050
1080
|
${formatTaskOptionLabel(task)}
|
|
1051
1081
|
</option>
|
|
1052
1082
|
`)}
|
|
@@ -1065,7 +1095,7 @@ function DispatchSection({ freeSlots, inputRef, className = "" }) {
|
|
|
1065
1095
|
disabled=${!canDispatch || dispatching}
|
|
1066
1096
|
onClick=${handleDispatch}
|
|
1067
1097
|
>
|
|
1068
|
-
${dispatching ? "Dispatching…" : iconText("
|
|
1098
|
+
${dispatching ? "Dispatching…" : iconText(":rocket: Dispatch")}
|
|
1069
1099
|
</button>
|
|
1070
1100
|
</div>
|
|
1071
1101
|
<//>
|
|
@@ -1322,7 +1352,7 @@ export function AgentsTab() {
|
|
|
1322
1352
|
|
|
1323
1353
|
<div class="fleet-quick-actions">
|
|
1324
1354
|
<button class="btn btn-primary btn-sm" onClick=${handleFocusDispatch}>
|
|
1325
|
-
${iconText("
|
|
1355
|
+
${iconText(":rocket: Dispatch")}
|
|
1326
1356
|
</button>
|
|
1327
1357
|
<button class="btn btn-secondary btn-sm" onClick=${handleFleetRefresh}>
|
|
1328
1358
|
↻ Refresh
|
|
@@ -1331,7 +1361,7 @@ export function AgentsTab() {
|
|
|
1331
1361
|
class="btn btn-ghost btn-sm"
|
|
1332
1362
|
onClick=${() => navigateTo("logs")}
|
|
1333
1363
|
>
|
|
1334
|
-
${iconText("
|
|
1364
|
+
${iconText(":file: Logs")}
|
|
1335
1365
|
</button>
|
|
1336
1366
|
</div>
|
|
1337
1367
|
<//>
|
|
@@ -1355,7 +1385,7 @@ export function AgentsTab() {
|
|
|
1355
1385
|
const st = slot ? slot.status || "busy" : "idle";
|
|
1356
1386
|
return html`
|
|
1357
1387
|
<div
|
|
1358
|
-
key=${i}
|
|
1388
|
+
key=${fleetSlotKey(i)}
|
|
1359
1389
|
class="slot-cell slot-${st}"
|
|
1360
1390
|
title=${slot
|
|
1361
1391
|
? `${slot.taskTitle || slot.taskId} (${st})`
|
|
@@ -1387,8 +1417,8 @@ export function AgentsTab() {
|
|
|
1387
1417
|
: "No active slots"}
|
|
1388
1418
|
</div>
|
|
1389
1419
|
${slots.length
|
|
1390
|
-
|
|
1391
|
-
|
|
1420
|
+
? slots.map(
|
|
1421
|
+
(slot, i) => html`
|
|
1392
1422
|
<div
|
|
1393
1423
|
key=${slot?.taskId || slot?.sessionId || `slot-${i}`}
|
|
1394
1424
|
class="task-card fleet-agent-card ${expandedSlot === i
|
|
@@ -1468,7 +1498,7 @@ export function AgentsTab() {
|
|
|
1468
1498
|
(slot.taskId || slot.branch || "").slice(0, 12),
|
|
1469
1499
|
)}
|
|
1470
1500
|
>
|
|
1471
|
-
${iconText("
|
|
1501
|
+
${iconText(":file: Logs")}
|
|
1472
1502
|
</button>
|
|
1473
1503
|
<button
|
|
1474
1504
|
class="btn btn-ghost btn-sm"
|
|
@@ -1477,19 +1507,19 @@ export function AgentsTab() {
|
|
|
1477
1507
|
`/steer focus on ${slot.taskTitle || slot.taskId}`,
|
|
1478
1508
|
)}
|
|
1479
1509
|
>
|
|
1480
|
-
${iconText("
|
|
1510
|
+
${iconText(":target: Steer")}
|
|
1481
1511
|
</button>
|
|
1482
1512
|
<button
|
|
1483
1513
|
class="btn btn-ghost btn-sm"
|
|
1484
1514
|
onClick=${() => openWorkspace(slot, i)}
|
|
1485
1515
|
>
|
|
1486
|
-
${iconText("
|
|
1516
|
+
${iconText(":search: View")}
|
|
1487
1517
|
</button>
|
|
1488
1518
|
<button
|
|
1489
1519
|
class="btn btn-danger btn-sm"
|
|
1490
1520
|
onClick=${() => handleForceStop({ ...slot, index: i })}
|
|
1491
1521
|
>
|
|
1492
|
-
${iconText("
|
|
1522
|
+
${iconText(":ban: Stop")}
|
|
1493
1523
|
</button>
|
|
1494
1524
|
</div>
|
|
1495
1525
|
</div>
|
|
@@ -1639,15 +1669,15 @@ function ContextViewer({ sessionId }) {
|
|
|
1639
1669
|
|
|
1640
1670
|
if (error) {
|
|
1641
1671
|
return html`<div class="chat-view chat-empty-state">
|
|
1642
|
-
<div class="session-empty-icon" style="color:var(--color-error)">${resolveIcon("
|
|
1672
|
+
<div class="session-empty-icon" style="color:var(--color-error)">${resolveIcon(":alert:")}</div>
|
|
1643
1673
|
<div class="session-empty-text">${error}</div>
|
|
1644
|
-
<button class="btn btn-primary btn-sm mt-sm" onClick=${() => { setLoading(true); setError(null); fetchContext(); }}>${iconText("
|
|
1674
|
+
<button class="btn btn-primary btn-sm mt-sm" onClick=${() => { setLoading(true); setError(null); fetchContext(); }}>${iconText(":refresh: Retry")}</button>
|
|
1645
1675
|
</div>`;
|
|
1646
1676
|
}
|
|
1647
1677
|
|
|
1648
1678
|
if (!ctx?.context) {
|
|
1649
1679
|
return html`<div class="chat-view chat-empty-state">
|
|
1650
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
1680
|
+
<div class="session-empty-icon">${resolveIcon(":clipboard:")}</div>
|
|
1651
1681
|
<div class="session-empty-text">No context available for this session</div>
|
|
1652
1682
|
</div>`;
|
|
1653
1683
|
}
|
|
@@ -1895,10 +1925,10 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1895
1925
|
</div>
|
|
1896
1926
|
<div class="btn-row">
|
|
1897
1927
|
<button class="btn btn-ghost btn-sm" onClick=${() => onOpenWorkspace(selectedEntry.slot, selectedEntry.index)}>
|
|
1898
|
-
${iconText("
|
|
1928
|
+
${iconText(":search: Workspace")}
|
|
1899
1929
|
</button>
|
|
1900
1930
|
<button class="btn btn-danger btn-sm" onClick=${() => onForceStop({ ...selectedEntry.slot, index: selectedEntry.index })}>
|
|
1901
|
-
${iconText("
|
|
1931
|
+
${iconText(":ban: Stop")}
|
|
1902
1932
|
</button>
|
|
1903
1933
|
</div>
|
|
1904
1934
|
</div>
|
|
@@ -1906,19 +1936,19 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1906
1936
|
<button
|
|
1907
1937
|
class="session-detail-tab ${detailTab === "stream" ? "active" : ""}"
|
|
1908
1938
|
onClick=${() => setDetailTab("stream")}
|
|
1909
|
-
>${iconText("
|
|
1939
|
+
>${iconText(":chat: Stream")}</button>
|
|
1910
1940
|
<button
|
|
1911
1941
|
class="session-detail-tab ${detailTab === "context" ? "active" : ""}"
|
|
1912
1942
|
onClick=${() => setDetailTab("context")}
|
|
1913
|
-
>${iconText("
|
|
1943
|
+
>${iconText(":clipboard: Context")}</button>
|
|
1914
1944
|
<button
|
|
1915
1945
|
class="session-detail-tab ${detailTab === "diff" ? "active" : ""}"
|
|
1916
1946
|
onClick=${() => setDetailTab("diff")}
|
|
1917
|
-
>${iconText("
|
|
1947
|
+
>${iconText(":edit: Diff")}</button>
|
|
1918
1948
|
<button
|
|
1919
1949
|
class="session-detail-tab ${detailTab === "logs" ? "active" : ""}"
|
|
1920
1950
|
onClick=${() => setDetailTab("logs")}
|
|
1921
|
-
>${iconText("
|
|
1951
|
+
>${iconText(":file: Logs")}</button>
|
|
1922
1952
|
</div>
|
|
1923
1953
|
<div class="fleet-session-body">
|
|
1924
1954
|
${detailTab === "stream"
|
|
@@ -1926,7 +1956,7 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1926
1956
|
? html`<${ChatView} sessionId=${sessionId} readOnly=${true} />`
|
|
1927
1957
|
: html`
|
|
1928
1958
|
<div class="chat-view chat-empty-state">
|
|
1929
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
1959
|
+
<div class="session-empty-icon">${resolveIcon(":chat:")}</div>
|
|
1930
1960
|
<div class="session-empty-text">No linked chat session found for this slot</div>
|
|
1931
1961
|
</div>
|
|
1932
1962
|
`
|
|
@@ -1935,7 +1965,7 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1935
1965
|
? html`<${ContextViewer} sessionId=${contextId} />`
|
|
1936
1966
|
: html`
|
|
1937
1967
|
<div class="chat-view chat-empty-state">
|
|
1938
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
1968
|
+
<div class="session-empty-icon">${resolveIcon(":clipboard:")}</div>
|
|
1939
1969
|
<div class="session-empty-text">No context source available</div>
|
|
1940
1970
|
</div>
|
|
1941
1971
|
`
|
|
@@ -1944,7 +1974,7 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1944
1974
|
? html`<${DiffViewer} sessionId=${sessionId} />`
|
|
1945
1975
|
: html`
|
|
1946
1976
|
<div class="chat-view chat-empty-state">
|
|
1947
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
1977
|
+
<div class="session-empty-icon">${resolveIcon(":edit:")}</div>
|
|
1948
1978
|
<div class="session-empty-text">Diff requires a linked session</div>
|
|
1949
1979
|
</div>
|
|
1950
1980
|
`
|
|
@@ -1955,7 +1985,7 @@ function FleetSessionsPanel({ slots, onOpenWorkspace, onForceStop }) {
|
|
|
1955
1985
|
`
|
|
1956
1986
|
: html`
|
|
1957
1987
|
<div class="chat-view chat-empty-state">
|
|
1958
|
-
<div class="session-empty-icon">${resolveIcon("
|
|
1988
|
+
<div class="session-empty-icon">${resolveIcon(":chat:")}</div>
|
|
1959
1989
|
<div class="session-empty-text">Select a slot to open full session view</div>
|
|
1960
1990
|
</div>
|
|
1961
1991
|
`}
|
package/ui/tabs/chat.js
CHANGED
|
@@ -54,8 +54,7 @@ import { routeParams, setRouteParams } from "../modules/router.js";
|
|
|
54
54
|
import { ChatView } from "../components/chat-view.js";
|
|
55
55
|
import { apiFetch } from "../modules/api.js";
|
|
56
56
|
import { showToast } from "../modules/state.js";
|
|
57
|
-
import { VoiceMicButton } from "../modules/voice.js";
|
|
58
|
-
import { VoiceOverlay } from "../modules/voice-overlay.js";
|
|
57
|
+
import { VoiceMicButton, requestVoiceModeOpen } from "../modules/voice.js";
|
|
59
58
|
import { iconText, resolveIcon } from "../modules/icon-utils.js";
|
|
60
59
|
import {
|
|
61
60
|
ChatInputToolbar,
|
|
@@ -246,8 +245,6 @@ export function ChatTab() {
|
|
|
246
245
|
const [slashActiveIdx, setSlashActiveIdx] = useState(0);
|
|
247
246
|
const [renamingSessionId, setRenamingSessionId] = useState(null);
|
|
248
247
|
const [sending, setSending] = useState(false);
|
|
249
|
-
const [voiceModeOpen, setVoiceModeOpen] = useState(false);
|
|
250
|
-
const [voiceConfig, setVoiceConfig] = useState(null);
|
|
251
248
|
const [isMobile, setIsMobile] = useState(() => {
|
|
252
249
|
try {
|
|
253
250
|
return globalThis.matchMedia?.("(max-width: 768px)")?.matches ?? false;
|
|
@@ -307,14 +304,6 @@ export function ChatTab() {
|
|
|
307
304
|
};
|
|
308
305
|
}, []);
|
|
309
306
|
|
|
310
|
-
/* ── Fetch voice config on mount ── */
|
|
311
|
-
useEffect(() => {
|
|
312
|
-
fetch("/api/voice/config")
|
|
313
|
-
.then(r => r.ok ? r.json() : null)
|
|
314
|
-
.then(cfg => setVoiceConfig(cfg))
|
|
315
|
-
.catch(() => setVoiceConfig(null));
|
|
316
|
-
}, []);
|
|
317
|
-
|
|
318
307
|
/* ── Track mobile viewport to avoid auto-select loops ── */
|
|
319
308
|
useEffect(() => {
|
|
320
309
|
const mq = globalThis.matchMedia?.("(max-width: 768px)");
|
|
@@ -536,7 +525,7 @@ export function ChatTab() {
|
|
|
536
525
|
method: "POST",
|
|
537
526
|
body: JSON.stringify({ command: cmdBase, args: cmdArgs }),
|
|
538
527
|
});
|
|
539
|
-
const resultText = resp?.result || resp?.data ||
|
|
528
|
+
const resultText = resp?.result || resp?.data || `:check: SDK command executed: ${cmdBase}`;
|
|
540
529
|
if (sessionId) {
|
|
541
530
|
const { sessionMessages } = await import("../components/session-list.js");
|
|
542
531
|
const now = new Date().toISOString();
|
|
@@ -560,7 +549,7 @@ export function ChatTab() {
|
|
|
560
549
|
const msgs = sessionMessages.value || [];
|
|
561
550
|
const userMsg = { id: `cmd-${Date.now()}`, role: "user", content, timestamp: now };
|
|
562
551
|
const resultText = data?.content || data?.error
|
|
563
|
-
|| (data?.readOnly ?
|
|
552
|
+
|| (data?.readOnly ? `:check: ${cmdBase} — see the relevant tab for details.` : `:check: Command executed: ${cmdBase}`);
|
|
564
553
|
const sysMsg = { id: `cmd-r-${Date.now()}`, role: "system", content: resultText, timestamp: now };
|
|
565
554
|
sessionMessages.value = [...msgs, userMsg, sysMsg];
|
|
566
555
|
} else {
|
|
@@ -702,6 +691,20 @@ export function ChatTab() {
|
|
|
702
691
|
await createSession({ type: "primary" });
|
|
703
692
|
}
|
|
704
693
|
|
|
694
|
+
const openMeetingRoom = useCallback(
|
|
695
|
+
(call = "voice") => {
|
|
696
|
+
requestVoiceModeOpen({
|
|
697
|
+
call: call === "video" ? "video" : "voice",
|
|
698
|
+
sessionId: sessionId || undefined,
|
|
699
|
+
initialVisionSource: call === "video" ? "camera" : null,
|
|
700
|
+
executor: activeAgent.value || undefined,
|
|
701
|
+
mode: agentMode.value || undefined,
|
|
702
|
+
model: selectedModel.value || undefined,
|
|
703
|
+
});
|
|
704
|
+
},
|
|
705
|
+
[sessionId],
|
|
706
|
+
);
|
|
707
|
+
|
|
705
708
|
/* ── Show/expand sessions: on mobile toggles drawer, on desktop fires rail-expand event ── */
|
|
706
709
|
const handleShowSessions = useCallback(() => {
|
|
707
710
|
if (isMobile) {
|
|
@@ -784,13 +787,29 @@ export function ChatTab() {
|
|
|
784
787
|
<div class="chat-shell-inner">
|
|
785
788
|
<!-- Sessions toggle: shown on mobile always; on desktop only when rail is collapsed (CSS-controlled) -->
|
|
786
789
|
<button class="session-drawer-btn session-drawer-btn-rail" onClick=${handleShowSessions}>
|
|
787
|
-
${iconText("
|
|
790
|
+
${iconText(":menu: Sessions")}
|
|
788
791
|
</button>
|
|
789
792
|
<div class="chat-shell-title">
|
|
790
793
|
<div class="chat-shell-name">${sessionTitle}</div>
|
|
791
794
|
<div class="chat-shell-meta">${sessionMeta || "Session"}</div>
|
|
792
795
|
</div>
|
|
793
796
|
<div class="chat-shell-actions">
|
|
797
|
+
<button
|
|
798
|
+
class="btn btn-ghost btn-sm"
|
|
799
|
+
onClick=${() => openMeetingRoom("voice")}
|
|
800
|
+
title="Start voice meeting for this session"
|
|
801
|
+
>
|
|
802
|
+
<span class="btn-icon">${resolveIcon("phone")}</span>
|
|
803
|
+
Call
|
|
804
|
+
</button>
|
|
805
|
+
<button
|
|
806
|
+
class="btn btn-ghost btn-sm"
|
|
807
|
+
onClick=${() => openMeetingRoom("video")}
|
|
808
|
+
title="Start video meeting for this session"
|
|
809
|
+
>
|
|
810
|
+
<span class="btn-icon">${resolveIcon("camera")}</span>
|
|
811
|
+
Video
|
|
812
|
+
</button>
|
|
794
813
|
${isDesktop &&
|
|
795
814
|
html`
|
|
796
815
|
<button
|
|
@@ -858,37 +877,23 @@ export function ChatTab() {
|
|
|
858
877
|
onKeyDown=${handleKeyDown}
|
|
859
878
|
/>
|
|
860
879
|
<${VoiceMicButton}
|
|
861
|
-
onTranscript=${(t) => {
|
|
862
|
-
setInputValue((prev) => (prev ? prev + " " + t : t));
|
|
863
|
-
if (textareaRef.current) textareaRef.current.focus();
|
|
864
|
-
}}
|
|
865
880
|
disabled=${sending}
|
|
866
|
-
title="
|
|
881
|
+
title="Live voice mode"
|
|
867
882
|
/>
|
|
868
|
-
${voiceConfig?.available && html`
|
|
869
|
-
<button
|
|
870
|
-
class="chat-send-btn"
|
|
871
|
-
onClick=${() => { setVoiceModeOpen(true); }}
|
|
872
|
-
title="Voice mode (${voiceConfig.tier === 1 ? 'Realtime' : 'Fallback'})"
|
|
873
|
-
style="background: linear-gradient(135deg, rgba(99,102,241,0.2), rgba(16,185,129,0.2)); border-color: rgba(99,102,241,0.3);"
|
|
874
|
-
>
|
|
875
|
-
${resolveIcon("headphones") || "\uD83C\uDFA7"}
|
|
876
|
-
</button>
|
|
877
|
-
`}
|
|
878
883
|
<button
|
|
879
884
|
class="chat-send-btn"
|
|
880
885
|
disabled=${!inputValue.trim() || sending}
|
|
881
886
|
onClick=${handleSend}
|
|
882
887
|
title="Send (Enter)"
|
|
883
888
|
>
|
|
884
|
-
${resolveIcon(sending ? "
|
|
889
|
+
${resolveIcon(sending ? ":clock:" : "➤")}
|
|
885
890
|
</button>
|
|
886
891
|
</div>
|
|
887
892
|
<div class="chat-input-hint">
|
|
888
893
|
<span>Shift+Enter for new line</span>
|
|
889
894
|
<span>Type / for commands</span>
|
|
890
895
|
${offlineQueueSize.peek() > 0 && html`
|
|
891
|
-
<span class="chat-offline-badge">${iconText(
|
|
896
|
+
<span class="chat-offline-badge">${iconText(`:upload: ${offlineQueueSize.peek()} queued`)}</span>
|
|
892
897
|
`}
|
|
893
898
|
</div>
|
|
894
899
|
</div>
|
|
@@ -908,14 +913,6 @@ export function ChatTab() {
|
|
|
908
913
|
onClick=${() => setDrawerOpen(false)}
|
|
909
914
|
></div>
|
|
910
915
|
`}
|
|
911
|
-
${voiceModeOpen && html`
|
|
912
|
-
<${VoiceOverlay}
|
|
913
|
-
visible=${voiceModeOpen}
|
|
914
|
-
onClose=${() => setVoiceModeOpen(false)}
|
|
915
|
-
tier=${voiceConfig?.tier || 2}
|
|
916
|
-
sessionId=${sessionId}
|
|
917
|
-
/>
|
|
918
|
-
`}
|
|
919
916
|
</div>
|
|
920
917
|
`;
|
|
921
918
|
}
|
package/ui/tabs/control.js
CHANGED
|
@@ -856,7 +856,7 @@ export function ControlTab() {
|
|
|
856
856
|
sendCmd(planFocus ? `/plan ${n} ${planFocus}` : `/plan ${n}`);
|
|
857
857
|
}}
|
|
858
858
|
>
|
|
859
|
-
${iconText("
|
|
859
|
+
${iconText(":clipboard: Plan")}
|
|
860
860
|
</button>
|
|
861
861
|
</div>
|
|
862
862
|
</div>
|
|
@@ -927,7 +927,7 @@ export function ControlTab() {
|
|
|
927
927
|
style="flex:1"
|
|
928
928
|
/>
|
|
929
929
|
<button class="btn btn-secondary btn-sm" onClick=${handleQuickCmd}>
|
|
930
|
-
${iconText("
|
|
930
|
+
${iconText(":play: Run")}
|
|
931
931
|
</button>
|
|
932
932
|
</div>
|
|
933
933
|
${quickCmdFeedback && html`
|
package/ui/tabs/dashboard.js
CHANGED
|
@@ -587,7 +587,7 @@ export function DashboardTab() {
|
|
|
587
587
|
Your AI development fleet is ready. Create your first task to get started.
|
|
588
588
|
</div>
|
|
589
589
|
<button class="btn btn-primary" onClick=${() => setShowCreate(true)}>
|
|
590
|
-
${iconText("
|
|
590
|
+
${iconText(":plus: Create your first task")}
|
|
591
591
|
</button>
|
|
592
592
|
</div>
|
|
593
593
|
<//>
|