bosun 0.36.2 → 0.36.4
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/agent-prompts.mjs +95 -0
- package/analyze-agent-work-helpers.mjs +308 -0
- package/analyze-agent-work.mjs +926 -0
- package/autofix.mjs +2 -0
- package/bosun.schema.json +101 -3
- package/codex-shell.mjs +85 -10
- package/desktop/main.mjs +871 -48
- package/desktop/preload.mjs +54 -1
- package/desktop-shortcut.mjs +90 -11
- package/git-editor-fix.mjs +273 -0
- package/mcp-registry.mjs +579 -0
- package/meeting-workflow-service.mjs +631 -0
- package/monitor.mjs +18 -103
- package/package.json +21 -2
- package/primary-agent.mjs +32 -12
- package/session-tracker.mjs +68 -0
- package/setup-web-server.mjs +20 -10
- package/setup.mjs +376 -83
- package/startup-service.mjs +51 -6
- package/stream-resilience.mjs +17 -7
- package/ui/app.js +164 -4
- package/ui/components/agent-selector.js +145 -1
- package/ui/components/chat-view.js +161 -15
- package/ui/components/session-list.js +2 -2
- package/ui/components/shared.js +188 -15
- package/ui/modules/icons.js +13 -0
- package/ui/modules/utils.js +44 -0
- package/ui/modules/voice-client-sdk.js +733 -0
- package/ui/modules/voice-overlay.js +128 -15
- package/ui/modules/voice.js +15 -6
- package/ui/setup.html +281 -81
- package/ui/styles/components.css +99 -3
- package/ui/styles/sessions.css +122 -14
- package/ui/styles.css +14 -0
- package/ui/tabs/agents.js +1 -1
- package/ui/tabs/chat.js +123 -14
- package/ui/tabs/control.js +16 -22
- package/ui/tabs/dashboard.js +85 -8
- package/ui/tabs/library.js +113 -17
- package/ui/tabs/settings.js +116 -2
- package/ui/tabs/tasks.js +388 -39
- package/ui/tabs/telemetry.js +0 -1
- package/ui/tabs/workflows.js +4 -0
- package/ui-server.mjs +400 -22
- package/update-check.mjs +41 -13
- package/voice-action-dispatcher.mjs +844 -0
- package/voice-agents-sdk.mjs +664 -0
- package/voice-auth-manager.mjs +164 -0
- package/voice-relay.mjs +1194 -0
- package/voice-tools.mjs +914 -0
- package/workflow-templates/agents.mjs +6 -2
- package/workflow-templates/github.mjs +154 -12
- package/workflow-templates.mjs +3 -0
- package/github-reconciler.mjs +0 -506
- package/merge-strategy.mjs +0 -1210
- package/pr-cleanup-daemon.mjs +0 -992
- package/workspace-reaper.mjs +0 -405
package/stream-resilience.mjs
CHANGED
|
@@ -46,9 +46,9 @@ const streamConfig = readInternalExecutorStreamConfig();
|
|
|
46
46
|
export const MAX_STREAM_RETRIES = parseNumericSetting({
|
|
47
47
|
envKey: "INTERNAL_EXECUTOR_STREAM_MAX_RETRIES",
|
|
48
48
|
configValue: streamConfig.maxRetries,
|
|
49
|
-
fallback:
|
|
49
|
+
fallback: 8,
|
|
50
50
|
min: 1,
|
|
51
|
-
max:
|
|
51
|
+
max: 20,
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
/** Base backoff in ms. Doubles per attempt: 2 s → 4 s → 8 s → 16 s → 32 s. */
|
|
@@ -62,7 +62,7 @@ const STREAM_RETRY_BASE_MS = parseNumericSetting({
|
|
|
62
62
|
const STREAM_RETRY_MAX_MS = parseNumericSetting({
|
|
63
63
|
envKey: "INTERNAL_EXECUTOR_STREAM_RETRY_MAX_MS",
|
|
64
64
|
configValue: streamConfig.retryMaxMs,
|
|
65
|
-
fallback:
|
|
65
|
+
fallback: 60_000,
|
|
66
66
|
min: STREAM_RETRY_BASE_MS,
|
|
67
67
|
max: 300_000,
|
|
68
68
|
});
|
|
@@ -111,23 +111,33 @@ export function isTransientStreamError(err) {
|
|
|
111
111
|
msg.includes("service_unavailable") ||
|
|
112
112
|
msg.includes("529") || // Azure overloaded
|
|
113
113
|
msg.includes("rate_limit_exceeded") ||
|
|
114
|
-
msg.includes("overloaded_error") // Anthropic overloaded
|
|
114
|
+
msg.includes("overloaded_error") || // Anthropic overloaded
|
|
115
|
+
// ── Azure / Foundry specific ────────────────────────────────────────────
|
|
116
|
+
msg.includes("reconnecting") ||
|
|
117
|
+
msg.includes("upstream connect error") ||
|
|
118
|
+
msg.includes("no healthy upstream") ||
|
|
119
|
+
msg.includes("gateway timeout") ||
|
|
120
|
+
msg.includes("model is currently overloaded") ||
|
|
121
|
+
msg.includes("the server had an error") ||
|
|
122
|
+
msg.includes("an error occurred during streaming")
|
|
115
123
|
);
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
/**
|
|
119
|
-
* Exponential backoff delay for stream retries, with ±
|
|
127
|
+
* Exponential backoff delay for stream retries, with ±25% jitter.
|
|
120
128
|
*
|
|
121
129
|
* attempt 0 → ~2 s
|
|
122
130
|
* attempt 1 → ~4 s
|
|
123
131
|
* attempt 2 → ~8 s
|
|
124
132
|
* attempt 3 → ~16 s
|
|
125
|
-
* attempt 4 → ~32 s
|
|
133
|
+
* attempt 4 → ~32 s
|
|
134
|
+
* attempt 5+ → ~60 s (capped)
|
|
126
135
|
*
|
|
127
136
|
* @param {number} attempt zero-based retry index
|
|
128
137
|
* @returns {number} delay in milliseconds
|
|
129
138
|
*/
|
|
130
139
|
export function streamRetryDelay(attempt) {
|
|
131
140
|
const base = Math.min(STREAM_RETRY_BASE_MS * 2 ** attempt, STREAM_RETRY_MAX_MS);
|
|
132
|
-
|
|
141
|
+
// ±25% jitter to avoid thundering herd on Azure reconnect
|
|
142
|
+
return base + (Math.random() - 0.5) * 0.5 * base;
|
|
133
143
|
}
|
package/ui/app.js
CHANGED
|
@@ -74,6 +74,7 @@ const VOICE_LAUNCH_QUERY_KEYS = [
|
|
|
74
74
|
"source",
|
|
75
75
|
"chat_id",
|
|
76
76
|
];
|
|
77
|
+
const FLOATING_CALL_STATE_KEY = "ve-floating-call-state";
|
|
77
78
|
|
|
78
79
|
function getAppLogoSource(index = 0) {
|
|
79
80
|
const safeIndex = Number.isFinite(index) ? Math.trunc(index) : 0;
|
|
@@ -143,6 +144,49 @@ function scrubVoiceLaunchQuery() {
|
|
|
143
144
|
window.history.replaceState(window.history.state, "", nextPath || "/");
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function isFollowWindowFromUrl() {
|
|
148
|
+
if (typeof window === "undefined") return false;
|
|
149
|
+
const params = new URLSearchParams(window.location.search || "");
|
|
150
|
+
return params.get("follow") === "1";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readFloatingCallState() {
|
|
154
|
+
if (typeof window === "undefined") return { active: false };
|
|
155
|
+
try {
|
|
156
|
+
const raw = localStorage.getItem(FLOATING_CALL_STATE_KEY);
|
|
157
|
+
if (!raw) return { active: false };
|
|
158
|
+
const parsed = JSON.parse(raw);
|
|
159
|
+
return {
|
|
160
|
+
active: parsed?.active === true,
|
|
161
|
+
call: String(parsed?.call || "").trim().toLowerCase() === "video"
|
|
162
|
+
? "video"
|
|
163
|
+
: "voice",
|
|
164
|
+
updatedAt: Number(parsed?.updatedAt || 0) || Date.now(),
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return { active: false };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function writeFloatingCallState(nextState) {
|
|
172
|
+
if (typeof window === "undefined") return;
|
|
173
|
+
try {
|
|
174
|
+
localStorage.setItem(
|
|
175
|
+
FLOATING_CALL_STATE_KEY,
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
active: nextState?.active === true,
|
|
178
|
+
call:
|
|
179
|
+
String(nextState?.call || "").trim().toLowerCase() === "video"
|
|
180
|
+
? "video"
|
|
181
|
+
: "voice",
|
|
182
|
+
updatedAt: Date.now(),
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
} catch {
|
|
186
|
+
// best effort
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
/* ── Module imports ── */
|
|
147
191
|
import { ICONS } from "./modules/icons.js";
|
|
148
192
|
import { iconText, resolveIcon } from "./modules/icon-utils.js";
|
|
@@ -665,7 +709,7 @@ function SidebarNav({ collapsed = false, onToggle }) {
|
|
|
665
709
|
<button
|
|
666
710
|
key=${tab.id}
|
|
667
711
|
class="sidebar-nav-item ${isActive ? "active" : ""} ${isChild ? "sidebar-nav-child" : ""}"
|
|
668
|
-
style
|
|
712
|
+
style="position:relative"
|
|
669
713
|
aria-label=${tab.label}
|
|
670
714
|
aria-current=${isActive ? "page" : null}
|
|
671
715
|
title=${collapsed ? tab.label : undefined}
|
|
@@ -962,7 +1006,7 @@ function InspectorPanel({ onResizeStart, onResizeReset, showResizer }) {
|
|
|
962
1006
|
* Bottom Navigation
|
|
963
1007
|
* ═══════════════════════════════════════════════ */
|
|
964
1008
|
const PRIMARY_NAV_TABS = ["dashboard", "chat", "tasks", "agents"];
|
|
965
|
-
const MORE_NAV_TABS = ["control", "infra", "logs", "library", "workflows", "settings"];
|
|
1009
|
+
const MORE_NAV_TABS = ["control", "infra", "logs", "telemetry", "library", "workflows", "settings"];
|
|
966
1010
|
|
|
967
1011
|
function getTabsById(ids) {
|
|
968
1012
|
return ids
|
|
@@ -1364,6 +1408,11 @@ function App() {
|
|
|
1364
1408
|
const [voiceInitialVisionSource, setVoiceInitialVisionSource] = useState(
|
|
1365
1409
|
null,
|
|
1366
1410
|
);
|
|
1411
|
+
const followWindowMode = isFollowWindowFromUrl();
|
|
1412
|
+
const followOverlayOpenedRef = useRef(false);
|
|
1413
|
+
const [floatingCallState, setFloatingCallState] = useState(() =>
|
|
1414
|
+
readFloatingCallState(),
|
|
1415
|
+
);
|
|
1367
1416
|
const resizeRef = useRef(null);
|
|
1368
1417
|
const [isCompactNav, setIsCompactNav] = useState(() => {
|
|
1369
1418
|
const win = globalThis.window;
|
|
@@ -1723,6 +1772,29 @@ function App() {
|
|
|
1723
1772
|
return;
|
|
1724
1773
|
}
|
|
1725
1774
|
|
|
1775
|
+
const desktopFollowApi = globalThis?.veDesktop?.follow;
|
|
1776
|
+
if (!followWindowMode && desktopFollowApi?.open) {
|
|
1777
|
+
try {
|
|
1778
|
+
await desktopFollowApi.open({
|
|
1779
|
+
call: requestedCallType,
|
|
1780
|
+
initialVisionSource: requestedVisionSource,
|
|
1781
|
+
sessionId: currentSessionId,
|
|
1782
|
+
executor: currentExecutor,
|
|
1783
|
+
mode: currentMode,
|
|
1784
|
+
model: currentModel,
|
|
1785
|
+
});
|
|
1786
|
+
const nextFloatingState = {
|
|
1787
|
+
active: true,
|
|
1788
|
+
call: requestedCallType,
|
|
1789
|
+
};
|
|
1790
|
+
setFloatingCallState(nextFloatingState);
|
|
1791
|
+
writeFloatingCallState(nextFloatingState);
|
|
1792
|
+
return;
|
|
1793
|
+
} catch {
|
|
1794
|
+
// Fall through to in-window overlay if desktop companion fails.
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1726
1798
|
setVoiceSessionId(currentSessionId);
|
|
1727
1799
|
setVoiceExecutor(currentExecutor);
|
|
1728
1800
|
setVoiceAgentMode(currentMode);
|
|
@@ -1749,8 +1821,39 @@ function App() {
|
|
|
1749
1821
|
globalThis.addEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
|
|
1750
1822
|
return () =>
|
|
1751
1823
|
globalThis.removeEventListener?.("ve:open-voice-mode", handleOpenVoiceMode);
|
|
1824
|
+
}, [followWindowMode]);
|
|
1825
|
+
|
|
1826
|
+
useEffect(() => {
|
|
1827
|
+
const onStorage = (event) => {
|
|
1828
|
+
if (event?.key && event.key !== FLOATING_CALL_STATE_KEY) return;
|
|
1829
|
+
setFloatingCallState(readFloatingCallState());
|
|
1830
|
+
};
|
|
1831
|
+
globalThis.addEventListener?.("storage", onStorage);
|
|
1832
|
+
return () => {
|
|
1833
|
+
globalThis.removeEventListener?.("storage", onStorage);
|
|
1834
|
+
};
|
|
1752
1835
|
}, []);
|
|
1753
1836
|
|
|
1837
|
+
useEffect(() => {
|
|
1838
|
+
if (!followWindowMode) return;
|
|
1839
|
+
const nextFloatingState = {
|
|
1840
|
+
active: Boolean(voiceOverlayOpen),
|
|
1841
|
+
call: voiceCallType,
|
|
1842
|
+
};
|
|
1843
|
+
setFloatingCallState(nextFloatingState);
|
|
1844
|
+
writeFloatingCallState(nextFloatingState);
|
|
1845
|
+
}, [followWindowMode, voiceOverlayOpen, voiceCallType]);
|
|
1846
|
+
|
|
1847
|
+
useEffect(() => {
|
|
1848
|
+
if (!followWindowMode) return;
|
|
1849
|
+
if (voiceOverlayOpen) {
|
|
1850
|
+
followOverlayOpenedRef.current = true;
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (!followOverlayOpenedRef.current) return;
|
|
1854
|
+
globalThis?.veDesktop?.follow?.hide?.().catch?.(() => {});
|
|
1855
|
+
}, [followWindowMode, voiceOverlayOpen]);
|
|
1856
|
+
|
|
1754
1857
|
useEffect(() => {
|
|
1755
1858
|
const launch = parseVoiceLaunchFromUrl();
|
|
1756
1859
|
if (!launch) return;
|
|
@@ -1875,6 +1978,14 @@ function App() {
|
|
|
1875
1978
|
const railSessionType = "primary";
|
|
1876
1979
|
const showDrawerToggles = isTablet;
|
|
1877
1980
|
const showInspectorToggle = isTablet && isChatOrAgents;
|
|
1981
|
+
const showRestoreFloatingCall =
|
|
1982
|
+
!followWindowMode &&
|
|
1983
|
+
floatingCallState?.active === true &&
|
|
1984
|
+
typeof globalThis?.veDesktop?.follow?.restore === "function";
|
|
1985
|
+
const floatingCallLabel =
|
|
1986
|
+
String(floatingCallState?.call || "").trim().toLowerCase() === "video"
|
|
1987
|
+
? "Restore floating video call"
|
|
1988
|
+
: "Restore floating voice call";
|
|
1878
1989
|
|
|
1879
1990
|
const shellStyle = isDesktop
|
|
1880
1991
|
? {
|
|
@@ -2053,9 +2164,42 @@ function App() {
|
|
|
2053
2164
|
open=${isBotOpen}
|
|
2054
2165
|
onClose=${closeBot}
|
|
2055
2166
|
/>
|
|
2167
|
+
${showRestoreFloatingCall
|
|
2168
|
+
? html`
|
|
2169
|
+
<button
|
|
2170
|
+
class="btn btn-primary floating-call-restore"
|
|
2171
|
+
title=${floatingCallLabel}
|
|
2172
|
+
onClick=${async () => {
|
|
2173
|
+
try {
|
|
2174
|
+
const result = await globalThis.veDesktop.follow.restore();
|
|
2175
|
+
if (!result?.ok) {
|
|
2176
|
+
const nextFloatingState = { active: false, call: floatingCallState?.call };
|
|
2177
|
+
setFloatingCallState(nextFloatingState);
|
|
2178
|
+
writeFloatingCallState(nextFloatingState);
|
|
2179
|
+
showToast("No floating call window is active.", "info");
|
|
2180
|
+
}
|
|
2181
|
+
} catch {
|
|
2182
|
+
showToast("Could not restore floating call window.", "error");
|
|
2183
|
+
}
|
|
2184
|
+
}}
|
|
2185
|
+
>
|
|
2186
|
+
${resolveIcon("phone")}
|
|
2187
|
+
${String(floatingCallState?.call || "").trim().toLowerCase() === "video"
|
|
2188
|
+
? " Restore Video Call"
|
|
2189
|
+
: " Restore Voice Call"}
|
|
2190
|
+
</button>
|
|
2191
|
+
`
|
|
2192
|
+
: null}
|
|
2056
2193
|
<${VoiceOverlay}
|
|
2057
2194
|
visible=${voiceOverlayOpen}
|
|
2058
2195
|
onClose=${() => setVoiceOverlayOpen(false)}
|
|
2196
|
+
onDismiss=${() => {
|
|
2197
|
+
if (followWindowMode && globalThis?.veDesktop?.follow?.hide) {
|
|
2198
|
+
globalThis.veDesktop.follow.hide().catch(() => {});
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
setVoiceOverlayOpen(false);
|
|
2202
|
+
}}
|
|
2059
2203
|
tier=${voiceTier}
|
|
2060
2204
|
sessionId=${voiceSessionId}
|
|
2061
2205
|
executor=${voiceExecutor}
|
|
@@ -2063,11 +2207,27 @@ function App() {
|
|
|
2063
2207
|
model=${voiceModel}
|
|
2064
2208
|
callType=${voiceCallType}
|
|
2065
2209
|
initialVisionSource=${voiceInitialVisionSource}
|
|
2210
|
+
compact=${followWindowMode}
|
|
2066
2211
|
/>
|
|
2067
2212
|
`;
|
|
2068
2213
|
}
|
|
2069
2214
|
|
|
2070
2215
|
/* ─── Mount ─── */
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2216
|
+
const mountRoot = () => document.getElementById("app");
|
|
2217
|
+
const mountApp = () => {
|
|
2218
|
+
const root = mountRoot();
|
|
2219
|
+
if (!root) return;
|
|
2220
|
+
preactRender(html`<${App} />`, root);
|
|
2221
|
+
};
|
|
2222
|
+
const remountApp = () => {
|
|
2223
|
+
const root = mountRoot();
|
|
2224
|
+
if (!root) return;
|
|
2225
|
+
try {
|
|
2226
|
+
preactRender(null, root);
|
|
2227
|
+
} catch {
|
|
2228
|
+
root.replaceChildren();
|
|
2229
|
+
}
|
|
2230
|
+
preactRender(html`<${App} />`, root);
|
|
2231
|
+
};
|
|
2232
|
+
globalThis.__veRemountApp = remountApp;
|
|
2073
2233
|
mountApp();
|
|
@@ -29,7 +29,7 @@ const html = htm.bind(h);
|
|
|
29
29
|
* ═══════════════════════════════════════════════ */
|
|
30
30
|
|
|
31
31
|
/** Current agent interaction mode */
|
|
32
|
-
export const agentMode = signal("
|
|
32
|
+
export const agentMode = signal("ask"); // "ask" | "agent" | "plan"
|
|
33
33
|
|
|
34
34
|
/** Available agents loaded from API */
|
|
35
35
|
export const availableAgents = signal([]); // Array<{ id, name, provider, available, busy, capabilities }>
|
|
@@ -697,6 +697,150 @@ const AGENT_SELECTOR_STYLES = `
|
|
|
697
697
|
.toolbar-select:focus { outline: none; border-color: var(--tg-theme-button-color, #3b82f6); }
|
|
698
698
|
.toolbar-select option { background: #1a1a2e; color: #fff; }
|
|
699
699
|
.toolbar-select--wide { min-width: 110px; }
|
|
700
|
+
|
|
701
|
+
/* ── Stop Button ── */
|
|
702
|
+
.chat-stop-btn {
|
|
703
|
+
display: flex;
|
|
704
|
+
align-items: center;
|
|
705
|
+
justify-content: center;
|
|
706
|
+
width: 36px;
|
|
707
|
+
height: 36px;
|
|
708
|
+
border-radius: 50%;
|
|
709
|
+
border: 2px solid #ef4444;
|
|
710
|
+
background: rgba(239, 68, 68, 0.12);
|
|
711
|
+
color: #ef4444;
|
|
712
|
+
cursor: pointer;
|
|
713
|
+
font-size: 14px;
|
|
714
|
+
flex-shrink: 0;
|
|
715
|
+
transition: background 0.2s ease, transform 0.1s ease;
|
|
716
|
+
-webkit-tap-highlight-color: transparent;
|
|
717
|
+
padding: 0;
|
|
718
|
+
}
|
|
719
|
+
.chat-stop-btn:hover {
|
|
720
|
+
background: rgba(239, 68, 68, 0.22);
|
|
721
|
+
transform: scale(1.05);
|
|
722
|
+
}
|
|
723
|
+
.chat-stop-btn:active {
|
|
724
|
+
transform: scale(0.95);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* ── Split Send Button Group ── */
|
|
728
|
+
.chat-send-group {
|
|
729
|
+
position: relative;
|
|
730
|
+
display: flex;
|
|
731
|
+
flex-shrink: 0;
|
|
732
|
+
}
|
|
733
|
+
.chat-send-main {
|
|
734
|
+
display: flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
height: 36px;
|
|
738
|
+
padding: 0 13px;
|
|
739
|
+
border: none;
|
|
740
|
+
border-radius: 8px 0 0 8px;
|
|
741
|
+
background: var(--tg-theme-button-color, #3b82f6);
|
|
742
|
+
color: var(--tg-theme-button-text-color, #fff);
|
|
743
|
+
cursor: pointer;
|
|
744
|
+
font-size: 15px;
|
|
745
|
+
transition: background 0.2s ease, opacity 0.2s ease;
|
|
746
|
+
-webkit-tap-highlight-color: transparent;
|
|
747
|
+
}
|
|
748
|
+
.chat-send-main:disabled {
|
|
749
|
+
opacity: 0.4;
|
|
750
|
+
cursor: not-allowed;
|
|
751
|
+
}
|
|
752
|
+
.chat-send-main:not(:disabled):hover {
|
|
753
|
+
background: #2563eb;
|
|
754
|
+
}
|
|
755
|
+
.chat-send-chevron {
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: center;
|
|
758
|
+
justify-content: center;
|
|
759
|
+
height: 36px;
|
|
760
|
+
width: 22px;
|
|
761
|
+
border: none;
|
|
762
|
+
border-left: 1px solid rgba(255,255,255,0.2);
|
|
763
|
+
border-radius: 0 8px 8px 0;
|
|
764
|
+
background: var(--tg-theme-button-color, #3b82f6);
|
|
765
|
+
color: rgba(255,255,255,0.85);
|
|
766
|
+
cursor: pointer;
|
|
767
|
+
font-size: 10px;
|
|
768
|
+
transition: background 0.2s ease, opacity 0.2s ease;
|
|
769
|
+
-webkit-tap-highlight-color: transparent;
|
|
770
|
+
padding: 0;
|
|
771
|
+
}
|
|
772
|
+
.chat-send-chevron:disabled {
|
|
773
|
+
opacity: 0.4;
|
|
774
|
+
cursor: not-allowed;
|
|
775
|
+
}
|
|
776
|
+
.chat-send-chevron:not(:disabled):hover {
|
|
777
|
+
background: #2563eb;
|
|
778
|
+
color: #fff;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* ── Send Options Dropdown ── */
|
|
782
|
+
.chat-send-menu {
|
|
783
|
+
position: absolute;
|
|
784
|
+
bottom: calc(100% + 6px);
|
|
785
|
+
right: 0;
|
|
786
|
+
min-width: 230px;
|
|
787
|
+
background: var(--tg-theme-bg-color, #0f0f23);
|
|
788
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
789
|
+
border-radius: 12px;
|
|
790
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
791
|
+
backdrop-filter: blur(16px);
|
|
792
|
+
padding: 4px;
|
|
793
|
+
z-index: 1010;
|
|
794
|
+
animation: agentDropIn 0.15s ease-out;
|
|
795
|
+
overflow: hidden;
|
|
796
|
+
}
|
|
797
|
+
.chat-send-menu-item {
|
|
798
|
+
display: flex;
|
|
799
|
+
align-items: center;
|
|
800
|
+
gap: 8px;
|
|
801
|
+
padding: 9px 12px;
|
|
802
|
+
width: 100%;
|
|
803
|
+
border: none;
|
|
804
|
+
border-radius: 8px;
|
|
805
|
+
background: transparent;
|
|
806
|
+
color: var(--tg-theme-text-color, #fff);
|
|
807
|
+
cursor: pointer;
|
|
808
|
+
font-size: 13px;
|
|
809
|
+
text-align: left;
|
|
810
|
+
transition: background 0.15s ease;
|
|
811
|
+
-webkit-tap-highlight-color: transparent;
|
|
812
|
+
line-height: 1.2;
|
|
813
|
+
}
|
|
814
|
+
.chat-send-menu-item:hover {
|
|
815
|
+
background: rgba(255,255,255,0.07);
|
|
816
|
+
}
|
|
817
|
+
.chat-send-menu-item.active {
|
|
818
|
+
background: rgba(59,130,246,0.15);
|
|
819
|
+
color: #93c5fd;
|
|
820
|
+
}
|
|
821
|
+
.chat-send-menu-item-icon {
|
|
822
|
+
font-size: 14px;
|
|
823
|
+
width: 18px;
|
|
824
|
+
text-align: center;
|
|
825
|
+
flex-shrink: 0;
|
|
826
|
+
}
|
|
827
|
+
.chat-send-menu-item-label {
|
|
828
|
+
flex: 1;
|
|
829
|
+
font-weight: 500;
|
|
830
|
+
}
|
|
831
|
+
.chat-send-menu-item-kbd {
|
|
832
|
+
display: inline-flex;
|
|
833
|
+
align-items: center;
|
|
834
|
+
padding: 1px 5px;
|
|
835
|
+
border-radius: 4px;
|
|
836
|
+
background: rgba(255,255,255,0.08);
|
|
837
|
+
border: 1px solid rgba(255,255,255,0.12);
|
|
838
|
+
font-size: 10px;
|
|
839
|
+
color: var(--tg-theme-hint-color, #999);
|
|
840
|
+
font-family: monospace;
|
|
841
|
+
white-space: nowrap;
|
|
842
|
+
flex-shrink: 0;
|
|
843
|
+
}
|
|
700
844
|
`;
|
|
701
845
|
|
|
702
846
|
let _agentStylesInjected = false;
|