@bitkyc08/opencodex 2.65.0 → 2.66.0
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 -0
- package/gui/dist/assets/App-DyHlFeNB.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
- package/gui/dist/assets/index-BiwnVAUm.js +86 -0
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
// LOCAL PATCH (runturn-websearch): web-search sidecar loop for runTurn adapters.
|
|
2
|
+
//
|
|
3
|
+
// Upstream's runWithWebSearch (loop.ts) only drives the buildRequest/fetch/
|
|
4
|
+
// parseStream transport, so adapters that own their turn loop (devin, cursor,
|
|
5
|
+
// qoder, codebuddy) silently dropped the hosted `web_search` declaration. This
|
|
6
|
+
// module replays the same interception contract over the runTurn event stream:
|
|
7
|
+
// the synthetic `web_search` function tool is injected into each iteration's
|
|
8
|
+
// tool list, calls to it are intercepted and executed through the configured
|
|
9
|
+
// sidecar backend, and the assistant call + toolResult are appended to the
|
|
10
|
+
// OcxMessage history before the next runTurn dispatch. Everything else passes
|
|
11
|
+
// through untouched.
|
|
12
|
+
//
|
|
13
|
+
// The file is deliberately self-contained (small helpers duplicated from
|
|
14
|
+
// loop.ts instead of exported) so the patch stays a pure addition plus a few
|
|
15
|
+
// small call-site edits — easier to re-apply after `ocx update`.
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AdapterEvent,
|
|
19
|
+
OcxAssistantContentPart,
|
|
20
|
+
OcxMessage,
|
|
21
|
+
OcxParsedRequest,
|
|
22
|
+
OcxProviderConfig,
|
|
23
|
+
} from "../types";
|
|
24
|
+
import type { SidecarPlan } from "./index";
|
|
25
|
+
import { WEB_SEARCH_TOOL_NAME, buildWebSearchTool } from "./synthetic-tool";
|
|
26
|
+
import { hasVisibleAssistantText, scanEventsForWebSearch } from "./loop";
|
|
27
|
+
import { isTruncatedStopReason } from "../responses/truncated-stop-reason";
|
|
28
|
+
import {
|
|
29
|
+
runWebSearch,
|
|
30
|
+
type SidecarOutcome,
|
|
31
|
+
type SidecarOutcomeRecorder,
|
|
32
|
+
} from "./executor";
|
|
33
|
+
import { runAnthropicWebSearch } from "./anthropic-executor";
|
|
34
|
+
import { runXaiWebSearch } from "./xai-executor";
|
|
35
|
+
import { runGeminiWebSearch } from "./gemini-executor";
|
|
36
|
+
import { runExaWebSearch } from "./exa-executor";
|
|
37
|
+
import { formatWebSearchResults } from "./format-result";
|
|
38
|
+
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
39
|
+
import { redactSecretString } from "../lib/redact";
|
|
40
|
+
import { guardEmptyCompletionEventStream } from "../server/responses/empty-completion-guard";
|
|
41
|
+
import {
|
|
42
|
+
isTranslatorBudgetExceededError,
|
|
43
|
+
type TranslatorBudget,
|
|
44
|
+
} from "../lib/translator-budget";
|
|
45
|
+
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
46
|
+
|
|
47
|
+
export interface RunTurnWebSearchDeps {
|
|
48
|
+
parsed: OcxParsedRequest;
|
|
49
|
+
plan: SidecarPlan;
|
|
50
|
+
/** Required for the openai backend (ChatGPT forward path). */
|
|
51
|
+
forwardProvider?: OcxProviderConfig;
|
|
52
|
+
forwardHeaders?: Headers;
|
|
53
|
+
/** Operator key for the exa backend; read from config by the caller. */
|
|
54
|
+
exaApiKey?: string;
|
|
55
|
+
abortSignal?: AbortSignal;
|
|
56
|
+
recordSidecarOutcome?: SidecarOutcomeRecorder;
|
|
57
|
+
/** Preserve the request's opt-in empty-completion retry before the search cap. */
|
|
58
|
+
emptyCompletionRetry?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Optional request translator budget. When present the loop charges what it
|
|
61
|
+
* retains — each iteration's buffered event batch and the replay history it
|
|
62
|
+
* appends to messages — with its own local byte counters, and releases
|
|
63
|
+
* exactly those totals when the retention ends. It deliberately does NOT use
|
|
64
|
+
* the shared event-ownership WeakMap helpers: other owners (queue, bridge,
|
|
65
|
+
* output collector) may hold independent leases on the same event objects.
|
|
66
|
+
*/
|
|
67
|
+
translatorBudget?: TranslatorBudget;
|
|
68
|
+
/**
|
|
69
|
+
* Dispatch one more routed-model iteration. Iterations 2+ get a fresh event
|
|
70
|
+
* stream from the runTurn transport with the grown message history.
|
|
71
|
+
*/
|
|
72
|
+
dispatch: (iterParsed: OcxParsedRequest) => AsyncIterable<AdapterEvent>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parsed request for the FIRST iteration when the runTurn web-search loop is
|
|
77
|
+
* active: same request plus the synthetic tool so the model can ask to search.
|
|
78
|
+
*/
|
|
79
|
+
export function runTurnWebSearchInitialParsed(parsed: OcxParsedRequest): OcxParsedRequest {
|
|
80
|
+
return {
|
|
81
|
+
...parsed,
|
|
82
|
+
context: {
|
|
83
|
+
...parsed.context,
|
|
84
|
+
tools: [...(parsed.context.tools ?? []), buildWebSearchTool()],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- helpers duplicated from loop.ts (kept private there; see header note) ---
|
|
90
|
+
|
|
91
|
+
function normalizeQuery(q: string): string {
|
|
92
|
+
return q.trim().toLowerCase().replace(/\s+/g, " ");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Rebuild the assistant content parts that preceded a web_search call in this
|
|
97
|
+
* iteration: visible text AND thinking, in event order, so the replayed
|
|
98
|
+
* assistant message preserves everything the model emitted before the call.
|
|
99
|
+
* Thinking extraction mirrors loop.ts's rules (signed blocks keep their own
|
|
100
|
+
* signature, raw reasoning stays a separate unsigned part); text_delta parts
|
|
101
|
+
* are folded in as OcxTextContent — upstream's thinking-only version silently
|
|
102
|
+
* dropped visible text that preceded a tool call.
|
|
103
|
+
*/
|
|
104
|
+
function extractIterationContent(events: AdapterEvent[]): OcxAssistantContentPart[] {
|
|
105
|
+
const parts: OcxAssistantContentPart[] = [];
|
|
106
|
+
let text = "";
|
|
107
|
+
let thinking = "";
|
|
108
|
+
let signature: string | undefined;
|
|
109
|
+
let rawReasoning = "";
|
|
110
|
+
const flushText = () => {
|
|
111
|
+
if (!text) return;
|
|
112
|
+
parts.push({ type: "text", text });
|
|
113
|
+
text = "";
|
|
114
|
+
};
|
|
115
|
+
const flushVisible = () => {
|
|
116
|
+
if (!thinking && !signature) return;
|
|
117
|
+
parts.push({ type: "thinking", thinking, ...(signature ? { signature } : {}) });
|
|
118
|
+
thinking = "";
|
|
119
|
+
signature = undefined;
|
|
120
|
+
};
|
|
121
|
+
const flushRaw = () => {
|
|
122
|
+
if (!rawReasoning) return;
|
|
123
|
+
parts.push({ type: "thinking", thinking: rawReasoning });
|
|
124
|
+
rawReasoning = "";
|
|
125
|
+
};
|
|
126
|
+
for (const e of events) {
|
|
127
|
+
if (e.type === "thinking_delta") {
|
|
128
|
+
flushText();
|
|
129
|
+
flushRaw();
|
|
130
|
+
thinking += e.thinking;
|
|
131
|
+
} else if (e.type === "reasoning_raw_delta") {
|
|
132
|
+
flushText();
|
|
133
|
+
flushVisible();
|
|
134
|
+
rawReasoning += e.text;
|
|
135
|
+
} else if (e.type === "thinking_signature") {
|
|
136
|
+
signature = e.signature;
|
|
137
|
+
flushVisible();
|
|
138
|
+
} else if (e.type === "redacted_thinking") {
|
|
139
|
+
flushText();
|
|
140
|
+
flushVisible();
|
|
141
|
+
flushRaw();
|
|
142
|
+
parts.push({ type: "thinking", thinking: "", redacted: [e.data] });
|
|
143
|
+
} else if (e.type === "text_delta") {
|
|
144
|
+
flushVisible();
|
|
145
|
+
flushRaw();
|
|
146
|
+
text += e.text;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
flushText();
|
|
150
|
+
flushVisible();
|
|
151
|
+
flushRaw();
|
|
152
|
+
return parts;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function forcedAnswerNudge(): OcxMessage {
|
|
156
|
+
return {
|
|
157
|
+
role: "developer",
|
|
158
|
+
content:
|
|
159
|
+
"Answer the user's question now using the web search results already gathered above. " +
|
|
160
|
+
"Ground your answer in what those results actually say, and reference the relevant sources " +
|
|
161
|
+
"when they are available. Do not claim you lack information that the results contain, and do " +
|
|
162
|
+
"not invent sources that were not returned.",
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Transient developer-role nudge for the ONE recovery pass after a forced
|
|
169
|
+
* answer came back empty. The recovery also removes every tool (toolChoice:
|
|
170
|
+
* "none"), so the model has nothing to call and can only return text.
|
|
171
|
+
*/
|
|
172
|
+
function forcedAnswerRetryNudge(): OcxMessage {
|
|
173
|
+
return {
|
|
174
|
+
role: "developer",
|
|
175
|
+
content:
|
|
176
|
+
"Your previous response contained no usable answer. Web search has finished for this turn and " +
|
|
177
|
+
"no tools are available for this response. Answer the user's question now in assistant text, " +
|
|
178
|
+
"using the web search results already gathered above. If those results are insufficient, say " +
|
|
179
|
+
"what is missing instead of returning an empty response.",
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Drive a runTurn adapter through the web-search loop. `first` is iteration
|
|
186
|
+
* 0's already-dispatched event stream (built from
|
|
187
|
+
* runTurnWebSearchInitialParsed); later iterations go through deps.dispatch.
|
|
188
|
+
* Intermediate iterations emit only heartbeats and the search cell begin/end
|
|
189
|
+
* pair — their text/thinking is folded back into the replayed history instead
|
|
190
|
+
* of reaching the client mid-turn, matching the fetch-path loop's ordering.
|
|
191
|
+
*/
|
|
192
|
+
export async function* runTurnWebSearchLoop(
|
|
193
|
+
first: AsyncIterable<AdapterEvent>,
|
|
194
|
+
deps: RunTurnWebSearchDeps,
|
|
195
|
+
): AsyncGenerator<AdapterEvent> {
|
|
196
|
+
const { parsed, plan, abortSignal } = deps;
|
|
197
|
+
const messages: OcxMessage[] = [...parsed.context.messages];
|
|
198
|
+
const allTools = [...(parsed.context.tools ?? []), buildWebSearchTool()];
|
|
199
|
+
const toolsNoWebSearch = (parsed.context.tools ?? []).filter(t => !t.webSearch);
|
|
200
|
+
const failedQueries = new Set<string>();
|
|
201
|
+
let searchesExecuted = 0;
|
|
202
|
+
let executedSearchCount = 0;
|
|
203
|
+
const HARD_CAP = plan.maxSearches + 3;
|
|
204
|
+
let source = first;
|
|
205
|
+
const loopT0 = Date.now();
|
|
206
|
+
|
|
207
|
+
// Translator-budget ownership (deps.translatorBudget): the loop charges only
|
|
208
|
+
// what it retains, tracks both totals locally, and releases exactly those
|
|
209
|
+
// amounts — never a shared stage total another owner may also be releasing
|
|
210
|
+
// from. Iteration event buffers release when the iteration's events are
|
|
211
|
+
// consumed; the replay history appended to messages stays charged until the
|
|
212
|
+
// loop exits because every later dispatch rebuilds its request from it.
|
|
213
|
+
// Event objects are NOT leased through the shared ownership WeakMap: the
|
|
214
|
+
// queue, the bridge, and the buffered-path output collector may each hold
|
|
215
|
+
// their own independent accounting on the same objects.
|
|
216
|
+
let bufferedBytes = 0;
|
|
217
|
+
let historyBytes = 0;
|
|
218
|
+
const chargeBuffered = (event: AdapterEvent): void => {
|
|
219
|
+
if (!deps.translatorBudget) return;
|
|
220
|
+
// Same per-event convention as retainTranslatedEvent: the first event owns
|
|
221
|
+
// the array brackets, later events own their separator.
|
|
222
|
+
const bytes = jsonUtf8Bytes(event) + (bufferedBytes === 0 ? 2 : 1);
|
|
223
|
+
deps.translatorBudget.chargeRetained(bytes, { kind: "retained_collectors" });
|
|
224
|
+
bufferedBytes += bytes;
|
|
225
|
+
};
|
|
226
|
+
const releaseBuffered = (): void => {
|
|
227
|
+
if (!deps.translatorBudget || bufferedBytes === 0) return;
|
|
228
|
+
deps.translatorBudget.releaseRetained(bufferedBytes, { kind: "retained_collectors" });
|
|
229
|
+
bufferedBytes = 0;
|
|
230
|
+
};
|
|
231
|
+
const chargeHistory = (message: OcxMessage): void => {
|
|
232
|
+
if (!deps.translatorBudget) return;
|
|
233
|
+
const bytes = jsonUtf8Bytes(message) + 1;
|
|
234
|
+
deps.translatorBudget.chargeRetained(bytes, { kind: "request_copies" });
|
|
235
|
+
historyBytes += bytes;
|
|
236
|
+
};
|
|
237
|
+
const releaseHistory = (): void => {
|
|
238
|
+
if (!deps.translatorBudget || historyBytes === 0) return;
|
|
239
|
+
deps.translatorBudget.releaseRetained(historyBytes, { kind: "request_copies" });
|
|
240
|
+
historyBytes = 0;
|
|
241
|
+
};
|
|
242
|
+
// Shared budget-failure convention (loop.ts): a 502 upstream_error carrying
|
|
243
|
+
// the translation_buffer_limit code.
|
|
244
|
+
const budgetErrorEvent = (): AdapterEvent => ({
|
|
245
|
+
type: "error",
|
|
246
|
+
status: 502,
|
|
247
|
+
errorType: "upstream_error",
|
|
248
|
+
code: "translation_buffer_limit",
|
|
249
|
+
message: "upstream translation buffer exceeded the safe limit",
|
|
250
|
+
});
|
|
251
|
+
const appendMessage = (message: OcxMessage): void => {
|
|
252
|
+
chargeHistory(message);
|
|
253
|
+
messages.push(message);
|
|
254
|
+
};
|
|
255
|
+
const liveTypes = new Set<AdapterEvent["type"]>([
|
|
256
|
+
"text_delta", "thinking_delta", "reasoning_raw_delta", "thinking_signature",
|
|
257
|
+
"redacted_thinking", "kiro_redacted_reasoning",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
const executeQuery = async (query: string, signal: AbortSignal | undefined): Promise<SidecarOutcome> => {
|
|
261
|
+
try {
|
|
262
|
+
switch (plan.backend) {
|
|
263
|
+
case "anthropic":
|
|
264
|
+
return plan.anthropicSidecar
|
|
265
|
+
? await runAnthropicWebSearch(query, plan.anthropicSidecar.providerName, plan.anthropicSidecar.provider, plan.settings, signal)
|
|
266
|
+
: { text: "", sources: [], error: "anthropic backend selected without a resolved sidecar provider" };
|
|
267
|
+
case "xai":
|
|
268
|
+
return plan.xaiSidecar
|
|
269
|
+
? await runXaiWebSearch(query, plan.xaiSidecar.providerName, plan.xaiSidecar.provider, plan.settings, plan.xaiSearchOptions ?? {}, signal)
|
|
270
|
+
: { text: "", sources: [], error: "xai backend selected without a resolved Grok OAuth provider" };
|
|
271
|
+
case "gemini":
|
|
272
|
+
return plan.geminiSidecar
|
|
273
|
+
? await runGeminiWebSearch(query, plan.geminiSidecar.providerName, plan.geminiSidecar.provider, plan.settings, signal)
|
|
274
|
+
: { text: "", sources: [], error: "gemini backend selected without a resolved Antigravity provider" };
|
|
275
|
+
case "exa":
|
|
276
|
+
return deps.exaApiKey
|
|
277
|
+
? await runExaWebSearch(query, deps.exaApiKey, plan.settings, signal)
|
|
278
|
+
: { text: "", sources: [], error: "exa backend selected without an exaApiKey" };
|
|
279
|
+
default:
|
|
280
|
+
return deps.forwardProvider
|
|
281
|
+
? await runWebSearch(query, plan.hostedTool, deps.forwardProvider, deps.forwardHeaders ?? new Headers(), plan.settings, signal, deps.recordSidecarOutcome)
|
|
282
|
+
: { text: "", sources: [], error: "openai backend selected without a resolved forward sidecar" };
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {
|
|
285
|
+
return { text: "", sources: [], error: `sidecar failed: ${redactSecretString(e instanceof Error ? e.message : String(e))}` };
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
async function* runSearchCall(
|
|
290
|
+
call: { id: string; queries: string[]; providerMetadata?: Parameters<typeof cloneProviderOpaqueToolCallMetadata>[0] },
|
|
291
|
+
precedingContent: OcxAssistantContentPart[],
|
|
292
|
+
): AsyncGenerator<AdapterEvent> {
|
|
293
|
+
const results: { query: string; outcome: SidecarOutcome }[] = [];
|
|
294
|
+
let beganCell = false;
|
|
295
|
+
if (call.queries.length === 0) {
|
|
296
|
+
searchesExecuted++;
|
|
297
|
+
results.push({ query: "", outcome: { text: "", sources: [], error: "the model called web_search with an empty query" } });
|
|
298
|
+
}
|
|
299
|
+
for (const query of call.queries) {
|
|
300
|
+
if (abortSignal?.aborted) {
|
|
301
|
+
if (beganCell) yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
yield { type: "heartbeat" };
|
|
305
|
+
if (abortSignal?.aborted) {
|
|
306
|
+
if (beganCell) yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
let outcome: SidecarOutcome;
|
|
310
|
+
if (failedQueries.has(normalizeQuery(query))) {
|
|
311
|
+
outcome = { text: "", sources: [], error: "this query already failed earlier in the turn — do not call web_search again for it; answer from existing context" };
|
|
312
|
+
} else if (searchesExecuted >= plan.maxSearches) {
|
|
313
|
+
outcome = { text: "", sources: [], error: "web search limit reached for this turn — answer from results already gathered" };
|
|
314
|
+
} else {
|
|
315
|
+
if (!beganCell) {
|
|
316
|
+
beganCell = true;
|
|
317
|
+
yield { type: "web_search_call_begin", id: call.id };
|
|
318
|
+
}
|
|
319
|
+
if (abortSignal?.aborted) {
|
|
320
|
+
yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
outcome = await executeQuery(query, abortSignal);
|
|
324
|
+
if (abortSignal?.aborted) {
|
|
325
|
+
// The client already saw this cell's begin — close it so the UI never
|
|
326
|
+
// shows a "Searching the web" spinner that never resolves.
|
|
327
|
+
yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: "failed" };
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
searchesExecuted++;
|
|
331
|
+
executedSearchCount++;
|
|
332
|
+
if (outcome.error) failedQueries.add(normalizeQuery(query));
|
|
333
|
+
}
|
|
334
|
+
results.push({ query, outcome });
|
|
335
|
+
}
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const callArgs: Record<string, unknown> = call.queries.length > 1
|
|
338
|
+
? { queries: call.queries }
|
|
339
|
+
: { query: call.queries[0] ?? "" };
|
|
340
|
+
appendMessage({
|
|
341
|
+
role: "assistant",
|
|
342
|
+
content: [
|
|
343
|
+
...precedingContent,
|
|
344
|
+
{
|
|
345
|
+
type: "toolCall" as const,
|
|
346
|
+
id: call.id,
|
|
347
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
348
|
+
arguments: callArgs,
|
|
349
|
+
...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
|
|
350
|
+
? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
|
|
351
|
+
: {}),
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
timestamp: now,
|
|
355
|
+
});
|
|
356
|
+
const allFailed = results.every(r => !!r.outcome.error);
|
|
357
|
+
appendMessage({
|
|
358
|
+
role: "toolResult", toolCallId: call.id, toolName: WEB_SEARCH_TOOL_NAME,
|
|
359
|
+
content: formatWebSearchResults(results, !!parsed._structuredOutput),
|
|
360
|
+
isError: allFailed, timestamp: now,
|
|
361
|
+
});
|
|
362
|
+
if (beganCell) {
|
|
363
|
+
const anySuccess = results.some(r => !r.outcome.error);
|
|
364
|
+
const sources: { url: string; title?: string }[] = [];
|
|
365
|
+
const seenSrc = new Set<string>();
|
|
366
|
+
for (const r of results) {
|
|
367
|
+
for (const s of r.outcome.sources) {
|
|
368
|
+
if (seenSrc.has(s.url)) continue;
|
|
369
|
+
seenSrc.add(s.url);
|
|
370
|
+
sources.push(s.title ? { url: s.url, title: s.title } : { url: s.url });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
yield {
|
|
374
|
+
type: "web_search_call_end", id: call.id,
|
|
375
|
+
queries: call.queries,
|
|
376
|
+
status: anySuccess ? "completed" : "failed",
|
|
377
|
+
...(sources.length > 0 ? { sources } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const abortEvent = (): AdapterEvent => ({
|
|
383
|
+
type: "error",
|
|
384
|
+
message: "client closed request during web-search",
|
|
385
|
+
});
|
|
386
|
+
const logDone = (iterations: number): void => {
|
|
387
|
+
if (executedSearchCount === 0) return;
|
|
388
|
+
const failedCount = failedQueries.size;
|
|
389
|
+
console.warn(
|
|
390
|
+
`[web-search-runturn] done — ${executedSearchCount} search${executedSearchCount > 1 ? "es" : ""}`
|
|
391
|
+
+ (failedCount > 0 ? ` (${failedCount} failed)` : "")
|
|
392
|
+
+ `, ${iterations + 1} iteration${iterations > 0 ? "s" : ""}, ${Date.now() - loopT0}ms`,
|
|
393
|
+
);
|
|
394
|
+
};
|
|
395
|
+
let emptyAnswerRetries = 0;
|
|
396
|
+
let ordinaryEmptyRetries = 0;
|
|
397
|
+
let currentParsed = runTurnWebSearchInitialParsed(parsed);
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
for (let i = 0; i < HARD_CAP; i++) {
|
|
401
|
+
releaseBuffered();
|
|
402
|
+
if (abortSignal?.aborted) {
|
|
403
|
+
yield abortEvent();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const events: AdapterEvent[] = [];
|
|
407
|
+
let liveWindowOpen = plan.streamRoutedModelOutput;
|
|
408
|
+
let streamedCount = 0;
|
|
409
|
+
// One identical-turn retry per request: once it is spent the guard is
|
|
410
|
+
// not re-applied, so a later empty terminal reaches the loop itself —
|
|
411
|
+
// at the search cap the forced-answer recovery below handles it, the
|
|
412
|
+
// same contract loop.ts keeps by running no inner guard at all. A
|
|
413
|
+
// maxRetries:0 wrap would instead convert that empty done into
|
|
414
|
+
// empty_completion_retry_failed and end the turn before recovery.
|
|
415
|
+
if (deps.emptyCompletionRetry && searchesExecuted < plan.maxSearches
|
|
416
|
+
&& ordinaryEmptyRetries === 0) {
|
|
417
|
+
source = guardEmptyCompletionEventStream({
|
|
418
|
+
firstEvents: source,
|
|
419
|
+
continuation: () => {
|
|
420
|
+
ordinaryEmptyRetries++;
|
|
421
|
+
return deps.dispatch(currentParsed);
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
for await (const e of source) {
|
|
427
|
+
if (abortSignal?.aborted) {
|
|
428
|
+
yield abortEvent();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (e.type === "error") {
|
|
432
|
+
yield e;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (e.type === "heartbeat") {
|
|
436
|
+
yield e;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
chargeBuffered(e);
|
|
440
|
+
events.push(e);
|
|
441
|
+
if (liveWindowOpen && liveTypes.has(e.type)) {
|
|
442
|
+
streamedCount++;
|
|
443
|
+
yield e;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
liveWindowOpen = false;
|
|
447
|
+
// Buffered text/reasoning/tool deltas are upstream activity too. Keep
|
|
448
|
+
// the bridge alive without exposing the synthetic call to the client.
|
|
449
|
+
if (e.type !== "done" && e.type !== "incomplete") yield { type: "heartbeat" };
|
|
450
|
+
}
|
|
451
|
+
} catch (e) {
|
|
452
|
+
if (isTranslatorBudgetExceededError(e)) throw e;
|
|
453
|
+
yield { type: "error", message: e instanceof Error ? e.message : String(e) };
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const terminals = events.filter(e => e.type === "done" || e.type === "incomplete");
|
|
458
|
+
if (terminals.length !== 1 || terminals[0] !== events.at(-1)) {
|
|
459
|
+
yield { type: "error", status: 502, errorType: "upstream_error",
|
|
460
|
+
message: "web-search runTurn stream must end with exactly one terminal event" };
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const split = scanEventsForWebSearch(events);
|
|
464
|
+
const terminal = terminals[0];
|
|
465
|
+
const replayEvents = split.passthrough.slice(streamedCount);
|
|
466
|
+
if (terminal.type === "incomplete") {
|
|
467
|
+
for (const e of replayEvents) yield e;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const forceAnswer = searchesExecuted >= plan.maxSearches;
|
|
471
|
+
// Loop only when the model's actionable output is purely web_search calls:
|
|
472
|
+
// a real tool call belongs to Codex, and a budget-exhausted turn must
|
|
473
|
+
// answer from what it already gathered.
|
|
474
|
+
const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall && !forceAnswer;
|
|
475
|
+
if (!shouldLoop) {
|
|
476
|
+
// A forced-answer pass that ends `done` must have produced usable output —
|
|
477
|
+
// never a malformed tool call, and never silence. A truncated/refusal
|
|
478
|
+
// stop is authoritative and replays as-is; an empty one gets exactly one
|
|
479
|
+
// recovery pass with all tools removed (same contract as loop.ts #1001).
|
|
480
|
+
if (forceAnswer) {
|
|
481
|
+
const terminalEvent = split.passthrough.find(event => event.type === "done");
|
|
482
|
+
if (terminalEvent?.type === "done" && !split.hasMalformedToolCall
|
|
483
|
+
&& isTruncatedStopReason(terminalEvent.stopReason)) {
|
|
484
|
+
logDone(i);
|
|
485
|
+
for (const e of replayEvents) yield e;
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (terminalEvent?.type === "done"
|
|
489
|
+
&& (split.hasMalformedToolCall
|
|
490
|
+
|| (!split.hasRealToolCall && !hasVisibleAssistantText(split.passthrough)))) {
|
|
491
|
+
console.warn("[web-search-runturn] unusable forced answer", JSON.stringify({
|
|
492
|
+
model: parsed.modelId,
|
|
493
|
+
recoveryAttempt: emptyAnswerRetries,
|
|
494
|
+
searchCalls: split.calls.length,
|
|
495
|
+
malformed: split.hasMalformedToolCall,
|
|
496
|
+
stopReason: terminalEvent.stopReason,
|
|
497
|
+
eventTypes: [...new Set(split.passthrough.map(event => event.type))],
|
|
498
|
+
}));
|
|
499
|
+
if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
|
|
500
|
+
emptyAnswerRetries++;
|
|
501
|
+
console.warn("[web-search-runturn] empty forced answer — retrying once without tools");
|
|
502
|
+
yield { type: "heartbeat" };
|
|
503
|
+
if (abortSignal?.aborted) { yield abortEvent(); return; }
|
|
504
|
+
source = deps.dispatch({
|
|
505
|
+
...parsed,
|
|
506
|
+
options: { ...parsed.options, toolChoice: "none" as const },
|
|
507
|
+
context: {
|
|
508
|
+
...parsed.context,
|
|
509
|
+
messages: [
|
|
510
|
+
...messages,
|
|
511
|
+
...(executedSearchCount > 0 ? [forcedAnswerNudge()] : []),
|
|
512
|
+
forcedAnswerRetryNudge(),
|
|
513
|
+
],
|
|
514
|
+
tools: [],
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
yield {
|
|
520
|
+
type: "error",
|
|
521
|
+
status: 502,
|
|
522
|
+
errorType: "upstream_error",
|
|
523
|
+
message: "forced-answer pass produced no usable assistant output",
|
|
524
|
+
};
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
logDone(i);
|
|
529
|
+
for (const e of replayEvents) yield e;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const iterationContent = extractIterationContent(split.passthrough);
|
|
534
|
+
for (const [callIndex, call] of split.calls.entries()) {
|
|
535
|
+
if (abortSignal?.aborted) { yield abortEvent(); return; }
|
|
536
|
+
yield* runSearchCall(call, callIndex === 0 ? iterationContent : []);
|
|
537
|
+
}
|
|
538
|
+
if (abortSignal?.aborted) {
|
|
539
|
+
yield abortEvent();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const nextForceAnswer = searchesExecuted >= plan.maxSearches;
|
|
544
|
+
const iterParsed: OcxParsedRequest = {
|
|
545
|
+
...parsed,
|
|
546
|
+
context: {
|
|
547
|
+
...parsed.context,
|
|
548
|
+
messages: nextForceAnswer && executedSearchCount > 0
|
|
549
|
+
? [...messages, forcedAnswerNudge()]
|
|
550
|
+
: messages,
|
|
551
|
+
tools: nextForceAnswer ? toolsNoWebSearch : allTools,
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
currentParsed = iterParsed;
|
|
555
|
+
source = deps.dispatch(iterParsed);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Safety net: the hard cap should be unreachable (forceAnswer ends the loop
|
|
559
|
+
// first), but never hang a client stream if an adapter misbehaves.
|
|
560
|
+
yield { type: "error", message: "web-search runTurn loop exceeded its iteration cap" };
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (!isTranslatorBudgetExceededError(error)) throw error;
|
|
563
|
+
yield budgetErrorEvent();
|
|
564
|
+
} finally {
|
|
565
|
+
releaseBuffered();
|
|
566
|
+
releaseHistory();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.codex-account-controls{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:12px;min-width:0;display:flex}.codex-account-auto-switch{flex:none;align-items:center;gap:7px;min-width:0;display:flex}.codex-account-auto-switch-label{font-size:var(--text-label);color:var(--muted);font-weight:var(--weight-medium);white-space:nowrap}.codex-account-auto-switch-input-wrap{align-items:stretch;display:inline-flex;position:relative}.codex-account-auto-switch-input{width:76px;height:30px;font-size:var(--text-label);text-align:right;padding:4px 38px 4px 8px}.codex-account-auto-switch-unit{color:var(--muted);font-size:var(--text-label);pointer-events:none;position:absolute;top:50%;right:25px;transform:translateY(-50%)}.codex-account-auto-switch-input-wrap .ocx-stepper{width:22px}.codex-account-auto-switch-toggle{transform-origin:50%;margin-inline:-3px;transform:scale(.8)}.quota-summary-bar{z-index:var(--z-sticky);border-bottom:1px solid var(--border);background:var(--bg);min-height:34px;font-size:var(--text-label);align-items:center;gap:12px;padding:4px 16px;display:flex;position:sticky;top:0}.quota-summary-list{flex-wrap:wrap;flex:1;align-items:center;gap:4px;min-width:0;margin:0;padding:0;list-style:none;display:flex}.quota-summary-item{position:relative}.quota-summary-chip{color:var(--text);font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:999px;align-items:center;gap:6px;padding:3px 8px;display:inline-flex}.quota-summary-chip:hover,.quota-summary-chip[aria-expanded=true]{border-color:var(--border)}.quota-summary-chip:focus-visible{outline:2px solid var(--text);outline-offset:1px}.quota-summary-name{color:var(--muted)}.quota-summary-pct{font-variant-numeric:tabular-nums;font-weight:600}.quota-summary-flag{width:14px;height:14px;color:var(--bg);font-size:var(--text-micro);border-radius:50%;place-items:center;font-weight:700;display:inline-grid}.quota-summary-item--warn .quota-summary-chip{background:var(--amber-soft)}.quota-summary-item--warn .quota-summary-pct{color:var(--amber)}.quota-summary-item--warn .quota-summary-flag{background:var(--amber)}.quota-summary-item--critical .quota-summary-chip{border-color:var(--red);background:var(--red-soft)}.quota-summary-item--critical .quota-summary-pct{color:var(--red)}.quota-summary-item--critical .quota-summary-flag{background:var(--red)}.quota-summary-popover{z-index:var(--z-popover);border:1px solid var(--border);background:var(--surface);border-radius:8px;min-width:260px;padding:10px 12px;position:absolute;top:calc(100% + 4px);left:0;box-shadow:0 8px 24px #00000024}.quota-summary-popover-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;display:flex}.quota-summary-badge{font-size:var(--text-caption);border-radius:999px;padding:1px 6px}.quota-summary-badge--warn{color:var(--amber);background:var(--amber-soft)}.quota-summary-badge--critical{color:var(--red);background:var(--red-soft)}.quota-summary-table{border-collapse:collapse;width:100%}.quota-summary-table th,.quota-summary-table td{text-align:left;white-space:nowrap;padding:3px 0;font-weight:400}.quota-summary-table td.quota-summary-row-pct{font-variant-numeric:tabular-nums;text-align:right;white-space:nowrap;padding:3px 12px;font-weight:600}.quota-summary-row-reset{color:var(--muted);white-space:nowrap}.quota-summary-table th[scope=row]{white-space:normal;overflow-wrap:anywhere}.quota-summary-row--warn .quota-summary-row-pct{color:var(--amber)}.quota-summary-row--critical .quota-summary-row-pct{color:var(--red)}.quota-summary-popover-foot{color:var(--muted);font-size:var(--text-caption);margin-top:6px}.quota-summary-updated{color:var(--muted);font-size:var(--text-caption);white-space:nowrap;flex-shrink:0}.quota-summary-updated--stale{color:var(--amber)}.main:has(>.quota-summary-bar):has(>.main-inner--combos .combos-workspace-shell){flex-direction:column;height:100dvh;display:flex}.main:has(>.quota-summary-bar)>.main-inner.main-inner--combos:has(.combos-workspace-shell){flex:auto;height:auto;min-height:0}@media (width<=760px){.quota-summary-bar{padding:4px 10px;position:static}.quota-summary-popover{min-width:0;margin-top:4px;position:fixed;top:auto;left:8px;right:8px}.main:has(>.quota-summary-bar):has(>.main-inner--combos .combos-workspace-shell){height:100%;min-height:0}}
|