Merge branch 'main' into feat/browser-allowed-domain

This commit is contained in:
cynthialong0-0
2026-03-10 06:51:21 -07:00
committed by GitHub
23 changed files with 542 additions and 90 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ runs:
INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}' INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}'
- name: '📦 Prepare bundled CLI for npm release' - name: '📦 Prepare bundled CLI for npm release'
if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/'" if: "inputs.npm-registry-url != 'https://npm.pkg.github.com/' && inputs.npm-tag != 'latest'"
working-directory: '${{ inputs.working-directory }}' working-directory: '${{ inputs.working-directory }}'
shell: 'bash' shell: 'bash'
run: | run: |
+21
View File
@@ -150,6 +150,27 @@ Plan Mode's default tool restrictions are managed by the
but you can customize these rules by creating your own policies in your but you can customize these rules by creating your own policies in your
`~/.gemini/policies/` directory (Tier 2). `~/.gemini/policies/` directory (Tier 2).
#### Global vs. mode-specific rules
As described in the
[policy engine documentation](../reference/policy-engine.md#approval-modes), any
rule that does not explicitly specify `modes` is considered "always active" and
will apply to Plan Mode as well.
If you want a rule to apply to other modes but _not_ to Plan Mode, you must
explicitly specify the target modes. For example, to allow `npm test` in default
and Auto-Edit modes but not in Plan Mode:
```toml
[[rule]]
toolName = "run_shell_command"
commandPrefix = "npm test"
decision = "allow"
priority = 100
# By omitting "plan", this rule will not be active in Plan Mode.
modes = ["default", "autoEdit"]
```
#### Example: Automatically approve read-only MCP tools #### Example: Automatically approve read-only MCP tools
By default, read-only MCP tools require user confirmation in Plan Mode. You can By default, read-only MCP tools require user confirmation in Plan Mode. You can
+19 -11
View File
@@ -106,6 +106,7 @@ export interface FooterRowItem {
flexGrow?: number; flexGrow?: number;
flexShrink?: number; flexShrink?: number;
isFocused?: boolean; isFocused?: boolean;
alignItems?: 'flex-start' | 'center' | 'flex-end';
} }
const COLUMN_GAP = 3; const COLUMN_GAP = 3;
@@ -117,10 +118,17 @@ export const FooterRow: React.FC<{
const elements: React.ReactNode[] = []; const elements: React.ReactNode[] = [];
items.forEach((item, idx) => { items.forEach((item, idx) => {
if (idx > 0 && !showLabels) { if (idx > 0) {
elements.push( elements.push(
<Box key={`sep-${item.key}`} height={1}> <Box
<Text color={theme.ui.comment}> · </Text> key={`sep-${item.key}`}
flexGrow={1}
flexShrink={1}
minWidth={showLabels ? COLUMN_GAP : 3}
justifyContent="center"
alignItems="center"
>
{!showLabels && <Text color={theme.ui.comment}> · </Text>}
</Box>, </Box>,
); );
} }
@@ -131,6 +139,7 @@ export const FooterRow: React.FC<{
flexDirection="column" flexDirection="column"
flexGrow={item.flexGrow ?? 0} flexGrow={item.flexGrow ?? 0}
flexShrink={item.flexShrink ?? 1} flexShrink={item.flexShrink ?? 1}
alignItems={item.alignItems}
backgroundColor={item.isFocused ? theme.background.focus : undefined} backgroundColor={item.isFocused ? theme.background.focus : undefined}
> >
{showLabels && ( {showLabels && (
@@ -148,12 +157,7 @@ export const FooterRow: React.FC<{
}); });
return ( return (
<Box <Box flexDirection="row" flexWrap="nowrap" width="100%">
flexDirection="row"
flexWrap="nowrap"
width="100%"
columnGap={showLabels ? COLUMN_GAP : 0}
>
{elements} {elements}
</Box> </Box>
); );
@@ -441,8 +445,9 @@ export const Footer: React.FC = () => {
} }
} }
const rowItems: FooterRowItem[] = columnsToRender.map((col) => { const rowItems: FooterRowItem[] = columnsToRender.map((col, index) => {
const isWorkspace = col.id === 'workspace'; const isWorkspace = col.id === 'workspace';
const isLast = index === columnsToRender.length - 1;
// Calculate exact space available for growth to prevent over-estimation truncation // Calculate exact space available for growth to prevent over-estimation truncation
const otherItemsWidth = columnsToRender const otherItemsWidth = columnsToRender
@@ -464,8 +469,10 @@ export const Footer: React.FC = () => {
key: col.id, key: col.id,
header: col.header, header: col.header,
element: col.element(estimatedWidth), element: col.element(estimatedWidth),
flexGrow: isWorkspace ? 1 : 0, flexGrow: 0,
flexShrink: isWorkspace ? 1 : 0, flexShrink: isWorkspace ? 1 : 0,
alignItems:
isLast && !droppedAny && index > 0 ? 'flex-end' : 'flex-start',
}; };
}); });
@@ -476,6 +483,7 @@ export const Footer: React.FC = () => {
element: <Text color={theme.ui.comment}>…</Text>, element: <Text color={theme.ui.comment}>…</Text>,
flexGrow: 0, flexGrow: 0,
flexShrink: 0, flexShrink: 0,
alignItems: 'flex-end',
}); });
} }
@@ -9,6 +9,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js'; import { waitFor } from '../../test-utils/async.js';
import { FooterConfigDialog } from './FooterConfigDialog.js'; import { FooterConfigDialog } from './FooterConfigDialog.js';
import { createMockSettings } from '../../test-utils/settings.js'; import { createMockSettings } from '../../test-utils/settings.js';
import { ALL_ITEMS } from '../../config/footerItems.js';
import { act } from 'react'; import { act } from 'react';
describe('<FooterConfigDialog />', () => { describe('<FooterConfigDialog />', () => {
@@ -213,4 +214,60 @@ describe('<FooterConfigDialog />', () => {
expect(bIdxAfter).toBeLessThan(wIdxAfter); expect(bIdxAfter).toBeLessThan(wIdxAfter);
}); });
}); });
it('updates the preview when Show footer labels is toggled off', async () => {
const settings = createMockSettings();
const renderResult = renderWithProviders(
<FooterConfigDialog onClose={mockOnClose} />,
{ settings },
);
const { lastFrame, stdin, waitUntilReady } = renderResult;
await waitUntilReady();
// By default labels are on
expect(lastFrame()).toContain('workspace (/directory)');
expect(lastFrame()).toContain('sandbox');
expect(lastFrame()).toContain('/model');
// Move to "Show footer labels" (which is the second to last item)
for (let i = 0; i < ALL_ITEMS.length; i++) {
act(() => {
stdin.write('\u001b[B'); // Down arrow
});
}
await waitFor(() => {
expect(lastFrame()).toMatch(/> \[✓\] Show footer labels/);
});
// Toggle it off
act(() => {
stdin.write('\r');
});
await waitFor(() => {
expect(lastFrame()).toMatch(/> \[ \] Show footer labels/);
// The headers should no longer be in the preview
expect(lastFrame()).not.toContain('workspace (/directory)');
expect(lastFrame()).not.toContain('/model');
// We can't strictly search for "sandbox" because the menu item also says "sandbox".
// Let's assert that the spacer dots are now present in the preview instead.
const previewLine =
lastFrame()
.split('\n')
.find((line) => line.includes('Preview:')) || '';
const nextLine =
lastFrame().split('\n')[
lastFrame().split('\n').indexOf(previewLine) + 1
] || '';
expect(nextLine).toContain('·');
expect(nextLine).toContain('~/project/path');
expect(nextLine).toContain('docker');
expect(nextLine).toContain('97%');
});
await expect(renderResult).toMatchSvgSnapshot();
});
}); });
@@ -266,7 +266,7 @@ export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
key: id, key: id,
header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id, header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id,
element: mockData[id], element: mockData[id],
flexGrow: 1, flexGrow: 0,
isFocused: id === focusKey, isFocused: id === focusKey,
})); }));
@@ -125,28 +125,27 @@
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="597" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text> <text x="45" y="597" fill="#afafaf" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<text x="288" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text> <text x="297" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="396" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text> <text x="405" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="504" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text> <text x="513" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="675" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text> <text x="693" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<rect x="783" y="595" width="36" height="17" fill="#001a00" /> <rect x="801" y="595" width="36" height="17" fill="#001a00" />
<text x="783" y="597" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text> <text x="801" y="597" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">diff</text>
<rect x="819" y="595" width="36" height="17" fill="#001a00" /> <rect x="837" y="595" width="18" height="17" fill="#001a00" />
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text> <text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="288" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text> <text x="297" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="396" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text> <text x="405" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="504" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text> <text x="513" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="675" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text> <text x="693" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<rect x="783" y="612" width="27" height="17" fill="#001a00" /> <rect x="801" y="612" width="27" height="17" fill="#001a00" />
<text x="783" y="614" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text> <text x="801" y="614" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">+12</text>
<rect x="810" y="612" width="9" height="17" fill="#001a00" /> <rect x="828" y="612" width="9" height="17" fill="#001a00" />
<rect x="819" y="612" width="18" height="17" fill="#001a00" />
<text x="819" y="614" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
<rect x="837" y="612" width="18" height="17" fill="#001a00" /> <rect x="837" y="612" width="18" height="17" fill="#001a00" />
<text x="837" y="614" fill="#ff87af" textLength="18" lengthAdjust="spacingAndGlyphs">-4</text>
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -126,22 +126,21 @@
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="595" width="198" height="17" fill="#001a00" /> <rect x="45" y="595" width="198" height="17" fill="#001a00" />
<text x="45" y="597" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text> <text x="45" y="597" fill="#ffffff" textLength="198" lengthAdjust="spacingAndGlyphs">workspace (/directory)</text>
<rect x="243" y="595" width="45" height="17" fill="#001a00" /> <text x="324" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text>
<text x="315" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">branch</text> <text x="459" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text>
<text x="432" y="597" fill="#afafaf" textLength="63" lengthAdjust="spacingAndGlyphs">sandbox</text> <text x="594" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text>
<text x="567" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/model</text> <text x="801" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<text x="756" y="597" fill="#afafaf" textLength="54" lengthAdjust="spacingAndGlyphs">/stats</text>
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="27" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="45" y="612" width="126" height="17" fill="#001a00" /> <rect x="45" y="612" width="126" height="17" fill="#001a00" />
<text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text> <text x="45" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<rect x="171" y="612" width="117" height="17" fill="#001a00" /> <rect x="171" y="612" width="72" height="17" fill="#001a00" />
<text x="315" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text> <text x="324" y="614" fill="#ffffff" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="432" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text> <text x="459" y="614" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="567" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text> <text x="594" y="614" fill="#ffffff" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="756" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text> <text x="801" y="614" fill="#ffffff" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="864" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text> <text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,143 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="683" viewBox="0 0 920 683">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="683" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="36" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs" font-weight="bold">Configure Footer</text>
<text x="891" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="53" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="70" fill="#afafaf" textLength="396" lengthAdjust="spacingAndGlyphs">Select which items to display in the footer.</text>
<text x="891" y="70" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="87" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="104" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="104" fill="#ffffff" textLength="90" lengthAdjust="spacingAndGlyphs"> workspace</text>
<text x="891" y="104" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="121" fill="#afafaf" textLength="234" lengthAdjust="spacingAndGlyphs"> Current working directory</text>
<text x="891" y="121" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="138" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="138" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs"> git-branch</text>
<text x="891" y="138" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="155" fill="#afafaf" textLength="477" lengthAdjust="spacingAndGlyphs"> Current git branch name (not shown when unavailable)</text>
<text x="891" y="155" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="172" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="172" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs"> sandbox</text>
<text x="891" y="172" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="189" fill="#afafaf" textLength="297" lengthAdjust="spacingAndGlyphs"> Sandbox type and trust indicator</text>
<text x="891" y="189" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="206" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="206" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs"> model-name</text>
<text x="891" y="206" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="223" fill="#afafaf" textLength="225" lengthAdjust="spacingAndGlyphs"> Current model identifier</text>
<text x="891" y="223" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="240" fill="#d7ffd7" textLength="27" lengthAdjust="spacingAndGlyphs">[✓]</text>
<text x="72" y="240" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs"> quota</text>
<text x="891" y="240" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="257" fill="#afafaf" textLength="540" lengthAdjust="spacingAndGlyphs"> Remaining usage on daily limit (not shown when unavailable)</text>
<text x="891" y="257" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="274" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="274" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> context-used</text>
<text x="891" y="274" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="291" fill="#afafaf" textLength="306" lengthAdjust="spacingAndGlyphs"> Percentage of context window used</text>
<text x="891" y="291" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="308" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="308" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> memory-usage</text>
<text x="891" y="308" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="325" fill="#afafaf" textLength="279" lengthAdjust="spacingAndGlyphs"> Memory used by the application</text>
<text x="891" y="325" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="342" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="342" fill="#ffffff" textLength="99" lengthAdjust="spacingAndGlyphs"> session-id</text>
<text x="891" y="342" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="359" fill="#afafaf" textLength="378" lengthAdjust="spacingAndGlyphs"> Unique identifier for the current session</text>
<text x="891" y="359" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="376" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="376" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs"> code-changes</text>
<text x="891" y="376" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="393" fill="#afafaf" textLength="513" lengthAdjust="spacingAndGlyphs"> Lines added/removed in the session (not shown when zero)</text>
<text x="891" y="393" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="410" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<text x="72" y="410" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs"> token-count</text>
<text x="891" y="410" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="427" fill="#afafaf" textLength="495" lengthAdjust="spacingAndGlyphs"> Total tokens used in the session (not shown when zero)</text>
<text x="891" y="427" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="442" width="9" height="17" fill="#001a00" />
<text x="27" y="444" fill="#00cd00" textLength="9" lengthAdjust="spacingAndGlyphs">&gt;</text>
<rect x="36" y="442" width="9" height="17" fill="#001a00" />
<rect x="45" y="442" width="27" height="17" fill="#001a00" />
<text x="45" y="444" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">[ ]</text>
<rect x="72" y="442" width="171" height="17" fill="#001a00" />
<text x="72" y="444" fill="#00cd00" textLength="171" lengthAdjust="spacingAndGlyphs"> Show footer labels</text>
<rect x="243" y="442" width="630" height="17" fill="#001a00" />
<text x="891" y="444" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<rect x="27" y="459" width="846" height="17" fill="#001a00" />
<text x="891" y="461" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="478" fill="#ffffff" textLength="207" lengthAdjust="spacingAndGlyphs">Reset to default footer</text>
<text x="891" y="478" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="495" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="512" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="529" fill="#afafaf" textLength="585" lengthAdjust="spacingAndGlyphs">Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close</text>
<text x="891" y="529" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="546" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="563" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">┌────────────────────────────────────────────────────────────────────────────────────────────┐</text>
<text x="891" y="563" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="580" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold">Preview:</text>
<text x="864" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="580" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="45" y="597" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">~/project/path</text>
<text x="207" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="279" y="597" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs">main</text>
<text x="351" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="432" y="597" fill="#00cd00" textLength="54" lengthAdjust="spacingAndGlyphs">docker</text>
<text x="522" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="594" y="597" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs">gemini-2.5-pro</text>
<text x="756" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs"> · </text>
<text x="828" y="597" fill="#afafaf" textLength="27" lengthAdjust="spacingAndGlyphs">97%</text>
<text x="864" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="597" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="27" y="614" fill="#333333" textLength="846" lengthAdjust="spacingAndGlyphs">└────────────────────────────────────────────────────────────────────────────────────────────┘</text>
<text x="891" y="614" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="891" y="631" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="648" fill="#333333" textLength="900" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

@@ -129,3 +129,45 @@ exports[`<FooterConfigDialog /> > renders correctly with default settings 2`] =
│ │ │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`; `;
exports[`<FooterConfigDialog /> > updates the preview when Show footer labels is toggled off 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Configure Footer │
│ │
│ Select which items to display in the footer. │
│ │
│ [✓] workspace │
│ Current working directory │
│ [✓] git-branch │
│ Current git branch name (not shown when unavailable) │
│ [✓] sandbox │
│ Sandbox type and trust indicator │
│ [✓] model-name │
│ Current model identifier │
│ [✓] quota │
│ Remaining usage on daily limit (not shown when unavailable) │
│ [ ] context-used │
│ Percentage of context window used │
│ [ ] memory-usage │
│ Memory used by the application │
│ [ ] session-id │
│ Unique identifier for the current session │
│ [ ] code-changes │
│ Lines added/removed in the session (not shown when zero) │
│ [ ] token-count │
│ Total tokens used in the session (not shown when zero) │
│ > [ ] Show footer labels │
│ │
│ Reset to default footer │
│ │
│ │
│ Enter to select · ↑/↓ to navigate · ←/→ to reorder · Esc to close │
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Preview: │ │
│ │ ~/project/path · main · docker · gemini-2.5-pro · 97% │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -19,23 +19,24 @@ vi.mock('../scheduler/scheduler.js', () => ({
})); }));
describe('agent-scheduler', () => { describe('agent-scheduler', () => {
let mockConfig: Mocked<Config>;
let mockToolRegistry: Mocked<ToolRegistry>; let mockToolRegistry: Mocked<ToolRegistry>;
let mockMessageBus: Mocked<MessageBus>; let mockMessageBus: Mocked<MessageBus>;
beforeEach(() => { beforeEach(() => {
vi.mocked(Scheduler).mockClear();
mockMessageBus = {} as Mocked<MessageBus>; mockMessageBus = {} as Mocked<MessageBus>;
mockToolRegistry = { mockToolRegistry = {
getTool: vi.fn(), getTool: vi.fn(),
getMessageBus: vi.fn().mockReturnValue(mockMessageBus), getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
} as unknown as Mocked<ToolRegistry>; } as unknown as Mocked<ToolRegistry>;
mockConfig = {
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
toolRegistry: mockToolRegistry,
} as unknown as Mocked<Config>;
}); });
it('should create a scheduler with agent-specific config', async () => { it('should create a scheduler with agent-specific config', async () => {
const mockConfig = {
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
toolRegistry: mockToolRegistry,
} as unknown as Mocked<Config>;
const requests: ToolCallRequestInfo[] = [ const requests: ToolCallRequestInfo[] = [
{ {
callId: 'call-1', callId: 'call-1',
@@ -68,8 +69,46 @@ describe('agent-scheduler', () => {
}), }),
); );
// Verify that the scheduler's config has the overridden tool registry
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config; const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry); expect(schedulerConfig.toolRegistry).toBe(mockToolRegistry);
}); });
it('should override toolRegistry getter from prototype chain', async () => {
const mainRegistry = { _id: 'main' } as unknown as Mocked<ToolRegistry>;
const agentRegistry = {
_id: 'agent',
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
} as unknown as Mocked<ToolRegistry>;
const config = {
getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
} as unknown as Mocked<Config>;
Object.defineProperty(config, 'toolRegistry', {
get: () => mainRegistry,
configurable: true,
});
await scheduleAgentTools(
config as unknown as Config,
[
{
callId: 'c1',
name: 'new_page',
args: {},
isClientInitiated: false,
prompt_id: 'p1',
},
],
{
schedulerId: 'browser-1',
toolRegistry: agentRegistry as unknown as ToolRegistry,
signal: new AbortController().signal,
},
);
const schedulerConfig = vi.mocked(Scheduler).mock.calls[0][0].config;
expect(schedulerConfig.toolRegistry).toBe(agentRegistry);
expect(schedulerConfig.toolRegistry).not.toBe(mainRegistry);
expect(schedulerConfig.getToolRegistry()).toBe(agentRegistry);
});
}); });
@@ -58,6 +58,11 @@ export async function scheduleAgentTools(
const agentConfig: Config = Object.create(config); const agentConfig: Config = Object.create(config);
agentConfig.getToolRegistry = () => toolRegistry; agentConfig.getToolRegistry = () => toolRegistry;
agentConfig.getMessageBus = () => toolRegistry.getMessageBus(); agentConfig.getMessageBus = () => toolRegistry.getMessageBus();
// Override toolRegistry property so AgentLoopContext reads the agent-specific registry.
Object.defineProperty(agentConfig, 'toolRegistry', {
get: () => toolRegistry,
configurable: true,
});
const scheduler = new Scheduler({ const scheduler = new Scheduler({
config: agentConfig, config: agentConfig,
@@ -210,6 +210,7 @@ describe('browserAgentFactory', () => {
expect(toolNames).toContain('analyze_screenshot'); expect(toolNames).toContain('analyze_screenshot');
}); });
<<<<<<< feat/browser-allowed-domain
it('should include domain restrictions in system prompt when configured', async () => { it('should include domain restrictions in system prompt when configured', async () => {
const configWithDomains = makeFakeConfig({ const configWithDomains = makeFakeConfig({
agents: { agents: {
@@ -227,6 +228,45 @@ describe('browserAgentFactory', () => {
const systemPrompt = definition.promptConfig?.systemPrompt ?? ''; const systemPrompt = definition.promptConfig?.systemPrompt ?? '';
expect(systemPrompt).toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:'); expect(systemPrompt).toContain('SECURITY DOMAIN RESTRICTION - CRITICAL:');
expect(systemPrompt).toContain('- restricted.com'); expect(systemPrompt).toContain('- restricted.com');
=======
it('should include all MCP navigation tools (new_page, navigate_page) in definition', async () => {
mockBrowserManager.getDiscoveredTools.mockResolvedValue([
{ name: 'take_snapshot', description: 'Take snapshot' },
{ name: 'click', description: 'Click element' },
{ name: 'fill', description: 'Fill form field' },
{ name: 'navigate_page', description: 'Navigate to URL' },
{ name: 'new_page', description: 'Open a new page/tab' },
{ name: 'close_page', description: 'Close page' },
{ name: 'select_page', description: 'Select page' },
{ name: 'press_key', description: 'Press key' },
{ name: 'hover', description: 'Hover element' },
]);
const { definition } = await createBrowserAgentDefinition(
mockConfig,
mockMessageBus,
);
const toolNames =
definition.toolConfig?.tools
?.filter(
(t): t is { name: string } => typeof t === 'object' && 'name' in t,
)
.map((t) => t.name) ?? [];
// All MCP tools must be present
expect(toolNames).toContain('new_page');
expect(toolNames).toContain('navigate_page');
expect(toolNames).toContain('close_page');
expect(toolNames).toContain('select_page');
expect(toolNames).toContain('click');
expect(toolNames).toContain('take_snapshot');
expect(toolNames).toContain('press_key');
// Custom composite tool must also be present
expect(toolNames).toContain('type_text');
// Total: 9 MCP + 1 type_text (no analyze_screenshot without visualModel)
expect(definition.toolConfig?.tools).toHaveLength(10);
>>>>>>> main
}); });
}); });
@@ -20,14 +20,21 @@ import {
} from '../config/models.js'; } from '../config/models.js';
import { AuthType } from '../core/contentGenerator.js'; import { AuthType } from '../core/contentGenerator.js';
const createMockConfig = (overrides: Partial<Config> = {}): Config => const createMockConfig = (overrides: Partial<Config> = {}): Config => {
({ const config = {
getUserTier: () => undefined, getUserTier: () => undefined,
getModel: () => 'gemini-2.5-pro', getModel: () => 'gemini-2.5-pro',
getGemini31LaunchedSync: () => false, getGemini31LaunchedSync: () => false,
getUseCustomToolModelSync: () => {
const useGemini31 = config.getGemini31LaunchedSync();
const authType = config.getContentGeneratorConfig().authType;
return useGemini31 && authType === AuthType.USE_GEMINI;
},
getContentGeneratorConfig: () => ({ authType: undefined }), getContentGeneratorConfig: () => ({ authType: undefined }),
...overrides, ...overrides,
}) as unknown as Config; } as unknown as Config;
return config;
};
describe('policyHelpers', () => { describe('policyHelpers', () => {
describe('resolvePolicyChain', () => { describe('resolvePolicyChain', () => {
@@ -6,7 +6,6 @@
import type { GenerateContentConfig } from '@google/genai'; import type { GenerateContentConfig } from '@google/genai';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { AuthType } from '../core/contentGenerator.js';
import type { import type {
FailureKind, FailureKind,
FallbackAction, FallbackAction,
@@ -46,9 +45,7 @@ export function resolvePolicyChain(
let chain; let chain;
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false; const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useCustomToolModel = const useCustomToolModel = config.getUseCustomToolModelSync?.() ?? false;
useGemini31 &&
config.getContentGeneratorConfig?.()?.authType === AuthType.USE_GEMINI;
const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true; const hasAccessToPreview = config.getHasAccessToPreviewModel?.() ?? true;
const resolvedModel = resolveModel( const resolvedModel = resolveModel(
+20
View File
@@ -2531,6 +2531,26 @@ export class Config implements McpContext, AgentLoopContext {
return this.getGemini31LaunchedSync(); return this.getGemini31LaunchedSync();
} }
/**
* Returns whether the custom tool model should be used.
*/
async getUseCustomToolModel(): Promise<boolean> {
const useGemini3_1 = await this.getGemini31Launched();
const authType = this.contentGeneratorConfig?.authType;
return useGemini3_1 && authType === AuthType.USE_GEMINI;
}
/**
* Returns whether the custom tool model should be used.
*
* Note: This method should only be called after startup, once experiments have been loaded.
*/
getUseCustomToolModelSync(): boolean {
const useGemini3_1 = this.getGemini31LaunchedSync();
const authType = this.contentGeneratorConfig?.authType;
return useGemini3_1 && authType === AuthType.USE_GEMINI;
}
/** /**
* Returns whether Gemini 3.1 has been launched. * Returns whether Gemini 3.1 has been launched.
* *
+2 -1
View File
@@ -168,7 +168,8 @@ export function isPreviewModel(model: string): boolean {
model === PREVIEW_GEMINI_3_1_MODEL || model === PREVIEW_GEMINI_3_1_MODEL ||
model === PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL || model === PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL ||
model === PREVIEW_GEMINI_FLASH_MODEL || model === PREVIEW_GEMINI_FLASH_MODEL ||
model === PREVIEW_GEMINI_MODEL_AUTO model === PREVIEW_GEMINI_MODEL_AUTO ||
model === GEMINI_MODEL_ALIAS_AUTO
); );
} }
@@ -15,7 +15,9 @@ import {
PREVIEW_GEMINI_FLASH_MODEL, PREVIEW_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_MODEL_AUTO, DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO, PREVIEW_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
} from '../../config/models.js'; } from '../../config/models.js';
import { AuthType } from '../../core/contentGenerator.js';
import { ApprovalMode } from '../../policy/types.js'; import { ApprovalMode } from '../../policy/types.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { BaseLlmClient } from '../../core/baseLlmClient.js';
@@ -40,6 +42,15 @@ describe('ApprovalModeStrategy', () => {
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
getApprovedPlanPath: vi.fn().mockReturnValue(undefined), getApprovedPlanPath: vi.fn().mockReturnValue(undefined),
getPlanModeRoutingEnabled: vi.fn().mockResolvedValue(true), getPlanModeRoutingEnabled: vi.fn().mockResolvedValue(true),
getGemini31Launched: vi.fn().mockResolvedValue(false),
getUseCustomToolModel: vi.fn().mockImplementation(async () => {
const launched = await mockConfig.getGemini31Launched();
const authType = mockConfig.getContentGeneratorConfig?.()?.authType;
return launched && authType === AuthType.USE_GEMINI;
}),
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE,
}),
} as unknown as Config; } as unknown as Config;
mockBaseLlmClient = {} as BaseLlmClient; mockBaseLlmClient = {} as BaseLlmClient;
@@ -184,4 +195,50 @@ describe('ApprovalModeStrategy', () => {
expect(decision?.model).toBe(PREVIEW_GEMINI_MODEL); expect(decision?.model).toBe(PREVIEW_GEMINI_MODEL);
}); });
it('should route to Preview models when using "auto" alias', async () => {
vi.mocked(mockConfig.getModel).mockReturnValue(GEMINI_MODEL_ALIAS_AUTO);
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.PLAN);
const decision = await strategy.route(
mockContext,
mockConfig,
mockBaseLlmClient,
);
expect(decision?.model).toBe(PREVIEW_GEMINI_MODEL);
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.DEFAULT);
vi.mocked(mockConfig.getApprovedPlanPath).mockReturnValue(
'/path/to/plan.md',
);
const implementationDecision = await strategy.route(
mockContext,
mockConfig,
mockBaseLlmClient,
);
expect(implementationDecision?.model).toBe(PREVIEW_GEMINI_FLASH_MODEL);
});
it('should route to Preview Flash model when an approved plan exists and Gemini 3.1 is launched', async () => {
vi.mocked(mockConfig.getModel).mockReturnValue(GEMINI_MODEL_ALIAS_AUTO);
vi.mocked(mockConfig.getGemini31Launched).mockResolvedValue(true);
// Exit plan mode with approved plan
vi.mocked(mockConfig.getApprovalMode).mockReturnValue(ApprovalMode.DEFAULT);
vi.mocked(mockConfig.getApprovedPlanPath).mockReturnValue(
'/path/to/plan.md',
);
const decision = await strategy.route(
mockContext,
mockConfig,
mockBaseLlmClient,
);
// Should resolve to Preview Flash (3.0) because resolveClassifierModel uses preview variants for Gemini 3
expect(decision?.model).toBe(PREVIEW_GEMINI_FLASH_MODEL);
});
}); });
@@ -6,12 +6,10 @@
import type { Config } from '../../config/config.js'; import type { Config } from '../../config/config.js';
import { import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
isAutoModel, isAutoModel,
isPreviewModel, resolveClassifierModel,
GEMINI_MODEL_ALIAS_FLASH,
GEMINI_MODEL_ALIAS_PRO,
} from '../../config/models.js'; } from '../../config/models.js';
import type { BaseLlmClient } from '../../core/baseLlmClient.js'; import type { BaseLlmClient } from '../../core/baseLlmClient.js';
import { ApprovalMode } from '../../policy/types.js'; import { ApprovalMode } from '../../policy/types.js';
@@ -50,11 +48,19 @@ export class ApprovalModeStrategy implements RoutingStrategy {
const approvalMode = config.getApprovalMode(); const approvalMode = config.getApprovalMode();
const approvedPlanPath = config.getApprovedPlanPath(); const approvedPlanPath = config.getApprovedPlanPath();
const isPreview = isPreviewModel(model); const [useGemini3_1, useCustomToolModel] = await Promise.all([
config.getGemini31Launched(),
config.getUseCustomToolModel(),
]);
// 1. Planning Phase: If ApprovalMode === PLAN, explicitly route to the Pro model. // 1. Planning Phase: If ApprovalMode === PLAN, explicitly route to the Pro model.
if (approvalMode === ApprovalMode.PLAN) { if (approvalMode === ApprovalMode.PLAN) {
const proModel = isPreview ? PREVIEW_GEMINI_MODEL : DEFAULT_GEMINI_MODEL; const proModel = resolveClassifierModel(
model,
GEMINI_MODEL_ALIAS_PRO,
useGemini3_1,
useCustomToolModel,
);
return { return {
model: proModel, model: proModel,
metadata: { metadata: {
@@ -65,9 +71,12 @@ export class ApprovalModeStrategy implements RoutingStrategy {
}; };
} else if (approvedPlanPath) { } else if (approvedPlanPath) {
// 2. Implementation Phase: If ApprovalMode !== PLAN AND an approved plan path is set, prefer the Flash model. // 2. Implementation Phase: If ApprovalMode !== PLAN AND an approved plan path is set, prefer the Flash model.
const flashModel = isPreview const flashModel = resolveClassifierModel(
? PREVIEW_GEMINI_FLASH_MODEL model,
: DEFAULT_GEMINI_FLASH_MODEL; GEMINI_MODEL_ALIAS_FLASH,
useGemini3_1,
useCustomToolModel,
);
return { return {
model: flashModel, model: flashModel,
metadata: { metadata: {
@@ -59,6 +59,11 @@ describe('ClassifierStrategy', () => {
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO), getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false), getNumericalRoutingEnabled: vi.fn().mockResolvedValue(false),
getGemini31Launched: vi.fn().mockResolvedValue(false), getGemini31Launched: vi.fn().mockResolvedValue(false),
getUseCustomToolModel: vi.fn().mockImplementation(async () => {
const launched = await mockConfig.getGemini31Launched();
const authType = mockConfig.getContentGeneratorConfig().authType;
return launched && authType === AuthType.USE_GEMINI;
}),
getContentGeneratorConfig: vi.fn().mockReturnValue({ getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE, authType: AuthType.LOGIN_WITH_GOOGLE,
}), }),
@@ -22,7 +22,6 @@ import {
import { debugLogger } from '../../utils/debugLogger.js'; import { debugLogger } from '../../utils/debugLogger.js';
import type { LocalLiteRtLmClient } from '../../core/localLiteRtLmClient.js'; import type { LocalLiteRtLmClient } from '../../core/localLiteRtLmClient.js';
import { LlmRole } from '../../telemetry/types.js'; import { LlmRole } from '../../telemetry/types.js';
import { AuthType } from '../../core/contentGenerator.js';
// The number of recent history turns to provide to the router for context. // The number of recent history turns to provide to the router for context.
const HISTORY_TURNS_FOR_CONTEXT = 4; const HISTORY_TURNS_FOR_CONTEXT = 4;
@@ -172,10 +171,10 @@ export class ClassifierStrategy implements RoutingStrategy {
const reasoning = routerResponse.reasoning; const reasoning = routerResponse.reasoning;
const latencyMs = Date.now() - startTime; const latencyMs = Date.now() - startTime;
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false; const [useGemini3_1, useCustomToolModel] = await Promise.all([
const useCustomToolModel = config.getGemini31Launched(),
useGemini3_1 && config.getUseCustomToolModel(),
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI; ]);
const selectedModel = resolveClassifierModel( const selectedModel = resolveClassifierModel(
model, model,
routerResponse.model_choice, routerResponse.model_choice,
@@ -58,6 +58,11 @@ describe('NumericalClassifierStrategy', () => {
getNumericalRoutingEnabled: vi.fn().mockResolvedValue(true), getNumericalRoutingEnabled: vi.fn().mockResolvedValue(true),
getClassifierThreshold: vi.fn().mockResolvedValue(undefined), getClassifierThreshold: vi.fn().mockResolvedValue(undefined),
getGemini31Launched: vi.fn().mockResolvedValue(false), getGemini31Launched: vi.fn().mockResolvedValue(false),
getUseCustomToolModel: vi.fn().mockImplementation(async () => {
const launched = await mockConfig.getGemini31Launched();
const authType = mockConfig.getContentGeneratorConfig().authType;
return launched && authType === AuthType.USE_GEMINI;
}),
getContentGeneratorConfig: vi.fn().mockReturnValue({ getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: AuthType.LOGIN_WITH_GOOGLE, authType: AuthType.LOGIN_WITH_GOOGLE,
}), }),
@@ -18,7 +18,6 @@ import type { Config } from '../../config/config.js';
import { debugLogger } from '../../utils/debugLogger.js'; import { debugLogger } from '../../utils/debugLogger.js';
import type { LocalLiteRtLmClient } from '../../core/localLiteRtLmClient.js'; import type { LocalLiteRtLmClient } from '../../core/localLiteRtLmClient.js';
import { LlmRole } from '../../telemetry/types.js'; import { LlmRole } from '../../telemetry/types.js';
import { AuthType } from '../../core/contentGenerator.js';
// The number of recent history turns to provide to the router for context. // The number of recent history turns to provide to the router for context.
const HISTORY_TURNS_FOR_CONTEXT = 8; const HISTORY_TURNS_FOR_CONTEXT = 8;
@@ -185,10 +184,10 @@ export class NumericalClassifierStrategy implements RoutingStrategy {
config, config,
config.getSessionId() || 'unknown-session', config.getSessionId() || 'unknown-session',
); );
const useGemini3_1 = (await config.getGemini31Launched?.()) ?? false; const [useGemini3_1, useCustomToolModel] = await Promise.all([
const useCustomToolModel = config.getGemini31Launched(),
useGemini3_1 && config.getUseCustomToolModel(),
config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI; ]);
const selectedModel = resolveClassifierModel( const selectedModel = resolveClassifierModel(
model, model,
modelAlias, modelAlias,