bingocode 1.1.200-beta.2 → 1.1.200-beta.21
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/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +813 -813
- package/src/commands/brain/brain.ts +4 -9
- package/src/components/PromptInput/PromptInput.tsx +1 -1
- package/src/constants/prompts.ts +31 -19
- package/src/hooks/useMergedTools.ts +3 -1
- package/src/main.tsx +4693 -4691
- package/src/manager/CliMenuManager.tsx +1589 -1583
- package/src/screens/REPL.tsx +5010 -5008
- package/src/screens/ResumeConversation.tsx +7 -1
- package/src/server/api/providers.ts +24 -0
- package/src/server/config/providers.yaml +21 -0
- package/src/server/proxy/handler.ts +16 -3
- package/src/server/services/providerService.ts +189 -50
- package/src/tools/AgentTool/UI.tsx +73 -8
- package/src/tools/AgentTool/agentToolUtils.ts +2 -2
- package/src/tools.ts +397 -390
- package/src/types/logs.ts +1 -0
- package/src/utils/attachments.ts +3 -2
- package/src/utils/conversationRecovery.ts +1 -0
- package/src/utils/sessionRestore.ts +2 -0
- package/src/utils/sessionStorage.ts +5180 -5105
- package/src/utils/tokens.ts +23 -0
|
@@ -100,6 +100,7 @@ export function ResumeConversation({
|
|
|
100
100
|
agentName?: string;
|
|
101
101
|
agentColor?: AgentColorName;
|
|
102
102
|
mainThreadAgentDefinition?: AgentDefinition;
|
|
103
|
+
brainMode?: boolean;
|
|
103
104
|
} | null>(null);
|
|
104
105
|
const [crossProjectCommand, setCrossProjectCommand] = React.useState<string | null>(null);
|
|
105
106
|
const sessionLogResultRef = React.useRef<SessionLogResult | null>(null);
|
|
@@ -250,6 +251,10 @@ export function ResumeConversation({
|
|
|
250
251
|
standaloneAgentContext
|
|
251
252
|
}));
|
|
252
253
|
}
|
|
254
|
+
setAppState(prev_2 => ({
|
|
255
|
+
...prev_2,
|
|
256
|
+
brainMode: result_3.brainMode ?? false
|
|
257
|
+
}));
|
|
253
258
|
void updateSessionName(result_3.agentName);
|
|
254
259
|
restoreSessionMetadata(forkSession ? {
|
|
255
260
|
...result_3,
|
|
@@ -279,7 +284,8 @@ export function ResumeConversation({
|
|
|
279
284
|
contentReplacements: result_3.contentReplacements,
|
|
280
285
|
agentName: result_3.agentName,
|
|
281
286
|
agentColor: (result_3.agentColor === 'default' ? undefined : result_3.agentColor) as AgentColorName | undefined,
|
|
282
|
-
mainThreadAgentDefinition: resolvedAgentDef
|
|
287
|
+
mainThreadAgentDefinition: resolvedAgentDef,
|
|
288
|
+
brainMode: result_3.brainMode ?? false
|
|
283
289
|
});
|
|
284
290
|
} catch (e) {
|
|
285
291
|
logEvent('tengu_session_resumed', {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import type { SlotName } from '../types/provider.js'
|
|
26
26
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
const providerService = new ProviderService()
|
|
29
30
|
providerService.init().catch((err) =>
|
|
30
31
|
console.error('[ProviderService] init failed:', err),
|
|
@@ -42,6 +43,22 @@ function sanitizeProvider(provider: Record<string, unknown>): Record<string, unk
|
|
|
42
43
|
return provider
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function isLocalhostRequest(req: Request): boolean {
|
|
47
|
+
const origin = req.headers.get('Origin')
|
|
48
|
+
const host = req.headers.get('Host')
|
|
49
|
+
const isLocalOrigin =
|
|
50
|
+
!!origin &&
|
|
51
|
+
(origin.startsWith('http://localhost') ||
|
|
52
|
+
origin.startsWith('http://127.0.0.1') ||
|
|
53
|
+
origin.startsWith('http://[::1]'))
|
|
54
|
+
const isLocalHost =
|
|
55
|
+
!!host &&
|
|
56
|
+
(host.startsWith('localhost') ||
|
|
57
|
+
host.startsWith('127.0.0.1') ||
|
|
58
|
+
host.startsWith('[::1]'))
|
|
59
|
+
return isLocalOrigin || isLocalHost
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
export async function handleProvidersApi(
|
|
46
63
|
req: Request,
|
|
47
64
|
_url: URL,
|
|
@@ -81,6 +98,13 @@ export async function handleProvidersApi(
|
|
|
81
98
|
|
|
82
99
|
// /api/providers/link-vscode — manage VS Code linking
|
|
83
100
|
if (id === 'link-vscode' && !action) {
|
|
101
|
+
// Defense-in-depth: deny requests from non-localhost origins unconditionally
|
|
102
|
+
if (!isLocalhostRequest(req)) {
|
|
103
|
+
return Response.json(
|
|
104
|
+
{ error: 'Forbidden', message: 'link-vscode only accessible from localhost' },
|
|
105
|
+
{ status: 403 },
|
|
106
|
+
)
|
|
107
|
+
}
|
|
84
108
|
if (req.method === 'GET') {
|
|
85
109
|
const status = await providerService.getVscodeStatus()
|
|
86
110
|
return Response.json(status)
|
|
@@ -118,6 +118,27 @@ presets:
|
|
|
118
118
|
secret: true
|
|
119
119
|
placeholder: 'sk-...'
|
|
120
120
|
|
|
121
|
+
- id: nvidia
|
|
122
|
+
name: NVIDIA API
|
|
123
|
+
baseUrl: https://integrate.api.nvidia.com/v1
|
|
124
|
+
apiFormat: openai_chat
|
|
125
|
+
needsApiKey: true
|
|
126
|
+
websiteUrl: https://build.nvidia.com/explore/discover
|
|
127
|
+
modelsUrl: /models
|
|
128
|
+
modelsAuthStyle: bearer
|
|
129
|
+
modelsDataPath: data
|
|
130
|
+
fields:
|
|
131
|
+
- key: name
|
|
132
|
+
label: Provider Nickname
|
|
133
|
+
required: true
|
|
134
|
+
secret: false
|
|
135
|
+
placeholder: 'e.g. My NVIDIA'
|
|
136
|
+
- key: apiKey
|
|
137
|
+
label: API Key
|
|
138
|
+
required: true
|
|
139
|
+
secret: true
|
|
140
|
+
placeholder: 'nvapi-...'
|
|
141
|
+
|
|
121
142
|
- id: zhipuglm
|
|
122
143
|
name: Zhipu GLM
|
|
123
144
|
baseUrl: https://open.bigmodel.cn/api/paas/v4
|
|
@@ -35,6 +35,19 @@ async function logToFile(message: string) {
|
|
|
35
35
|
// Disabled log output for production
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a base URL that may already contain a /v1 suffix,
|
|
40
|
+
* then append /v1/<path> so that double /v1 segments are avoided.
|
|
41
|
+
*
|
|
42
|
+
* Example:
|
|
43
|
+
* "https://integrate.api.nvidia.com/v1" + "chat/completions"
|
|
44
|
+
* => "https://integrate.api.nvidia.com/v1/chat/completions" (not /v1/v1/...)
|
|
45
|
+
*/
|
|
46
|
+
function v1Url(baseUrl: string, path: string): string {
|
|
47
|
+
const normalized = baseUrl.replace(/\/v1\/?$/, '')
|
|
48
|
+
return `${normalized}/v1/${path.replace(/^\//, '')}`
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function sendAnthropicError(message: string, _model: string | undefined, status = 502): Response {
|
|
39
52
|
const fullMessage = `[Bingo Proxy] ${message}`
|
|
40
53
|
void logToFile(`ERROR: ${fullMessage} (status: ${status})`)
|
|
@@ -176,7 +189,7 @@ async function handleAnthropicPassthrough(
|
|
|
176
189
|
uiLabel: string | null = null,
|
|
177
190
|
betaHeader: string | null = null,
|
|
178
191
|
): Promise<Response> {
|
|
179
|
-
const url =
|
|
192
|
+
const url = v1Url(baseUrl, 'messages')
|
|
180
193
|
const upstreamHeaders: Record<string, string> = {
|
|
181
194
|
'Content-Type': 'application/json',
|
|
182
195
|
'x-api-key': apiKey,
|
|
@@ -226,7 +239,7 @@ async function handleOpenaiChat(
|
|
|
226
239
|
uiLabel: string | null = null,
|
|
227
240
|
): Promise<Response> {
|
|
228
241
|
const transformed = anthropicToOpenaiChat(body)
|
|
229
|
-
const url =
|
|
242
|
+
const url = v1Url(baseUrl, 'chat/completions')
|
|
230
243
|
|
|
231
244
|
const upstream = await fetch(url, {
|
|
232
245
|
method: 'POST',
|
|
@@ -277,7 +290,7 @@ async function handleOpenaiResponses(
|
|
|
277
290
|
uiLabel: string | null = null,
|
|
278
291
|
): Promise<Response> {
|
|
279
292
|
const transformed = anthropicToOpenaiResponses(body)
|
|
280
|
-
const url =
|
|
293
|
+
const url = v1Url(baseUrl, 'responses')
|
|
281
294
|
|
|
282
295
|
const upstream = await fetch(url, {
|
|
283
296
|
method: 'POST',
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs/promises'
|
|
10
10
|
import * as path from 'path'
|
|
11
11
|
import * as os from 'os'
|
|
12
|
+
import { parse as parseJsonc } from 'jsonc-parser/lib/esm/main.js'
|
|
12
13
|
import { getDirectFetchOptions } from '../../utils/proxy.ts'
|
|
13
14
|
import { ApiError } from '../middleware/errorHandler.js'
|
|
14
15
|
import { anthropicToOpenaiChat } from '../proxy/transform/anthropicToOpenaiChat.js'
|
|
@@ -32,6 +33,10 @@ import type {
|
|
|
32
33
|
} from '../types/provider.ts'
|
|
33
34
|
import { SlotTableSchema } from '../types/provider.ts'
|
|
34
35
|
|
|
36
|
+
// NOTE: When the proxy is active, ANTHROPIC_AUTH_TOKEN is written as "proxy-managed"
|
|
37
|
+
// (a sentinel placeholder). This tells the proxy to resolve the real API key from the
|
|
38
|
+
// active provider config at request time. It is NOT a real credential and is expected
|
|
39
|
+
// to be overwritten every time a provider is activated or VS Code is linked.
|
|
35
40
|
const MANAGED_ENV_KEYS = [
|
|
36
41
|
'ANTHROPIC_BASE_URL',
|
|
37
42
|
'ANTHROPIC_AUTH_TOKEN',
|
|
@@ -264,6 +269,18 @@ export class ProviderService {
|
|
|
264
269
|
// --- Settings sync ---
|
|
265
270
|
|
|
266
271
|
private async syncToSettings(provider: SavedProvider): Promise<void> {
|
|
272
|
+
// Prefer slot-based model IDs when slots are configured, so that the
|
|
273
|
+
// slot table (set via "Configure Model Slots" in the TUI) is the single
|
|
274
|
+
// source of truth. Falling back to provider.models only when no slots
|
|
275
|
+
// exist keeps things working for users who haven't configured slots yet.
|
|
276
|
+
const slots = await this.readSlots()
|
|
277
|
+
const hasSlots = Object.keys(slots).length > 0
|
|
278
|
+
if (hasSlots) {
|
|
279
|
+
await this.syncSettingsForSlots(slots)
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Legacy fallback: no slots configured — use provider.models directly
|
|
267
284
|
const settings = await this.readSettings()
|
|
268
285
|
const existingEnv = (settings.env as Record<string, string>) || {}
|
|
269
286
|
|
|
@@ -530,6 +547,14 @@ export class ProviderService {
|
|
|
530
547
|
|
|
531
548
|
// --- VS Code linking ---
|
|
532
549
|
|
|
550
|
+
private buildBingoEnv(envVars: Array<{ name: string; value: string }>): Record<string, string> {
|
|
551
|
+
const env: Record<string, string> = {}
|
|
552
|
+
for (const v of envVars) {
|
|
553
|
+
env[v.name] = v.value
|
|
554
|
+
}
|
|
555
|
+
return env
|
|
556
|
+
}
|
|
557
|
+
|
|
533
558
|
/**
|
|
534
559
|
* Build the environmentVariables array for VS Code claudeCode settings
|
|
535
560
|
* based on current slot configuration. Uses user-set labels or falls back
|
|
@@ -576,11 +601,24 @@ export class ProviderService {
|
|
|
576
601
|
* Write claudeCode settings into ALL detected VS Code editor instances
|
|
577
602
|
* (stable, Insiders, Cursor). Persists linked state in bingo settings so
|
|
578
603
|
* it survives restarts.
|
|
604
|
+
*
|
|
605
|
+
* Fixes applied:
|
|
606
|
+
* #8 - Saves deep copy of original settings.claudeCode as vscodePreLinkClaudeCode
|
|
607
|
+
* so unlink can restore exactly what was there before.
|
|
608
|
+
* #9 - Collects failed paths; on partial failure rollback successful writes.
|
|
609
|
+
* #11 - Creates .bak backup before mutating VS Code settings.json; restores from
|
|
610
|
+
* .bak or in-memory copy on write failure.
|
|
579
611
|
*/
|
|
580
|
-
async linkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
612
|
+
async linkVscode(): Promise<{ paths: string[]; linked: boolean; failedPaths?: string[] }> {
|
|
581
613
|
const envVars = await this.buildVscodeEnvVars()
|
|
582
614
|
const candidatePaths = getVscodeSettingsPaths()
|
|
583
615
|
const writtenPaths: string[] = []
|
|
616
|
+
const failedPaths: string[] = []
|
|
617
|
+
|
|
618
|
+
// Fix #8: Snapshot original VS Code claudeCode per path for unlink restore.
|
|
619
|
+
const vscodePreLinkClaudeCode: Record<string, unknown> = {}
|
|
620
|
+
// Fix #9: Backup original contents for rollback on partial failure.
|
|
621
|
+
const originalContents = new Map<string, string | null>()
|
|
584
622
|
|
|
585
623
|
for (const settingsPath of candidatePaths) {
|
|
586
624
|
// Only write if the editor config directory exists (editor is installed)
|
|
@@ -592,12 +630,31 @@ export class ProviderService {
|
|
|
592
630
|
}
|
|
593
631
|
|
|
594
632
|
let settings: Record<string, unknown> = {}
|
|
633
|
+
let originalContent: string | null = null
|
|
595
634
|
try {
|
|
596
|
-
|
|
597
|
-
settings
|
|
635
|
+
originalContent = await fs.readFile(settingsPath, "utf-8")
|
|
636
|
+
// VS Code settings.json uses JSONC (JSON with comments) —
|
|
637
|
+
// parseJsonc handles comments/trailing commas, unlike JSON.parse
|
|
638
|
+
settings = parseJsonc(originalContent) as Record<string, unknown>
|
|
598
639
|
} catch {
|
|
599
640
|
// File does not exist yet — start fresh
|
|
600
641
|
}
|
|
642
|
+
originalContents.set(settingsPath, originalContent)
|
|
643
|
+
|
|
644
|
+
// Fix #8: Save deep copy of original claudeCode before overwriting
|
|
645
|
+
vscodePreLinkClaudeCode[settingsPath] = settings.claudeCode !== undefined
|
|
646
|
+
? JSON.parse(JSON.stringify(settings.claudeCode))
|
|
647
|
+
: undefined
|
|
648
|
+
|
|
649
|
+
// Fix #11: Write backup before mutating
|
|
650
|
+
if (originalContent !== null) {
|
|
651
|
+
const bakPath = `${settingsPath}.bak`
|
|
652
|
+
try {
|
|
653
|
+
await fs.writeFile(bakPath, originalContent, "utf-8")
|
|
654
|
+
} catch {
|
|
655
|
+
// best-effort backup — proceed without it
|
|
656
|
+
}
|
|
657
|
+
}
|
|
601
658
|
|
|
602
659
|
settings.claudeCode = {
|
|
603
660
|
...(settings.claudeCode as Record<string, unknown> || {}),
|
|
@@ -606,9 +663,73 @@ export class ProviderService {
|
|
|
606
663
|
environmentVariables: envVars,
|
|
607
664
|
}
|
|
608
665
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
666
|
+
try {
|
|
667
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true })
|
|
668
|
+
const tmpPath = `${settingsPath}.tmp.${Date.now()}`
|
|
669
|
+
await fs.writeFile(tmpPath, JSON.stringify(settings, null, 4) + '\n')
|
|
670
|
+
await fs.rename(tmpPath, settingsPath)
|
|
671
|
+
writtenPaths.push(settingsPath)
|
|
672
|
+
} catch (err) {
|
|
673
|
+
failedPaths.push(settingsPath)
|
|
674
|
+
console.error(
|
|
675
|
+
`[ProviderService] Failed to write VS Code settings to ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
// Fix #11: Attempt restore from backup on write failure
|
|
679
|
+
if (originalContent !== null) {
|
|
680
|
+
try {
|
|
681
|
+
const tmpPath2 = `${settingsPath}.tmp.${Date.now()}`
|
|
682
|
+
await fs.writeFile(tmpPath2, originalContent, "utf-8")
|
|
683
|
+
await fs.rename(tmpPath2, settingsPath)
|
|
684
|
+
console.error(`[ProviderService] Restored ${settingsPath} from in-memory copy after write failure`)
|
|
685
|
+
} catch {
|
|
686
|
+
// In-memory restore failed, try .bak file
|
|
687
|
+
try {
|
|
688
|
+
await fs.copyFile(`${settingsPath}.bak`, settingsPath)
|
|
689
|
+
console.error(`[ProviderService] Restored ${settingsPath} from .bak after write failure`)
|
|
690
|
+
} catch (restoreErr) {
|
|
691
|
+
console.error(
|
|
692
|
+
`[ProviderService] Failed to restore ${settingsPath} after write failure: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Clean up .bak files for successfully written paths
|
|
701
|
+
for (const p of writtenPaths) {
|
|
702
|
+
await fs.unlink(`${p}.bak`).catch(() => {})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Fix #9: Rollback successful writes on partial failure for atomicity.
|
|
706
|
+
// If any writes failed AND some succeeded, attempt to restore originals.
|
|
707
|
+
if (failedPaths.length > 0 && writtenPaths.length > 0) {
|
|
708
|
+
console.error(
|
|
709
|
+
`[ProviderService] Partial VS Code link failure: ${writtenPaths.length} succeeded, ${failedPaths.length} failed. Rolling back successful writes.`,
|
|
710
|
+
)
|
|
711
|
+
let allRolledBack = true
|
|
712
|
+
for (const p of writtenPaths) {
|
|
713
|
+
const original = originalContents.get(p)
|
|
714
|
+
try {
|
|
715
|
+
if (original !== null) {
|
|
716
|
+
const tmpPath = `${p}.tmp.${Date.now()}`
|
|
717
|
+
await fs.writeFile(tmpPath, original, "utf-8")
|
|
718
|
+
await fs.rename(tmpPath, p)
|
|
719
|
+
} else {
|
|
720
|
+
await fs.unlink(p).catch(() => {})
|
|
721
|
+
}
|
|
722
|
+
} catch (rollbackErr) {
|
|
723
|
+
allRolledBack = false
|
|
724
|
+
console.error(
|
|
725
|
+
`[ProviderService] Rollback failed for ${p}: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (allRolledBack) {
|
|
730
|
+
writtenPaths.length = 0
|
|
731
|
+
}
|
|
732
|
+
// If rollback itself failed we have a mess — leave writtenPaths as-is so caller sees what happened.
|
|
612
733
|
}
|
|
613
734
|
|
|
614
735
|
if (writtenPaths.length === 0) {
|
|
@@ -621,55 +742,49 @@ export class ProviderService {
|
|
|
621
742
|
const bingoSettings = await this.readSettings()
|
|
622
743
|
bingoSettings.vscodeLinked = true
|
|
623
744
|
bingoSettings.vscodeLinkedPaths = writtenPaths
|
|
745
|
+
bingoSettings.vscodePreLinkClaudeCode = vscodePreLinkClaudeCode
|
|
746
|
+
bingoSettings.env = this.buildBingoEnv(envVars)
|
|
624
747
|
await this.writeSettings(bingoSettings)
|
|
625
748
|
|
|
626
|
-
|
|
749
|
+
const result: { paths: string[]; linked: boolean; failedPaths?: string[] } = {
|
|
750
|
+
paths: writtenPaths,
|
|
751
|
+
linked: true,
|
|
752
|
+
}
|
|
753
|
+
if (failedPaths.length > 0) {
|
|
754
|
+
result.failedPaths = failedPaths
|
|
755
|
+
}
|
|
756
|
+
return result
|
|
627
757
|
}
|
|
628
758
|
|
|
629
759
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
760
|
+
* Reset VS Code settings to pre-link state. Does NOT touching bingo
|
|
761
|
+
* settings.env — the CLI keeps routing through the proxy independently.
|
|
762
|
+
*
|
|
763
|
+
* Restores the original claudeCode snapshot (vscodePreLinkClaudeCode)
|
|
764
|
+
* when available, otherwise writes an override that points VS Code to
|
|
765
|
+
* api.anthropic.com so the extension shows its native login screen.
|
|
632
766
|
*/
|
|
633
767
|
async unlinkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
634
768
|
const bingoSettings = await this.readSettings()
|
|
635
769
|
const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
|
|
636
770
|
|
|
771
|
+
// Restore original claudeCode snapshot per-path. Fall back to a clean
|
|
772
|
+
const preLinkClaudeCode =
|
|
773
|
+
(bingoSettings.vscodePreLinkClaudeCode as Record<string, unknown> | undefined) || {}
|
|
774
|
+
|
|
637
775
|
for (const settingsPath of linkedPaths) {
|
|
638
776
|
try {
|
|
639
777
|
const raw = await fs.readFile(settingsPath, "utf-8")
|
|
640
|
-
const settings =
|
|
641
|
-
|
|
642
|
-
// env vars take precedence over ~/.claude/bingo/settings.json
|
|
643
|
-
// so the spawned CLI does NOT route through bingo.
|
|
644
|
-
settings.claudeCode = {
|
|
778
|
+
const settings = parseJsonc(raw) as Record<string, unknown>
|
|
779
|
+
settings.claudeCode = preLinkClaudeCode[settingsPath] ?? {
|
|
645
780
|
preferredLocation: "panel",
|
|
646
|
-
disableLoginPrompt: true,
|
|
647
781
|
environmentVariables: [
|
|
648
782
|
{ name: "ANTHROPIC_BASE_URL", value: "https://api.anthropic.com" },
|
|
649
783
|
],
|
|
650
784
|
}
|
|
651
|
-
|
|
652
|
-
')
|
|
653
|
-
|
|
654
|
-
// File may have been moved or deleted since linking — harmless
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
bingoSettings.vscodeLinked = false
|
|
659
|
-
delete bingoSettings.vscodeLinkedPaths
|
|
660
|
-
await this.writeSettings(bingoSettings)
|
|
661
|
-
|
|
662
|
-
return { paths: linkedPaths, linked: false }
|
|
663
|
-
}> {
|
|
664
|
-
const bingoSettings = await this.readSettings()
|
|
665
|
-
const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
|
|
666
|
-
|
|
667
|
-
for (const settingsPath of linkedPaths) {
|
|
668
|
-
try {
|
|
669
|
-
const raw = await fs.readFile(settingsPath, "utf-8")
|
|
670
|
-
const settings = JSON.parse(raw)
|
|
671
|
-
delete settings.claudeCode
|
|
672
|
-
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
|
|
785
|
+
const tmpPath = `${settingsPath}.tmp.${Date.now()}`
|
|
786
|
+
await fs.writeFile(tmpPath, JSON.stringify(settings, null, 4) + '\n')
|
|
787
|
+
await fs.rename(tmpPath, settingsPath)
|
|
673
788
|
} catch {
|
|
674
789
|
// File may have been moved or deleted since linking — harmless
|
|
675
790
|
}
|
|
@@ -677,6 +792,8 @@ export class ProviderService {
|
|
|
677
792
|
|
|
678
793
|
bingoSettings.vscodeLinked = false
|
|
679
794
|
delete bingoSettings.vscodeLinkedPaths
|
|
795
|
+
delete bingoSettings.vscodePreLinkClaudeCode
|
|
796
|
+
delete bingoSettings.vscodePreLinkEnv
|
|
680
797
|
await this.writeSettings(bingoSettings)
|
|
681
798
|
|
|
682
799
|
return { paths: linkedPaths, linked: false }
|
|
@@ -703,12 +820,26 @@ export class ProviderService {
|
|
|
703
820
|
if (status.linked) {
|
|
704
821
|
try {
|
|
705
822
|
await this.linkVscode()
|
|
706
|
-
} catch {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
823
|
+
} catch (err: unknown) {
|
|
824
|
+
console.error(
|
|
825
|
+
`[ProviderService] init: failed to re-sync VS Code link: ${err instanceof Error ? err.message : String(err)}`,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
// Only clear linked state for "VS Code not installed" type errors.
|
|
829
|
+
// Disk failures, corrupt JSON, etc. are transient and should not
|
|
830
|
+
// silently destroy the user's linked state.
|
|
831
|
+
const isNotFound =
|
|
832
|
+
err instanceof Error &&
|
|
833
|
+
(err.message.includes('No VS Code installation detected') ||
|
|
834
|
+
((err as { statusCode?: number }).statusCode === 400))
|
|
835
|
+
|
|
836
|
+
if (isNotFound) {
|
|
837
|
+
const bingoSettings = await this.readSettings()
|
|
838
|
+
bingoSettings.vscodeLinked = false
|
|
839
|
+
delete bingoSettings.vscodeLinkedPaths
|
|
840
|
+
await this.writeSettings(bingoSettings)
|
|
841
|
+
}
|
|
842
|
+
// For other errors, keep existing state — the link may be recoverable.
|
|
712
843
|
}
|
|
713
844
|
}
|
|
714
845
|
}
|
|
@@ -902,10 +1033,10 @@ export class ProviderService {
|
|
|
902
1033
|
let transformedBody: unknown
|
|
903
1034
|
if (format === 'openai_chat') {
|
|
904
1035
|
transformedBody = anthropicToOpenaiChat(anthropicReq)
|
|
905
|
-
upstreamUrl =
|
|
1036
|
+
upstreamUrl = v1Url(base, 'chat/completions')
|
|
906
1037
|
} else {
|
|
907
1038
|
transformedBody = anthropicToOpenaiResponses(anthropicReq)
|
|
908
|
-
upstreamUrl =
|
|
1039
|
+
upstreamUrl = v1Url(base, 'responses')
|
|
909
1040
|
}
|
|
910
1041
|
|
|
911
1042
|
// Call upstream with transformed request
|
|
@@ -952,6 +1083,15 @@ export class ProviderService {
|
|
|
952
1083
|
|
|
953
1084
|
// ─── Helpers ───────────────────────────────────────────────
|
|
954
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Normalize a base URL that may already contain a /v1 suffix,
|
|
1088
|
+
* then append /v1/<path> so that double /v1 segments are avoided.
|
|
1089
|
+
*/
|
|
1090
|
+
function v1Url(base: string, path: string): string {
|
|
1091
|
+
const normalized = base.replace(/\/v1\/?$/, '')
|
|
1092
|
+
return `${normalized}/v1/${path.replace(/^\//, '')}`
|
|
1093
|
+
}
|
|
1094
|
+
|
|
955
1095
|
function buildDirectTestRequest(
|
|
956
1096
|
base: string,
|
|
957
1097
|
apiKey: string,
|
|
@@ -962,21 +1102,21 @@ function buildDirectTestRequest(
|
|
|
962
1102
|
|
|
963
1103
|
if (format === 'openai_chat') {
|
|
964
1104
|
return {
|
|
965
|
-
url:
|
|
1105
|
+
url: v1Url(base, 'chat/completions'),
|
|
966
1106
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
967
1107
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
968
1108
|
}
|
|
969
1109
|
}
|
|
970
1110
|
if (format === 'openai_responses') {
|
|
971
1111
|
return {
|
|
972
|
-
url:
|
|
1112
|
+
url: v1Url(base, 'responses'),
|
|
973
1113
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
974
1114
|
body: { model: modelId, max_output_tokens: 16, input: [{ type: 'message', role: 'user', content: prompt }] },
|
|
975
1115
|
}
|
|
976
1116
|
}
|
|
977
1117
|
// anthropic
|
|
978
1118
|
return {
|
|
979
|
-
url:
|
|
1119
|
+
url: v1Url(base, 'messages'),
|
|
980
1120
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
981
1121
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
982
1122
|
}
|
|
@@ -1008,5 +1148,4 @@ function validateResponseBody(
|
|
|
1008
1148
|
return { ok: false, error: 'Not a valid Anthropic Messages endpoint' }
|
|
1009
1149
|
}
|
|
1010
1150
|
return { ok: true, model: (body.model as string) || undefined }
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1151
|
+
}
|
|
@@ -5,7 +5,7 @@ import { ConfigurableShortcutHint } from 'src/components/ConfigurableShortcutHin
|
|
|
5
5
|
import { CtrlOToExpand, SubAgentProvider } from 'src/components/CtrlOToExpand.js';
|
|
6
6
|
import { Byline } from 'src/components/design-system/Byline.js';
|
|
7
7
|
import { KeyboardShortcutHint } from 'src/components/design-system/KeyboardShortcutHint.js';
|
|
8
|
-
import
|
|
8
|
+
import { z } from 'zod/v4';
|
|
9
9
|
import { AgentProgressLine } from '../../components/AgentProgressLine.js';
|
|
10
10
|
import { FallbackToolUseErrorMessage } from '../../components/FallbackToolUseErrorMessage.js';
|
|
11
11
|
import { FallbackToolUseRejectedMessage } from '../../components/FallbackToolUseRejectedMessage.js';
|
|
@@ -15,14 +15,14 @@ import { MessageResponse } from '../../components/MessageResponse.js';
|
|
|
15
15
|
import { ToolUseLoader } from '../../components/ToolUseLoader.js';
|
|
16
16
|
import { Box, Text } from '../../ink.js';
|
|
17
17
|
import { getDumpPromptsPath } from '../../services/api/dumpPrompts.js';
|
|
18
|
-
import { findToolByName, type Tools } from '../../Tool.js';
|
|
18
|
+
import { buildTool, findToolByName, toolMatchesName, type Tool, type Tools } from '../../Tool.js';
|
|
19
19
|
import type { Message, ProgressMessage } from '../../types/message.js';
|
|
20
20
|
import type { AgentToolProgress } from '../../types/tools.js';
|
|
21
21
|
import { count } from '../../utils/array.js';
|
|
22
22
|
import { getSearchOrReadFromContent, getSearchReadSummaryText } from '../../utils/collapseReadSearch.js';
|
|
23
23
|
import { getDisplayPath } from '../../utils/file.js';
|
|
24
24
|
import { formatDuration, formatNumber } from '../../utils/format.js';
|
|
25
|
-
import { buildSubagentLookups, createAssistantMessage, EMPTY_LOOKUPS } from '../../utils/messages.js';
|
|
25
|
+
import { buildSubagentLookups, createAssistantMessage, EMPTY_LOOKUPS, type MessageLookups } from '../../utils/messages.js';
|
|
26
26
|
import type { ModelAlias } from '../../utils/model/aliases.js';
|
|
27
27
|
import { getMainLoopModel, parseUserSpecifiedModel, renderModelName } from '../../utils/model/model.js';
|
|
28
28
|
import type { Theme, ThemeName } from '../../utils/theme.js';
|
|
@@ -32,6 +32,58 @@ import { getAgentColor } from './agentColorManager.js';
|
|
|
32
32
|
import { GENERAL_PURPOSE_AGENT } from './built-in/generalPurposeAgent.js';
|
|
33
33
|
const MAX_PROGRESS_MESSAGES_TO_SHOW = 3;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Create a minimal fallback Tool object for a sub-agent tool name not present
|
|
37
|
+
* in the coordinator's tool set. Used in brain mode where the coordinator only
|
|
38
|
+
* has [Agent, AskUserQuestion] but sub-agents use Read/Grep/Bash/Edit/etc.
|
|
39
|
+
* Without fallbacks, findToolByName returns null and renders blank lines.
|
|
40
|
+
*/
|
|
41
|
+
function createFallbackTool(name: string): Tool {
|
|
42
|
+
return buildTool({
|
|
43
|
+
name,
|
|
44
|
+
inputSchema: z.object({}).passthrough(),
|
|
45
|
+
description: async () => `Sub-agent tool: ${name}`,
|
|
46
|
+
call: async () => ({ data: {} as unknown }) as any,
|
|
47
|
+
maxResultSizeChars: 100_000,
|
|
48
|
+
prompt: async () => '',
|
|
49
|
+
mapToolResultToToolResultBlockParam: (_content: unknown, toolUseID: string) => ({
|
|
50
|
+
tool_use_id: toolUseID,
|
|
51
|
+
content: [{ type: 'text' as const, text: '' }],
|
|
52
|
+
type: 'tool_result' as const,
|
|
53
|
+
}),
|
|
54
|
+
renderToolUseMessage: (input: Record<string, unknown>) => {
|
|
55
|
+
const keys = Object.keys(input);
|
|
56
|
+
if (keys.length === 0) return '';
|
|
57
|
+
return keys.map(k => {
|
|
58
|
+
const v = input[k];
|
|
59
|
+
let s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
60
|
+
if (s.length > 80) s = s.slice(0, 80) + '\u2026';
|
|
61
|
+
return `${k}: ${s}`;
|
|
62
|
+
}).join(', ');
|
|
63
|
+
},
|
|
64
|
+
renderToolResultMessage: () => null,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Given the result of buildSubagentLookups, create fallback Tool entries
|
|
70
|
+
* for any tool name not already in `tools`. Returns the original `tools`
|
|
71
|
+
* array if no new names are found. Avoids re-scanning progress data.
|
|
72
|
+
*/
|
|
73
|
+
function augmentToolsFromLookups(
|
|
74
|
+
tools: Tools,
|
|
75
|
+
lookups: MessageLookups,
|
|
76
|
+
): Tools {
|
|
77
|
+
const toolNames = new Set<string>()
|
|
78
|
+
for (const block of lookups.toolUseByToolUseID.values()) {
|
|
79
|
+
toolNames.add(block.name)
|
|
80
|
+
}
|
|
81
|
+
if (toolNames.size === 0) return tools
|
|
82
|
+
const missing = [...toolNames].filter(n => !tools.some(t => toolMatchesName(t, n)))
|
|
83
|
+
if (missing.length === 0) return tools
|
|
84
|
+
return [...tools, ...missing.map(createFallbackTool)]
|
|
85
|
+
}
|
|
86
|
+
|
|
35
87
|
/**
|
|
36
88
|
* Guard: checks if progress data has a `message` field (agent_progress or
|
|
37
89
|
* skill_progress). Other progress types (e.g. bash_progress forwarded from
|
|
@@ -262,12 +314,12 @@ function VerboseAgentTranscript(t0) {
|
|
|
262
314
|
lookups: agentLookups,
|
|
263
315
|
inProgressToolUseIDs
|
|
264
316
|
} = t1;
|
|
265
|
-
let t2;
|
|
317
|
+
const augmentedTools = augmentToolsFromLookups(tools, agentLookups); let t2;
|
|
266
318
|
if ($[2] !== agentLookups || $[3] !== inProgressToolUseIDs || $[4] !== progressMessages || $[5] !== tools || $[6] !== verbose) {
|
|
267
319
|
const filteredMessages = progressMessages.filter(_temp4);
|
|
268
320
|
let t3;
|
|
269
321
|
if ($[8] !== agentLookups || $[9] !== inProgressToolUseIDs || $[10] !== tools || $[11] !== verbose) {
|
|
270
|
-
t3 = progressMessage => <MessageResponse key={progressMessage.uuid} height={1}><MessageComponent message={progressMessage.data.message} lookups={agentLookups} addMargin={false} tools={
|
|
322
|
+
t3 = progressMessage => <MessageResponse key={progressMessage.uuid} height={1}><MessageComponent message={progressMessage.data.message} lookups={agentLookups} addMargin={false} tools={augmentedTools} commands={[]} verbose={verbose} inProgressToolUseIDs={inProgressToolUseIDs} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} /></MessageResponse>;
|
|
271
323
|
$[8] = agentLookups;
|
|
272
324
|
$[9] = inProgressToolUseIDs;
|
|
273
325
|
$[10] = tools;
|
|
@@ -384,6 +436,12 @@ export function renderToolResultMessage(data: Output, progressMessagesForMessage
|
|
|
384
436
|
speed: null
|
|
385
437
|
}
|
|
386
438
|
});
|
|
439
|
+
const agentProgressData = progressMessagesForMessage
|
|
440
|
+
.filter((pm): pm is ProgressMessage<AgentToolProgress> => hasProgressMessage(pm.data))
|
|
441
|
+
.map(pm => pm.data)
|
|
442
|
+
const { lookups: resultLookups } = buildSubagentLookups(agentProgressData)
|
|
443
|
+
const augmentedTools = augmentToolsFromLookups(tools, resultLookups)
|
|
444
|
+
|
|
387
445
|
return <Box flexDirection="column">
|
|
388
446
|
{"external" === 'ant' && <MessageResponse>
|
|
389
447
|
<Text color="warning">
|
|
@@ -394,13 +452,13 @@ export function renderToolResultMessage(data: Output, progressMessagesForMessage
|
|
|
394
452
|
<AgentPromptDisplay prompt={prompt} theme={theme} />
|
|
395
453
|
</MessageResponse>}
|
|
396
454
|
{isTranscriptMode ? <SubAgentProvider>
|
|
397
|
-
<VerboseAgentTranscript progressMessages={progressMessagesForMessage} tools={
|
|
455
|
+
<VerboseAgentTranscript progressMessages={progressMessagesForMessage} tools={augmentedTools} verbose={verbose} />
|
|
398
456
|
</SubAgentProvider> : null}
|
|
399
457
|
{isTranscriptMode && content && content.length > 0 && <MessageResponse>
|
|
400
458
|
<AgentResponseDisplay content={content} theme={theme} />
|
|
401
459
|
</MessageResponse>}
|
|
402
460
|
<MessageResponse height={1}>
|
|
403
|
-
<MessageComponent message={finalAssistantMessage} lookups={EMPTY_LOOKUPS} addMargin={false} tools={
|
|
461
|
+
<MessageComponent message={finalAssistantMessage} lookups={EMPTY_LOOKUPS} addMargin={false} tools={augmentedTools} commands={[]} verbose={verbose} inProgressToolUseIDs={new Set()} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} isTranscriptMode={false} isStatic={true} />
|
|
404
462
|
</MessageResponse>
|
|
405
463
|
{!isTranscriptMode && <Text dimColor>
|
|
406
464
|
{' '}
|
|
@@ -540,6 +598,13 @@ export function renderToolUseProgressMessage(progressMessages: ProgressMessage<P
|
|
|
540
598
|
lookups: subagentLookups,
|
|
541
599
|
inProgressToolUseIDs: collapsedInProgressIDs
|
|
542
600
|
} = buildSubagentLookups(progressMessages.filter((pm): pm is ProgressMessage<AgentToolProgress> => hasProgressMessage(pm.data)).map(pm => pm.data));
|
|
601
|
+
|
|
602
|
+
// Augment tools with fallback entries for sub-agent tools not in the
|
|
603
|
+
// coordinator's tool set. In brain mode the coordinator only has
|
|
604
|
+
// [Agent, AskUserQuestion], but sub-agents call Read/Grep/Bash/Edit/etc.
|
|
605
|
+
// Without fallbacks, findToolByName returns null and renders blank lines.
|
|
606
|
+
const augmentedTools = augmentToolsFromLookups(tools, subagentLookups);
|
|
607
|
+
|
|
543
608
|
return <MessageResponse>
|
|
544
609
|
<Box flexDirection="column">
|
|
545
610
|
<SubAgentProvider>
|
|
@@ -558,7 +623,7 @@ export function renderToolUseProgressMessage(progressMessages: ProgressMessage<P
|
|
|
558
623
|
// content (tool not found, renderToolUseMessage returns null)
|
|
559
624
|
// doesn't leave a blank line. Tool call headers are single-line
|
|
560
625
|
// anyway so truncation isn't needed.
|
|
561
|
-
return <MessageComponent key={processed.message.uuid} message={processed.message.data.message} lookups={subagentLookups} addMargin={false} tools={
|
|
626
|
+
return <MessageComponent key={processed.message.uuid} message={processed.message.data.message} lookups={subagentLookups} addMargin={false} tools={augmentedTools} commands={[]} verbose={verbose} inProgressToolUseIDs={collapsedInProgressIDs} progressMessagesForMessage={[]} shouldAnimate={false} shouldShowDot={false} style="condensed" isTranscriptMode={false} isStatic={true} />;
|
|
562
627
|
})}
|
|
563
628
|
</SubAgentProvider>
|
|
564
629
|
{hiddenToolUseCount > 0 && <Text dimColor>
|