bingocode 1.1.200-beta.9 → 1.1.201
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 +1 -1
- package/src/constants/prompts.ts +30 -18
- 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 +181 -43
- package/src/tools/AgentTool/UI.tsx +73 -8
- package/src/tools/AgentTool/agentToolUtils.ts +2 -2
- package/src/tools.ts +397 -390
- package/src/utils/attachments.ts +2 -2
- package/src/utils/context.ts +1 -1
- package/src/utils/sessionStorage.ts +39 -8
- 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
|
|
|
@@ -584,17 +601,24 @@ export class ProviderService {
|
|
|
584
601
|
* Write claudeCode settings into ALL detected VS Code editor instances
|
|
585
602
|
* (stable, Insiders, Cursor). Persists linked state in bingo settings so
|
|
586
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.
|
|
587
611
|
*/
|
|
588
|
-
async linkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
612
|
+
async linkVscode(): Promise<{ paths: string[]; linked: boolean; failedPaths?: string[] }> {
|
|
589
613
|
const envVars = await this.buildVscodeEnvVars()
|
|
590
614
|
const candidatePaths = getVscodeSettingsPaths()
|
|
591
615
|
const writtenPaths: string[] = []
|
|
616
|
+
const failedPaths: string[] = []
|
|
592
617
|
|
|
593
|
-
//
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
await this.writeSettings(bingoSettings)
|
|
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>()
|
|
598
622
|
|
|
599
623
|
for (const settingsPath of candidatePaths) {
|
|
600
624
|
// Only write if the editor config directory exists (editor is installed)
|
|
@@ -606,12 +630,31 @@ export class ProviderService {
|
|
|
606
630
|
}
|
|
607
631
|
|
|
608
632
|
let settings: Record<string, unknown> = {}
|
|
633
|
+
let originalContent: string | null = null
|
|
609
634
|
try {
|
|
610
|
-
|
|
611
|
-
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>
|
|
612
639
|
} catch {
|
|
613
640
|
// File does not exist yet — start fresh
|
|
614
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
|
+
}
|
|
615
658
|
|
|
616
659
|
settings.claudeCode = {
|
|
617
660
|
...(settings.claudeCode as Record<string, unknown> || {}),
|
|
@@ -620,9 +663,73 @@ export class ProviderService {
|
|
|
620
663
|
environmentVariables: envVars,
|
|
621
664
|
}
|
|
622
665
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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.
|
|
626
733
|
}
|
|
627
734
|
|
|
628
735
|
if (writtenPaths.length === 0) {
|
|
@@ -632,45 +739,52 @@ export class ProviderService {
|
|
|
632
739
|
}
|
|
633
740
|
|
|
634
741
|
// Persist so re-sync on restart and unlink work correctly
|
|
742
|
+
const bingoSettings = await this.readSettings()
|
|
635
743
|
bingoSettings.vscodeLinked = true
|
|
636
744
|
bingoSettings.vscodeLinkedPaths = writtenPaths
|
|
745
|
+
bingoSettings.vscodePreLinkClaudeCode = vscodePreLinkClaudeCode
|
|
637
746
|
bingoSettings.env = this.buildBingoEnv(envVars)
|
|
638
747
|
await this.writeSettings(bingoSettings)
|
|
639
748
|
|
|
640
|
-
|
|
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
|
|
641
757
|
}
|
|
642
758
|
|
|
643
759
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
760
|
+
* Reset VS Code settings to pre-link state. Does NOT touching bingo
|
|
761
|
+
* settings.env — the CLI keeps routing through the proxy independently.
|
|
646
762
|
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
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.
|
|
650
766
|
*/
|
|
651
767
|
async unlinkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
652
768
|
const bingoSettings = await this.readSettings()
|
|
653
769
|
const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
|
|
654
770
|
|
|
655
|
-
// Restore
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
if (preLinkEnv === null || Object.keys(preLinkEnv as Record<string, unknown>).length === 0) {
|
|
659
|
-
delete bingoSettings.env
|
|
660
|
-
} else {
|
|
661
|
-
bingoSettings.env = preLinkEnv
|
|
662
|
-
}
|
|
663
|
-
delete bingoSettings.vscodePreLinkEnv
|
|
664
|
-
}
|
|
771
|
+
// Restore original claudeCode snapshot per-path. Fall back to a clean
|
|
772
|
+
const preLinkClaudeCode =
|
|
773
|
+
(bingoSettings.vscodePreLinkClaudeCode as Record<string, unknown> | undefined) || {}
|
|
665
774
|
|
|
666
775
|
for (const settingsPath of linkedPaths) {
|
|
667
776
|
try {
|
|
668
777
|
const raw = await fs.readFile(settingsPath, "utf-8")
|
|
669
|
-
const settings =
|
|
670
|
-
settings.claudeCode = {
|
|
778
|
+
const settings = parseJsonc(raw) as Record<string, unknown>
|
|
779
|
+
settings.claudeCode = preLinkClaudeCode[settingsPath] ?? {
|
|
671
780
|
preferredLocation: "panel",
|
|
781
|
+
environmentVariables: [
|
|
782
|
+
{ name: "ANTHROPIC_BASE_URL", value: "https://api.anthropic.com" },
|
|
783
|
+
],
|
|
672
784
|
}
|
|
673
|
-
|
|
785
|
+
const tmpPath = `${settingsPath}.tmp.${Date.now()}`
|
|
786
|
+
await fs.writeFile(tmpPath, JSON.stringify(settings, null, 4) + '\n')
|
|
787
|
+
await fs.rename(tmpPath, settingsPath)
|
|
674
788
|
} catch {
|
|
675
789
|
// File may have been moved or deleted since linking — harmless
|
|
676
790
|
}
|
|
@@ -678,6 +792,8 @@ export class ProviderService {
|
|
|
678
792
|
|
|
679
793
|
bingoSettings.vscodeLinked = false
|
|
680
794
|
delete bingoSettings.vscodeLinkedPaths
|
|
795
|
+
delete bingoSettings.vscodePreLinkClaudeCode
|
|
796
|
+
delete bingoSettings.vscodePreLinkEnv
|
|
681
797
|
await this.writeSettings(bingoSettings)
|
|
682
798
|
|
|
683
799
|
return { paths: linkedPaths, linked: false }
|
|
@@ -704,12 +820,26 @@ export class ProviderService {
|
|
|
704
820
|
if (status.linked) {
|
|
705
821
|
try {
|
|
706
822
|
await this.linkVscode()
|
|
707
|
-
} catch {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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.
|
|
713
843
|
}
|
|
714
844
|
}
|
|
715
845
|
}
|
|
@@ -903,10 +1033,10 @@ export class ProviderService {
|
|
|
903
1033
|
let transformedBody: unknown
|
|
904
1034
|
if (format === 'openai_chat') {
|
|
905
1035
|
transformedBody = anthropicToOpenaiChat(anthropicReq)
|
|
906
|
-
upstreamUrl =
|
|
1036
|
+
upstreamUrl = v1Url(base, 'chat/completions')
|
|
907
1037
|
} else {
|
|
908
1038
|
transformedBody = anthropicToOpenaiResponses(anthropicReq)
|
|
909
|
-
upstreamUrl =
|
|
1039
|
+
upstreamUrl = v1Url(base, 'responses')
|
|
910
1040
|
}
|
|
911
1041
|
|
|
912
1042
|
// Call upstream with transformed request
|
|
@@ -953,6 +1083,15 @@ export class ProviderService {
|
|
|
953
1083
|
|
|
954
1084
|
// ─── Helpers ───────────────────────────────────────────────
|
|
955
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
|
+
|
|
956
1095
|
function buildDirectTestRequest(
|
|
957
1096
|
base: string,
|
|
958
1097
|
apiKey: string,
|
|
@@ -963,21 +1102,21 @@ function buildDirectTestRequest(
|
|
|
963
1102
|
|
|
964
1103
|
if (format === 'openai_chat') {
|
|
965
1104
|
return {
|
|
966
|
-
url:
|
|
1105
|
+
url: v1Url(base, 'chat/completions'),
|
|
967
1106
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
968
1107
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
969
1108
|
}
|
|
970
1109
|
}
|
|
971
1110
|
if (format === 'openai_responses') {
|
|
972
1111
|
return {
|
|
973
|
-
url:
|
|
1112
|
+
url: v1Url(base, 'responses'),
|
|
974
1113
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
975
1114
|
body: { model: modelId, max_output_tokens: 16, input: [{ type: 'message', role: 'user', content: prompt }] },
|
|
976
1115
|
}
|
|
977
1116
|
}
|
|
978
1117
|
// anthropic
|
|
979
1118
|
return {
|
|
980
|
-
url:
|
|
1119
|
+
url: v1Url(base, 'messages'),
|
|
981
1120
|
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
982
1121
|
body: { model: modelId, max_tokens: 16, messages: [{ role: 'user', content: prompt }] },
|
|
983
1122
|
}
|
|
@@ -1009,5 +1148,4 @@ function validateResponseBody(
|
|
|
1009
1148
|
return { ok: false, error: 'Not a valid Anthropic Messages endpoint' }
|
|
1010
1149
|
}
|
|
1011
1150
|
return { ok: true, model: (body.model as string) || undefined }
|
|
1012
|
-
}
|
|
1013
|
-
|
|
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>
|