bingocode 1.1.195 → 1.1.200-beta.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/bin/bingocode-win.cjs +1 -1
- package/bin/claude +1 -1
- package/bin/claude-win.cjs +1 -1
- package/package.json +1 -1
- package/src/cli/ProviderPanel.tsx +63 -50
- package/src/commands/{exec/exec.ts → brain/brain.ts} +7 -7
- package/src/commands/brain/index.ts +14 -0
- package/src/commands.ts +2 -2
- package/src/components/PromptInput/PromptInput.tsx +5 -5
- package/src/constants/prompts.ts +44 -9
- package/src/main.tsx +1 -1
- package/src/server/api/providers.ts +20 -0
- package/src/server/services/providerService.ts +199 -1
- package/src/state/AppStateStore.ts +3 -3
- package/src/utils/attachments.ts +5 -5
- package/src/utils/settings/types.ts +2 -2
- package/src/commands/exec/index.ts +0 -16
package/bin/bingocode-win.cjs
CHANGED
package/bin/claude
CHANGED
package/bin/claude-win.cjs
CHANGED
package/package.json
CHANGED
|
@@ -51,8 +51,7 @@ type Stage =
|
|
|
51
51
|
| 'editing'
|
|
52
52
|
| 'slot_config'
|
|
53
53
|
| 'slot_loading'
|
|
54
|
-
| 'slot_select_model'
|
|
55
|
-
| 'slot_input_label';
|
|
54
|
+
| 'slot_select_model';
|
|
56
55
|
|
|
57
56
|
export const ProviderPanel: React.FC<{
|
|
58
57
|
apiUrl: string;
|
|
@@ -97,9 +96,11 @@ export const ProviderPanel: React.FC<{
|
|
|
97
96
|
const [slotProviderModels, setSlotProviderModels] = useState<Record<string, string[]>>({});
|
|
98
97
|
const [currentSlotName, setCurrentSlotName] = useState<string>('main');
|
|
99
98
|
const [slotLoadingMsg, setSlotLoadingMsg] = useState<string>('');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const [
|
|
99
|
+
|
|
100
|
+
// VS Code link status
|
|
101
|
+
const [vscodeLinked, setVscodeLinked] = useState(false);
|
|
102
|
+
const [vscodeLinkedPaths, setVscodeLinkedPaths] = useState<string[]>([]);
|
|
103
|
+
|
|
103
104
|
|
|
104
105
|
const base = apiUrl.replace(/\/+$/, '');
|
|
105
106
|
|
|
@@ -146,6 +147,16 @@ export const ProviderPanel: React.FC<{
|
|
|
146
147
|
loadPresets();
|
|
147
148
|
}, [loadProviders, loadPresets]);
|
|
148
149
|
|
|
150
|
+
// Fetch VS Code link status on mount
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
axios.get(`${base}/api/providers/link-vscode`)
|
|
153
|
+
.then(r => {
|
|
154
|
+
setVscodeLinked(r.data.linked);
|
|
155
|
+
setVscodeLinkedPaths(r.data.paths || []);
|
|
156
|
+
})
|
|
157
|
+
.catch(() => {});
|
|
158
|
+
}, [base]);
|
|
159
|
+
|
|
149
160
|
// Key processing for Page Up/Down and Arrow keys in scrolling lists
|
|
150
161
|
useEffect(() => {
|
|
151
162
|
const handler = (buf: Buffer) => {
|
|
@@ -320,6 +331,7 @@ export const ProviderPanel: React.FC<{
|
|
|
320
331
|
{ label: 'Add Provider', value: 'add' },
|
|
321
332
|
{ label: 'Edit Provider (Name/Key)', value: 'edit' },
|
|
322
333
|
{ label: 'Configure Slots', value: 'slots' },
|
|
334
|
+
{ label: vscodeLinked ? `Disconnect from VS Code (${vscodeLinkedPaths.length} editor(s))` : 'Connect to VS Code', value: 'link_vscode' },
|
|
323
335
|
{ label: 'Connectivity Test', value: 'test' },
|
|
324
336
|
{ label: 'Delete Provider', value: 'delete' },
|
|
325
337
|
{ label: 'Refresh', value: 'refresh' },
|
|
@@ -348,6 +360,33 @@ export const ProviderPanel: React.FC<{
|
|
|
348
360
|
setStage('slot_config');
|
|
349
361
|
setListOffset(0);
|
|
350
362
|
break;
|
|
363
|
+
case 'link_vscode':
|
|
364
|
+
setOpMsg(null);
|
|
365
|
+
setErr(null);
|
|
366
|
+
if (vscodeLinked) {
|
|
367
|
+
// Unlink
|
|
368
|
+
axios.delete(`${base}/api/providers/link-vscode`)
|
|
369
|
+
.then(() => {
|
|
370
|
+
setVscodeLinked(false);
|
|
371
|
+
setVscodeLinkedPaths([]);
|
|
372
|
+
setOpMsg('Disconnected from VS Code.');
|
|
373
|
+
})
|
|
374
|
+
.catch(e => {
|
|
375
|
+
setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Failed to disconnect VS Code');
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
// Link
|
|
379
|
+
axios.post(`${base}/api/providers/link-vscode`)
|
|
380
|
+
.then(r => {
|
|
381
|
+
setVscodeLinked(true);
|
|
382
|
+
setVscodeLinkedPaths(r.data.paths || []);
|
|
383
|
+
setOpMsg(`Connected to VS Code (${(r.data.paths || []).length} editor(s)).`);
|
|
384
|
+
})
|
|
385
|
+
.catch(e => {
|
|
386
|
+
setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Failed to connect VS Code');
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
351
390
|
case 'test':
|
|
352
391
|
setStage('test_select');
|
|
353
392
|
setListOffset(0);
|
|
@@ -780,10 +819,25 @@ export const ProviderPanel: React.FC<{
|
|
|
780
819
|
const sepIdx = val.indexOf('::');
|
|
781
820
|
const providerId = val.slice(0, sepIdx);
|
|
782
821
|
const modelId = val.slice(sepIdx + 2);
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
822
|
+
axios.put(`${base}/api/providers/slots/${currentSlotName}`, {
|
|
823
|
+
providerId,
|
|
824
|
+
modelId,
|
|
825
|
+
label: null,
|
|
826
|
+
})
|
|
827
|
+
.then(() => {
|
|
828
|
+
setSlotTable(prev => ({
|
|
829
|
+
...prev,
|
|
830
|
+
[currentSlotName]: { providerId, modelId, label: null }
|
|
831
|
+
}));
|
|
832
|
+
setOpMsg(`Configured [${currentSlotName}] -> ${modelId}`);
|
|
833
|
+
setErr(null);
|
|
834
|
+
setListOffset(0);
|
|
835
|
+
setStage('slot_config');
|
|
836
|
+
})
|
|
837
|
+
.catch(e => {
|
|
838
|
+
setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
|
|
839
|
+
setStage('slot_config');
|
|
840
|
+
});
|
|
787
841
|
}}
|
|
788
842
|
/>
|
|
789
843
|
</Box>
|
|
@@ -794,47 +848,6 @@ export const ProviderPanel: React.FC<{
|
|
|
794
848
|
);
|
|
795
849
|
}
|
|
796
850
|
|
|
797
|
-
if (stage === 'slot_input_label') {
|
|
798
|
-
return (
|
|
799
|
-
<Box flexDirection="column" flexGrow={1}>
|
|
800
|
-
<Title color="cyan">Configure Slot [{currentSlotName}] — Set Display Name</Title>
|
|
801
|
-
<Text>
|
|
802
|
-
Model: {providers.find(p => p.id === tempSlotProviderId)?.name || tempSlotProviderId} / {tempSlotModelId}
|
|
803
|
-
</Text>
|
|
804
|
-
<Box marginTop={1}>
|
|
805
|
-
<Text>Display Name (Label): </Text>
|
|
806
|
-
<TextInput
|
|
807
|
-
value={slotLabelInput}
|
|
808
|
-
onChange={setSlotLabelInput}
|
|
809
|
-
onSubmit={() => {
|
|
810
|
-
const label = slotLabelInput.trim() || tempSlotModelId;
|
|
811
|
-
axios.put(`${base}/api/providers/slots/${currentSlotName}`, {
|
|
812
|
-
providerId: tempSlotProviderId,
|
|
813
|
-
modelId: tempSlotModelId,
|
|
814
|
-
label,
|
|
815
|
-
})
|
|
816
|
-
.then(() => {
|
|
817
|
-
setSlotTable(prev => ({
|
|
818
|
-
...prev,
|
|
819
|
-
[currentSlotName]: { providerId: tempSlotProviderId, modelId: tempSlotModelId, label }
|
|
820
|
-
}));
|
|
821
|
-
setOpMsg(`Configured [${currentSlotName}] -> ${label}`);
|
|
822
|
-
setErr(null);
|
|
823
|
-
setListOffset(0);
|
|
824
|
-
setStage('slot_config');
|
|
825
|
-
})
|
|
826
|
-
.catch(e => {
|
|
827
|
-
setErr((e as any)?.response?.data?.message || (e as any)?.message || 'Save failed');
|
|
828
|
-
setStage('slot_config');
|
|
829
|
-
});
|
|
830
|
-
}}
|
|
831
|
-
/>
|
|
832
|
-
</Box>
|
|
833
|
-
<Hint>Enter: Save (Display name in UI) · ESC: Back to Models</Hint>
|
|
834
|
-
</Box>
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
851
|
return null;
|
|
839
852
|
};
|
|
840
853
|
|
|
@@ -16,22 +16,22 @@ export async function call(
|
|
|
16
16
|
const arg = (args ?? '').trim().toLowerCase()
|
|
17
17
|
const enable = arg !== 'off'
|
|
18
18
|
|
|
19
|
-
const result = updateSettingsForSource('userSettings', {
|
|
19
|
+
const result = updateSettingsForSource('userSettings', { brainMode: enable })
|
|
20
20
|
if (result.error) {
|
|
21
21
|
logError(result.error)
|
|
22
|
-
onDone(`Failed to ${enable ? 'enable' : 'disable'}
|
|
22
|
+
onDone(`Failed to ${enable ? 'enable' : 'disable'} Brain Mode: ${result.error.message}`, { display: 'system' })
|
|
23
23
|
return null
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
if (enable) {
|
|
27
|
-
context.setAppState(prev => ({ ...prev,
|
|
28
|
-
onDone('✓
|
|
27
|
+
context.setAppState(prev => ({ ...prev, brainMode: true }))
|
|
28
|
+
onDone('✓ Brain Mode enabled — thinking, deciding, orchestrating.', { display: 'system' })
|
|
29
29
|
} else {
|
|
30
|
-
context.setAppState(prev => ({ ...prev,
|
|
31
|
-
onDone('✗
|
|
30
|
+
context.setAppState(prev => ({ ...prev, brainMode: false }))
|
|
31
|
+
onDone('✗ Brain Mode disabled — back to full execution.', { display: 'system' })
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
logEvent('
|
|
34
|
+
logEvent('tengu_brain_mode_toggled', {
|
|
35
35
|
enabled: enable,
|
|
36
36
|
source: 'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
37
37
|
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from '../../commands.js'
|
|
2
|
+
|
|
3
|
+
const brain = {
|
|
4
|
+
type: 'local-jsx',
|
|
5
|
+
name: 'brain',
|
|
6
|
+
description:
|
|
7
|
+
'Brain mode — think, decide, orchestrate. Delegate execution to sub-agents.',
|
|
8
|
+
argumentHint: '[off]',
|
|
9
|
+
isEnabled: () => true,
|
|
10
|
+
immediate: true,
|
|
11
|
+
load: () => import('./brain.js'),
|
|
12
|
+
} satisfies Command
|
|
13
|
+
|
|
14
|
+
export default brain
|
package/src/commands.ts
CHANGED
|
@@ -124,7 +124,7 @@ import thinkbackPlay from './commands/thinkback-play/index.js'
|
|
|
124
124
|
import permissions from './commands/permissions/index.js'
|
|
125
125
|
import plan from './commands/plan/index.js'
|
|
126
126
|
import fast from './commands/fast/index.js'
|
|
127
|
-
import
|
|
127
|
+
import brain from './commands/brain/index.js'
|
|
128
128
|
import passes from './commands/passes/index.js'
|
|
129
129
|
import privacySettings from './commands/privacy-settings/index.js'
|
|
130
130
|
import hooks from './commands/hooks/index.js'
|
|
@@ -275,7 +275,7 @@ const COMMANDS = memoize((): Command[] => [
|
|
|
275
275
|
effort,
|
|
276
276
|
exit,
|
|
277
277
|
fast,
|
|
278
|
-
|
|
278
|
+
brain,
|
|
279
279
|
files,
|
|
280
280
|
heapDump,
|
|
281
281
|
help,
|
|
@@ -323,7 +323,7 @@ function PromptInput({
|
|
|
323
323
|
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession);
|
|
324
324
|
const thinkingEnabled = useAppState(s => s.thinkingEnabled);
|
|
325
325
|
const isFastMode = useAppState(s => isFastModeEnabled() ? s.fastMode : false);
|
|
326
|
-
const
|
|
326
|
+
const isBrainMode = useAppState(s => s.brainMode ?? false);
|
|
327
327
|
const effortValue = useAppState(s => s.effortValue);
|
|
328
328
|
const viewedTeammate = getViewedTeammateTask(store.getState());
|
|
329
329
|
const viewingAgentName = viewedTeammate?.identity.agentName;
|
|
@@ -2261,7 +2261,7 @@ function PromptInput({
|
|
|
2261
2261
|
</Box>
|
|
2262
2262
|
</Box>
|
|
2263
2263
|
<Text color={swarmBanner.bgColor}>{'─'.repeat(columns)}</Text>
|
|
2264
|
-
</> : <Box flexDirection="row" alignItems="flex-start" justifyContent="flex-start" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%" borderText={buildBorderText(showFastIcon ?? false, showFastIconHint, fastModeCooldown,
|
|
2264
|
+
</> : <Box flexDirection="row" alignItems="flex-start" justifyContent="flex-start" borderColor={getBorderColor()} borderStyle="round" borderLeft={false} borderRight={false} borderBottom width="100%" borderText={buildBorderText(showFastIcon ?? false, showFastIconHint, fastModeCooldown, isBrainMode)}>
|
|
2265
2265
|
<PromptInputModeIndicator mode={mode} isLoading={isLoading} viewingAgentName={viewingAgentName} viewingAgentColor={viewingAgentColor} />
|
|
2266
2266
|
<Box flexGrow={1} flexShrink={1} onClick={handleInputClick}>
|
|
2267
2267
|
{textInputElement}
|
|
@@ -2321,13 +2321,13 @@ function getInitialPasteId(messages: Message[]): number {
|
|
|
2321
2321
|
}
|
|
2322
2322
|
return maxId + 1;
|
|
2323
2323
|
}
|
|
2324
|
-
function buildBorderText(showFastIcon: boolean, showFastIconHint: boolean, fastModeCooldown: boolean,
|
|
2325
|
-
if (!showFastIcon && !
|
|
2324
|
+
function buildBorderText(showFastIcon: boolean, showFastIconHint: boolean, fastModeCooldown: boolean, isBrainMode: boolean): BorderTextOptions | undefined {
|
|
2325
|
+
if (!showFastIcon && !isBrainMode) return undefined
|
|
2326
2326
|
const segments: string[] = []
|
|
2327
2327
|
if (showFastIcon) {
|
|
2328
2328
|
segments.push(showFastIconHint ? `${getFastIconString(true, fastModeCooldown)} ${chalk.dim('/fast')}` : getFastIconString(true, fastModeCooldown))
|
|
2329
2329
|
}
|
|
2330
|
-
if (
|
|
2330
|
+
if (isBrainMode) {
|
|
2331
2331
|
segments.push(chalk.yellow('» EXEC'))
|
|
2332
2332
|
}
|
|
2333
2333
|
return { content: ` ${segments.join(' ')} `, position: 'top', align: 'end', offset: 0 }
|
package/src/constants/prompts.ts
CHANGED
|
@@ -564,7 +564,7 @@ ${CYBER_RISK_INSTRUCTION}`,
|
|
|
564
564
|
...(feature('KAIROS') || feature('KAIROS_BRIEF')
|
|
565
565
|
? [systemPromptSection('brief', () => getBriefSection())]
|
|
566
566
|
: []),
|
|
567
|
-
DANGEROUS_uncachedSystemPromptSection('
|
|
567
|
+
DANGEROUS_uncachedSystemPromptSection('brain_policy', () => getBrainPolicySection(), 'toggles mid-session via /brain'),
|
|
568
568
|
]
|
|
569
569
|
|
|
570
570
|
const resolvedDynamicSections =
|
|
@@ -926,15 +926,50 @@ The user context may include a \`terminalFocus\` field indicating whether the us
|
|
|
926
926
|
- **Focused**: The user is watching. Be more collaborative — surface choices, ask before committing to large changes, and keep your output concise so it's easy to follow in real time.${BRIEF_PROACTIVE_SECTION && briefToolModule?.isBriefEnabled() ? `\n\n${BRIEF_PROACTIVE_SECTION}` : ''}`
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
-
function
|
|
929
|
+
function getBrainPolicySection(): string | null {
|
|
930
930
|
const settings = getInitialSettings()
|
|
931
|
-
if (!settings.
|
|
932
|
-
return `#
|
|
931
|
+
if (!settings.brainMode) return null
|
|
932
|
+
return `# Brain Mode (/brain)
|
|
933
933
|
|
|
934
|
-
|
|
934
|
+
You are the coordinator — the brain, not the hands. Your context window is precious
|
|
935
|
+
strategic real estate. Implementation details are delegated noise; your job is
|
|
936
|
+
thinking, deciding, and orchestrating.
|
|
935
937
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
938
|
+
## Hard Constraints
|
|
939
|
+
|
|
940
|
+
**You MUST NOT execute work directly.** Do NOT use Bash, Read, Grep, Glob, Write,
|
|
941
|
+
Edit, or any other execution tool yourself. Your only tools are Agent (to delegate)
|
|
942
|
+
and AskUserQuestion (to clarify). Everything else is forbidden.
|
|
943
|
+
|
|
944
|
+
**Every task goes through sub-agents.** Even a single file read, even a one-line
|
|
945
|
+
grep — delegate it. Your context must stay clean for strategic reasoning.
|
|
946
|
+
|
|
947
|
+
**When in doubt, spawn an agent.** If you're about to reach for a tool, stop.
|
|
948
|
+
Write a prompt for an agent instead.
|
|
949
|
+
|
|
950
|
+
## Workflow
|
|
951
|
+
|
|
952
|
+
1. **Analyze** — understand the user's request at the system level. What's the real
|
|
953
|
+
goal? What decisions need to be made?
|
|
954
|
+
2. **Decompose** — break into independent workstreams. Each workstream becomes an
|
|
955
|
+
agent prompt with clear scope, context, and expected output.
|
|
956
|
+
3. **Parallelize** — launch all independent agents simultaneously. Never serialize
|
|
957
|
+
what can run in parallel.
|
|
958
|
+
4. **Integrate** — synthesize agent results. Make decisions. Report to user.
|
|
959
|
+
5. **Iterate** — if results reveal new questions, spawn another round of agents.
|
|
960
|
+
|
|
961
|
+
## Agent Prompt Quality
|
|
962
|
+
|
|
963
|
+
Each agent prompt must include:
|
|
964
|
+
- What we're trying to accomplish and why
|
|
965
|
+
- What's already known / ruled out
|
|
966
|
+
- Specific files, line numbers, commands — never "investigate the bug"
|
|
967
|
+
- Expected output format (short report, code change, test results)
|
|
968
|
+
- Whether to write code or only research
|
|
969
|
+
|
|
970
|
+
## Output Style
|
|
971
|
+
|
|
972
|
+
Report compressed: decisions made, blockers hit, next steps. Never narrate what
|
|
973
|
+
agents are doing moment-by-moment. "Agent A found X, agent B confirmed Y. Decision:
|
|
974
|
+
rollback needed." is enough.`
|
|
940
975
|
}
|
package/src/main.tsx
CHANGED
|
@@ -2640,7 +2640,7 @@ async function run(): Promise<CommanderCommand> {
|
|
|
2640
2640
|
...(isFastModeEnabled() && {
|
|
2641
2641
|
fastMode: getInitialFastModeSetting(effectiveModel ?? null)
|
|
2642
2642
|
}),
|
|
2643
|
-
|
|
2643
|
+
brainMode: getInitialSettings().brainMode === true,
|
|
2644
2644
|
...(isAdvisorEnabled() && advisorModel && {
|
|
2645
2645
|
advisorModel
|
|
2646
2646
|
}),
|
|
@@ -26,6 +26,9 @@ import type { SlotName } from '../types/provider.js'
|
|
|
26
26
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
27
27
|
|
|
28
28
|
const providerService = new ProviderService()
|
|
29
|
+
providerService.init().catch((err) =>
|
|
30
|
+
console.error('[ProviderService] init failed:', err),
|
|
31
|
+
)
|
|
29
32
|
|
|
30
33
|
function maskApiKey(key: string): string {
|
|
31
34
|
if (key.length <= 8) return '****'
|
|
@@ -76,6 +79,23 @@ export async function handleProvidersApi(
|
|
|
76
79
|
return Response.json(slots)
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
// /api/providers/link-vscode — manage VS Code linking
|
|
83
|
+
if (id === 'link-vscode' && !action) {
|
|
84
|
+
if (req.method === 'GET') {
|
|
85
|
+
const status = await providerService.getVscodeStatus()
|
|
86
|
+
return Response.json(status)
|
|
87
|
+
}
|
|
88
|
+
if (req.method === 'POST') {
|
|
89
|
+
const result = await providerService.linkVscode()
|
|
90
|
+
return Response.json(result)
|
|
91
|
+
}
|
|
92
|
+
if (req.method === 'DELETE') {
|
|
93
|
+
const result = await providerService.unlinkVscode()
|
|
94
|
+
return Response.json(result)
|
|
95
|
+
}
|
|
96
|
+
throw methodNotAllowed(req.method)
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
// PUT /api/providers/slots/:slotName — set one slot
|
|
80
100
|
if (id === 'slots' && action && req.method === 'PUT') {
|
|
81
101
|
const parsed = SlotNameSchema.safeParse(action)
|
|
@@ -41,6 +41,44 @@ const MANAGED_ENV_KEYS = [
|
|
|
41
41
|
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
42
42
|
] as const
|
|
43
43
|
|
|
44
|
+
const CLAUDE_DEFAULT_LABELS: Record<SlotName, string> = {
|
|
45
|
+
main: 'claude-sonnet-4-5',
|
|
46
|
+
haiku: 'claude-haiku-4-5',
|
|
47
|
+
sonnet: 'claude-sonnet-4-5',
|
|
48
|
+
opus: 'claude-opus-4-7',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const VSCODE_EDITOR_NAMES = ['Code', 'Code - Insiders', 'Cursor'] as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return candidate VS Code settings.json paths for the current platform.
|
|
55
|
+
* These are checked for existence (editor directory must exist) before writing.
|
|
56
|
+
*/
|
|
57
|
+
function getVscodeSettingsPaths(): string[] {
|
|
58
|
+
const home = os.homedir()
|
|
59
|
+
const paths: string[] = []
|
|
60
|
+
|
|
61
|
+
if (process.platform === 'win32') {
|
|
62
|
+
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming')
|
|
63
|
+
for (const name of VSCODE_EDITOR_NAMES) {
|
|
64
|
+
paths.push(path.join(appData, name, 'User', 'settings.json'))
|
|
65
|
+
}
|
|
66
|
+
} else if (process.platform === 'darwin') {
|
|
67
|
+
const lib = path.join(home, 'Library', 'Application Support')
|
|
68
|
+
for (const name of VSCODE_EDITOR_NAMES) {
|
|
69
|
+
paths.push(path.join(lib, name, 'User', 'settings.json'))
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Linux / other Unix
|
|
73
|
+
const config = path.join(home, '.config')
|
|
74
|
+
for (const name of VSCODE_EDITOR_NAMES) {
|
|
75
|
+
paths.push(path.join(config, name, 'User', 'settings.json'))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return paths
|
|
80
|
+
}
|
|
81
|
+
|
|
44
82
|
const DEFAULT_INDEX: ProvidersIndex = { activeId: null, providers: [] }
|
|
45
83
|
|
|
46
84
|
export class ProviderService {
|
|
@@ -476,12 +514,172 @@ export class ProviderService {
|
|
|
476
514
|
const index = await this.readIndex()
|
|
477
515
|
const provider = index.providers.find((p) => p.id === entry.providerId)
|
|
478
516
|
if (!provider) return null
|
|
517
|
+
|
|
518
|
+
// Auto-fill Claude model names as label when user hasn't set one.
|
|
519
|
+
// This lets closed-source clients (e.g. VS Code extension) see Claude
|
|
520
|
+
// model names while the CLI continues to show the real upstream modelId
|
|
521
|
+
// (syncSettingsForSlots reads raw entry.label, which stays null).
|
|
479
522
|
return {
|
|
480
523
|
baseUrl: provider.baseUrl,
|
|
481
524
|
apiKey: provider.apiKey,
|
|
482
525
|
apiFormat: provider.apiFormat ?? 'anthropic',
|
|
483
526
|
modelId: entry.modelId,
|
|
484
|
-
label: entry.label,
|
|
527
|
+
label: entry.label || CLAUDE_DEFAULT_LABELS[slotName],
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// --- VS Code linking ---
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Build the environmentVariables array for VS Code claudeCode settings
|
|
535
|
+
* based on current slot configuration. Uses user-set labels or falls back
|
|
536
|
+
* to Claude default model names so the VS Code extension shows the right labels.
|
|
537
|
+
*/
|
|
538
|
+
private async buildVscodeEnvVars(): Promise<Array<{ name: string; value: string }>> {
|
|
539
|
+
const slots = await this.readSlots()
|
|
540
|
+
const index = await this.readIndex()
|
|
541
|
+
|
|
542
|
+
function getSlotLabel(slotName: SlotName): string | null {
|
|
543
|
+
const entry = slots[slotName]
|
|
544
|
+
if (!entry) return null
|
|
545
|
+
const provider = index.providers.find((p) => p.id === entry.providerId)
|
|
546
|
+
if (!provider) return null
|
|
547
|
+
return entry.label || CLAUDE_DEFAULT_LABELS[slotName]
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const envVars: Array<{ name: string; value: string }> = [
|
|
551
|
+
{ name: 'ANTHROPIC_BASE_URL', value: `http://127.0.0.1:${ProviderService.serverPort}/proxy` },
|
|
552
|
+
{ name: 'ANTHROPIC_AUTH_TOKEN', value: 'proxy-managed' },
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
const mainLabel = getSlotLabel("main")
|
|
556
|
+
if (mainLabel) {
|
|
557
|
+
envVars.push({ name: "ANTHROPIC_MODEL", value: mainLabel })
|
|
558
|
+
}
|
|
559
|
+
const haikuLabel = getSlotLabel("haiku")
|
|
560
|
+
if (haikuLabel) {
|
|
561
|
+
envVars.push({ name: "ANTHROPIC_DEFAULT_HAIKU_MODEL", value: haikuLabel })
|
|
562
|
+
}
|
|
563
|
+
const sonnetLabel = getSlotLabel("sonnet")
|
|
564
|
+
if (sonnetLabel) {
|
|
565
|
+
envVars.push({ name: "ANTHROPIC_DEFAULT_SONNET_MODEL", value: sonnetLabel })
|
|
566
|
+
}
|
|
567
|
+
const opusLabel = getSlotLabel("opus")
|
|
568
|
+
if (opusLabel) {
|
|
569
|
+
envVars.push({ name: "ANTHROPIC_DEFAULT_OPUS_MODEL", value: opusLabel })
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return envVars
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Write claudeCode settings into ALL detected VS Code editor instances
|
|
577
|
+
* (stable, Insiders, Cursor). Persists linked state in bingo settings so
|
|
578
|
+
* it survives restarts.
|
|
579
|
+
*/
|
|
580
|
+
async linkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
581
|
+
const envVars = await this.buildVscodeEnvVars()
|
|
582
|
+
const candidatePaths = getVscodeSettingsPaths()
|
|
583
|
+
const writtenPaths: string[] = []
|
|
584
|
+
|
|
585
|
+
for (const settingsPath of candidatePaths) {
|
|
586
|
+
// Only write if the editor config directory exists (editor is installed)
|
|
587
|
+
const editorDir = path.dirname(path.dirname(settingsPath))
|
|
588
|
+
try {
|
|
589
|
+
await fs.access(editorDir)
|
|
590
|
+
} catch {
|
|
591
|
+
continue
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let settings: Record<string, unknown> = {}
|
|
595
|
+
try {
|
|
596
|
+
const raw = await fs.readFile(settingsPath, "utf-8")
|
|
597
|
+
settings = JSON.parse(raw)
|
|
598
|
+
} catch {
|
|
599
|
+
// File does not exist yet — start fresh
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
settings.claudeCode = {
|
|
603
|
+
...(settings.claudeCode as Record<string, unknown> || {}),
|
|
604
|
+
preferredLocation: "panel",
|
|
605
|
+
disableLoginPrompt: true,
|
|
606
|
+
environmentVariables: envVars,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true })
|
|
610
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
|
|
611
|
+
writtenPaths.push(settingsPath)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (writtenPaths.length === 0) {
|
|
615
|
+
throw ApiError.badRequest(
|
|
616
|
+
'No VS Code installation detected. Please install VS Code (stable, Insiders, or Cursor) first.',
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Persist so re-sync on restart and unlink work correctly
|
|
621
|
+
const bingoSettings = await this.readSettings()
|
|
622
|
+
bingoSettings.vscodeLinked = true
|
|
623
|
+
bingoSettings.vscodeLinkedPaths = writtenPaths
|
|
624
|
+
await this.writeSettings(bingoSettings)
|
|
625
|
+
|
|
626
|
+
return { paths: writtenPaths, linked: true }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Remove claudeCode keys from all previously-linked VS Code settings files.
|
|
631
|
+
* Does NOT delete the entire file — it only removes the claudeCode key.
|
|
632
|
+
*/
|
|
633
|
+
async unlinkVscode(): Promise<{ paths: string[]; linked: boolean }> {
|
|
634
|
+
const bingoSettings = await this.readSettings()
|
|
635
|
+
const linkedPaths = (bingoSettings.vscodeLinkedPaths as string[]) || []
|
|
636
|
+
|
|
637
|
+
for (const settingsPath of linkedPaths) {
|
|
638
|
+
try {
|
|
639
|
+
const raw = await fs.readFile(settingsPath, "utf-8")
|
|
640
|
+
const settings = JSON.parse(raw)
|
|
641
|
+
delete settings.claudeCode
|
|
642
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 4) + '\n')
|
|
643
|
+
} catch {
|
|
644
|
+
// File may have been moved or deleted since linking — harmless
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
bingoSettings.vscodeLinked = false
|
|
649
|
+
delete bingoSettings.vscodeLinkedPaths
|
|
650
|
+
await this.writeSettings(bingoSettings)
|
|
651
|
+
|
|
652
|
+
return { paths: linkedPaths, linked: false }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Return current VS Code link status from persisted bingo settings.
|
|
657
|
+
*/
|
|
658
|
+
async getVscodeStatus(): Promise<{ linked: boolean; paths: string[] }> {
|
|
659
|
+
const bingoSettings = await this.readSettings()
|
|
660
|
+
return {
|
|
661
|
+
linked: Boolean(bingoSettings.vscodeLinked),
|
|
662
|
+
paths: (bingoSettings.vscodeLinkedPaths as string[]) || [],
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Initialise the service. If vscodeLinked is true in bingo settings,
|
|
668
|
+
* re-sync VS Code settings so they stay current with the slot config.
|
|
669
|
+
* Should be called once after server startup.
|
|
670
|
+
*/
|
|
671
|
+
async init(): Promise<void> {
|
|
672
|
+
const status = await this.getVscodeStatus()
|
|
673
|
+
if (status.linked) {
|
|
674
|
+
try {
|
|
675
|
+
await this.linkVscode()
|
|
676
|
+
} catch {
|
|
677
|
+
// VS Code may have been uninstalled — clear stale state
|
|
678
|
+
const bingoSettings = await this.readSettings()
|
|
679
|
+
bingoSettings.vscodeLinked = false
|
|
680
|
+
delete bingoSettings.vscodeLinkedPaths
|
|
681
|
+
await this.writeSettings(bingoSettings)
|
|
682
|
+
}
|
|
485
683
|
}
|
|
486
684
|
}
|
|
487
685
|
|
|
@@ -421,8 +421,8 @@ export type AppState = DeepImmutable<{
|
|
|
421
421
|
activeOverlays: ReadonlySet<string>
|
|
422
422
|
// Fast mode
|
|
423
423
|
fastMode?: boolean
|
|
424
|
-
//
|
|
425
|
-
|
|
424
|
+
// Brain mode
|
|
425
|
+
brainMode?: boolean
|
|
426
426
|
// Advisor model for server-side advisor tool (undefined = disabled).
|
|
427
427
|
advisorModel?: string
|
|
428
428
|
// Effort value
|
|
@@ -567,6 +567,6 @@ export function getDefaultAppState(): AppState {
|
|
|
567
567
|
effortValue: undefined,
|
|
568
568
|
activeOverlays: new Set<string>(),
|
|
569
569
|
fastMode: false,
|
|
570
|
-
|
|
570
|
+
brainMode: false,
|
|
571
571
|
}
|
|
572
572
|
}
|
package/src/utils/attachments.ts
CHANGED
|
@@ -915,8 +915,8 @@ export async function getAttachments(
|
|
|
915
915
|
maybe('critical_system_reminder', () =>
|
|
916
916
|
Promise.resolve(getCriticalSystemReminderAttachment(toolUseContext)),
|
|
917
917
|
),
|
|
918
|
-
maybe('
|
|
919
|
-
Promise.resolve(
|
|
918
|
+
maybe('brain_mode_reminder', () =>
|
|
919
|
+
Promise.resolve(getBrainModeReminderAttachment()),
|
|
920
920
|
),
|
|
921
921
|
...(feature('COMPACTION_REMINDERS')
|
|
922
922
|
? [
|
|
@@ -1593,15 +1593,15 @@ function getCriticalSystemReminderAttachment(
|
|
|
1593
1593
|
return [{ type: 'critical_system_reminder', content: reminder }]
|
|
1594
1594
|
}
|
|
1595
1595
|
|
|
1596
|
-
function
|
|
1597
|
-
if (!getInitialSettings().
|
|
1596
|
+
function getBrainModeReminderAttachment(): Attachment[] {
|
|
1597
|
+
if (!getInitialSettings().brainMode) {
|
|
1598
1598
|
return []
|
|
1599
1599
|
}
|
|
1600
1600
|
return [
|
|
1601
1601
|
{
|
|
1602
1602
|
type: 'system_reminder' as const,
|
|
1603
1603
|
content:
|
|
1604
|
-
'/
|
|
1604
|
+
'/brain active — Only use Agent and AskUserQuestion. Do NOT use Bash/Read/Grep/Glob/Write/Edit directly. Delegate everything.',
|
|
1605
1605
|
},
|
|
1606
1606
|
]
|
|
1607
1607
|
}
|
|
@@ -725,11 +725,11 @@ export const SettingsSchema = lazySchema(() =>
|
|
|
725
725
|
.describe(
|
|
726
726
|
'When true, fast mode does not persist across sessions. Each session starts with fast mode off.',
|
|
727
727
|
),
|
|
728
|
-
|
|
728
|
+
brainMode: z
|
|
729
729
|
.boolean()
|
|
730
730
|
.optional()
|
|
731
731
|
.describe(
|
|
732
|
-
'
|
|
732
|
+
'Coordinator mode — delegates all execution to sub-agents via Agent tool, restricts direct tool use to protect context.',
|
|
733
733
|
),
|
|
734
734
|
promptSuggestionEnabled: z
|
|
735
735
|
.boolean()
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Command } from '../../commands.js'
|
|
2
|
-
|
|
3
|
-
const exec = {
|
|
4
|
-
type: 'local-jsx',
|
|
5
|
-
name: 'exec',
|
|
6
|
-
description:
|
|
7
|
-
'Enable execution policy — dispatch-first, context protection, compressed output',
|
|
8
|
-
availability: ['claude-ai', 'console'],
|
|
9
|
-
aliases: ['executor'],
|
|
10
|
-
argumentHint: '[off]',
|
|
11
|
-
isEnabled: () => true,
|
|
12
|
-
immediate: true,
|
|
13
|
-
load: () => import('./exec.js'),
|
|
14
|
-
} satisfies Command
|
|
15
|
-
|
|
16
|
-
export default exec
|