diff --git a/.github/actions/publish-release/action.yml b/.github/actions/publish-release/action.yml index 70a413f13a..54c404c7c1 100644 --- a/.github/actions/publish-release/action.yml +++ b/.github/actions/publish-release/action.yml @@ -193,7 +193,7 @@ runs: INPUTS_A2A_PACKAGE_NAME: '${{ inputs.a2a-package-name }}' - 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 }}' shell: 'bash' run: | diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 617f8492fb..c7a2f4bd4e 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -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 `~/.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 By default, read-only MCP tools require user confirmation in Plan Mode. You can diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index e5a1f9e8b6..c6816339f5 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -106,6 +106,7 @@ export interface FooterRowItem { flexGrow?: number; flexShrink?: number; isFocused?: boolean; + alignItems?: 'flex-start' | 'center' | 'flex-end'; } const COLUMN_GAP = 3; @@ -117,10 +118,17 @@ export const FooterRow: React.FC<{ const elements: React.ReactNode[] = []; items.forEach((item, idx) => { - if (idx > 0 && !showLabels) { + if (idx > 0) { elements.push( - - ยท + + {!showLabels && ยท } , ); } @@ -131,6 +139,7 @@ export const FooterRow: React.FC<{ flexDirection="column" flexGrow={item.flexGrow ?? 0} flexShrink={item.flexShrink ?? 1} + alignItems={item.alignItems} backgroundColor={item.isFocused ? theme.background.focus : undefined} > {showLabels && ( @@ -148,12 +157,7 @@ export const FooterRow: React.FC<{ }); return ( - + {elements} ); @@ -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 isLast = index === columnsToRender.length - 1; // Calculate exact space available for growth to prevent over-estimation truncation const otherItemsWidth = columnsToRender @@ -464,8 +469,10 @@ export const Footer: React.FC = () => { key: col.id, header: col.header, element: col.element(estimatedWidth), - flexGrow: isWorkspace ? 1 : 0, + flexGrow: 0, flexShrink: isWorkspace ? 1 : 0, + alignItems: + isLast && !droppedAny && index > 0 ? 'flex-end' : 'flex-start', }; }); @@ -476,6 +483,7 @@ export const Footer: React.FC = () => { element: โ€ฆ, flexGrow: 0, flexShrink: 0, + alignItems: 'flex-end', }); } diff --git a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx index 3141c3a1d7..9d3688e17a 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx @@ -9,6 +9,7 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { FooterConfigDialog } from './FooterConfigDialog.js'; import { createMockSettings } from '../../test-utils/settings.js'; +import { ALL_ITEMS } from '../../config/footerItems.js'; import { act } from 'react'; describe('', () => { @@ -213,4 +214,60 @@ describe('', () => { expect(bIdxAfter).toBeLessThan(wIdxAfter); }); }); + + it('updates the preview when Show footer labels is toggled off', async () => { + const settings = createMockSettings(); + const renderResult = renderWithProviders( + , + { 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(); + }); }); diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx index cda58574a3..562bbabd81 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx @@ -266,7 +266,7 @@ export const FooterConfigDialog: React.FC = ({ key: id, header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id, element: mockData[id], - flexGrow: 1, + flexGrow: 0, isFocused: id === focusKey, })); diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap index 2d98d66f03..3980ddbd0a 100644 --- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap @@ -1,26 +1,26 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`