Skip to content

Commit bcd2eb8

Browse files
committed
feat: add debug_tui configuration option and resolve thinking issues
1 parent 3f9c14e commit bcd2eb8

8 files changed

Lines changed: 71 additions & 54 deletions

File tree

src/plugin/config/loader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ function applyEnvOverrides(config: AntigravityConfig): AntigravityConfig {
129129
? env.OPENCODE_ANTIGRAVITY_DEBUG !== "0" && env.OPENCODE_ANTIGRAVITY_DEBUG !== "false"
130130
: config.debug,
131131

132+
// OPENCODE_ANTIGRAVITY_DEBUG_TUI=1
133+
debug_tui: env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "1" || env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "true"
134+
? true
135+
: config.debug_tui,
136+
132137
// OPENCODE_ANTIGRAVITY_LOG_DIR=/path/to/logs
133138
log_dir: env.OPENCODE_ANTIGRAVITY_LOG_DIR || config.log_dir,
134139

src/plugin/config/schema.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ export const AntigravityConfigSchema = z.object({
9696
* @default false
9797
*/
9898
debug: z.boolean().default(false),
99+
100+
/**
101+
* Show debug logs in the TUI log panel.
102+
* Requires `debug: true` to have any effect.
103+
* Env override: OPENCODE_ANTIGRAVITY_DEBUG_TUI=1
104+
* @default false
105+
*/
106+
debug_tui: z.boolean().default(false),
99107

100108
/**
101109
* Custom directory for debug logs.
@@ -435,6 +443,7 @@ export const DEFAULT_CONFIG: AntigravityConfig = {
435443
quiet_mode: false,
436444
toast_scope: 'root_only',
437445
debug: false,
446+
debug_tui: false,
438447
keep_thinking: false,
439448
session_recovery: true,
440449
auto_resume: true,

src/plugin/debug.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const DEBUG_MESSAGE_PREFIX = "[opencode-antigravity-auth debug]";
1717
interface DebugState {
1818
debugLevel: number;
1919
debugEnabled: boolean;
20+
debugTuiEnabled: boolean;
2021
verboseEnabled: boolean;
2122
logFilePath: string | undefined;
2223
logWriter: (line: string) => void;
@@ -101,6 +102,7 @@ export function initializeDebug(config: AntigravityConfig): void {
101102
const debugLevel = config.debug ? (envDebugFlag === "2" || envDebugFlag === "verbose" ? 2 : 1) : parseDebugLevel(envDebugFlag);
102103
const debugEnabled = debugLevel >= 1;
103104
const verboseEnabled = debugLevel >= 2;
105+
const debugTuiEnabled = debugEnabled && (config.debug_tui || env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "1" || env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "true");
104106
const logFilePath = debugEnabled ? createLogFilePath(config.log_dir) : undefined;
105107
const logWriter = createLogWriter(logFilePath);
106108

@@ -111,6 +113,7 @@ export function initializeDebug(config: AntigravityConfig): void {
111113
debugState = {
112114
debugLevel,
113115
debugEnabled,
116+
debugTuiEnabled,
114117
verboseEnabled,
115118
logFilePath,
116119
logWriter,
@@ -128,12 +131,14 @@ function getDebugState(): DebugState {
128131
const debugLevel = parseDebugLevel(envDebugFlag);
129132
const debugEnabled = debugLevel >= 1;
130133
const verboseEnabled = debugLevel >= 2;
134+
const debugTuiEnabled = debugEnabled && (env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "1" || env.OPENCODE_ANTIGRAVITY_DEBUG_TUI === "true");
131135
const logFilePath = debugEnabled ? createLogFilePath() : undefined;
132136
const logWriter = createLogWriter(logFilePath);
133137

134138
debugState = {
135139
debugLevel,
136140
debugEnabled,
141+
debugTuiEnabled,
137142
verboseEnabled,
138143
logFilePath,
139144
logWriter,
@@ -150,6 +155,10 @@ export function isDebugEnabled(): boolean {
150155
return getDebugState().debugEnabled;
151156
}
152157

158+
export function isDebugTuiEnabled(): boolean {
159+
return getDebugState().debugTuiEnabled;
160+
}
161+
153162
export function isVerboseEnabled(): boolean {
154163
return getDebugState().verboseEnabled;
155164
}

src/plugin/logger.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
* Logging behavior:
55
* - debug disabled → no logs anywhere
66
* - debug enabled → log files only (via debug.ts logWriter)
7-
* - debug enabled → log files + TUI log panel
7+
* - debug_tui enabled → log files + TUI log panel
88
* - OPENCODE_ANTIGRAVITY_CONSOLE_LOG=1 → console output (independent of debug flags)
99
*/
1010

1111
import type { PluginClient } from "./types";
12-
import { isDebugEnabled } from "./debug";
12+
import { isDebugTuiEnabled } from "./debug";
1313

1414
type LogLevel = "debug" | "info" | "warn" | "error";
1515

@@ -65,8 +65,8 @@ export function createLogger(module: string): Logger {
6565
const service = `antigravity.${module}`;
6666

6767
const log = (level: LogLevel, message: string, extra?: Record<string, unknown>): void => {
68-
// TUI logging: only when debug is enabled
69-
if (isDebugEnabled()) {
68+
// TUI logging: only when debug TUI is enabled
69+
if (isDebugTuiEnabled()) {
7070
const app = _client?.app;
7171
if (app && typeof app.log === "function") {
7272
app

src/plugin/recovery.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ describe("detectErrorType", () => {
7474
const error = { message: "Rate limit exceeded. Retry after 5s" };
7575
expect(detectErrorType(error)).toBeNull();
7676
});
77+
78+
it("returns null for generic INVALID_ARGUMENT with debug expected/found metadata", () => {
79+
const error = {
80+
message:
81+
"Request contains an invalid argument. [Debug Info] Requested Model: antigravity-claude-opus-4-6-thinking Tool Debug Summary: expected=1 found=0",
82+
};
83+
expect(detectErrorType(error)).toBeNull();
84+
});
7785
});
7886
});
7987

src/plugin/recovery.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ function extractMessageIndex(error: unknown): number | null {
8585
*/
8686
export function detectErrorType(error: unknown): RecoveryErrorType {
8787
const message = getErrorMessage(error);
88+
const hasExpectedFoundThinkingOrder =
89+
(message.includes("expected thinking") || message.includes("expected a thinking")) &&
90+
message.includes("found");
8891

8992
// tool_result_missing: Happens when ESC is pressed during tool execution
9093
if (message.includes("tool_use") && message.includes("tool_result")) {
@@ -97,7 +100,8 @@ export function detectErrorType(error: unknown): RecoveryErrorType {
97100
(message.includes("first block") ||
98101
message.includes("must start with") ||
99102
message.includes("preceeding") ||
100-
(message.includes("expected") && message.includes("found")))
103+
message.includes("preceding") ||
104+
hasExpectedFoundThinkingOrder)
101105
) {
102106
return "thinking_block_order";
103107
}

src/plugin/request.test.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -518,31 +518,6 @@ describe("request.ts", () => {
518518
});
519519

520520
describe("prepareAntigravityRequest", () => {
521-
it("copies thoughtSignature to all functionCall parts in the same turn", () => {
522-
const mockPayload = {
523-
contents: [
524-
{
525-
role: "model",
526-
parts: [
527-
{ functionCall: { name: "foo", args: {} }, thoughtSignature: "signature-123" },
528-
{ functionCall: { name: "bar", args: {} } }
529-
]
530-
}
531-
]
532-
};
533-
534-
const result = prepareAntigravityRequest(
535-
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro:generateContent",
536-
{ method: "POST", body: JSON.stringify(mockPayload) },
537-
"test-token",
538-
"test-project"
539-
);
540-
541-
const parsedBody = JSON.parse(result.init.body as string);
542-
expect(parsedBody.request.contents[0].parts[0].thoughtSignature).toBe("signature-123");
543-
expect(parsedBody.request.contents[0].parts[1].thoughtSignature).toBe("signature-123");
544-
});
545-
546521
const mockAccessToken = "test-token";
547522
const mockProjectId = "test-project";
548523

src/plugin/request.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,37 @@ function sanitizeRequestPayloadForAntigravity(payload: Record<string, unknown>):
369369

370370
const contentRecord = content as Record<string, unknown>;
371371
const rawParts = Array.isArray(contentRecord.parts) ? contentRecord.parts : [];
372-
const sanitizedParts = rawParts.filter(isValidRequestPart);
372+
let foundFirstFunctionCall = false;
373+
374+
const sanitizedParts = rawParts.filter(isValidRequestPart).map((part: any) => {
375+
if (part && typeof part === "object" && part.functionCall) {
376+
let sig = part.thoughtSignature || part.thought_signature;
377+
378+
// Only the first functionCall part in a block should have the signature.
379+
// If it's the first one and missing a valid signature, inject the sentinel
380+
// to prevent the API from rejecting the request with a 400 error.
381+
if (!foundFirstFunctionCall) {
382+
foundFirstFunctionCall = true;
383+
if (!sig || sig.length < MIN_SIGNATURE_LENGTH) {
384+
sig = SKIP_THOUGHT_SIGNATURE;
385+
}
386+
} else {
387+
// Parallel function calls MUST NOT have a signature
388+
sig = undefined;
389+
}
390+
391+
if (sig) {
392+
return { ...part, thought_signature: sig, thoughtSignature: sig };
393+
}
394+
395+
// If not the first part, just return the part without adding any signature keys
396+
const newPart = { ...part };
397+
delete newPart.thoughtSignature;
398+
delete newPart.thought_signature;
399+
return newPart;
400+
}
401+
return part;
402+
});
373403

374404
if (sanitizedParts.length === 0) {
375405
return null;
@@ -1237,29 +1267,6 @@ export function prepareAntigravityRequest(
12371267
const conversationKey = resolveConversationKey(requestPayload);
12381268
signatureSessionKey = buildSignatureSessionKey(PLUGIN_SESSION_ID, effectiveModel, conversationKey, resolveProjectKey(projectId));
12391269

1240-
// Ensure thoughtSignature is present on all functionCall parts if at least one part has it
1241-
// This is required by Gemini 3.1 Pro which otherwise fails with 400 Bad Request
1242-
if (Array.isArray(requestPayload.contents)) {
1243-
requestPayload.contents = requestPayload.contents.map((content: any) => {
1244-
if (!content || !Array.isArray(content.parts)) return content;
1245-
1246-
// Find if any part has a thoughtSignature
1247-
const signature = content.parts.find((p: any) => p && typeof p === "object" && typeof p.thoughtSignature === "string")?.thoughtSignature;
1248-
1249-
if (signature) {
1250-
const newParts = content.parts.map((p: any) => {
1251-
if (p && typeof p === "object" && p.functionCall && !p.thoughtSignature) {
1252-
return { ...p, thoughtSignature: signature };
1253-
}
1254-
return p;
1255-
});
1256-
return { ...content, parts: newParts };
1257-
}
1258-
1259-
return content;
1260-
});
1261-
}
1262-
12631270
// For Claude models, filter out unsigned thinking blocks (required by Claude API)
12641271
// Attempts to restore signatures from cache for multi-turn conversations
12651272
// Handle both Gemini-style contents[] and Anthropic-style messages[] payloads.

0 commit comments

Comments
 (0)