diff --git a/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx b/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx index f89467336..640807acc 100644 --- a/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx +++ b/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx @@ -487,40 +487,50 @@ describe("FollowUpPromptBox", () => { expect(mocks.scrollToBottom).toHaveBeenCalledOnce(); }); - it("preserves scroll position after queueing a follow-up", () => { - const props = createFollowUpPromptBoxProps({ - kind: "queue", - onStop: vi.fn(), - }); - render(); - - fireEvent.click(screen.getByText("Submit")); - - expect(props.composer?.onSubmit).toHaveBeenCalledOnce(); - expect(mocks.scrollToBottom).not.toHaveBeenCalled(); - }); - - it("swaps Enter and modifier submit while steer-on-Enter is enabled", () => { - const props = createFollowUpPromptBoxProps({ - kind: "queue", - onStop: vi.fn(), - }); - if (!props.composer) { - throw new Error("Expected follow-up composer props"); - } - props.composer.steerActiveThreadOnEnter = true; - render(); - - fireEvent.click(screen.getByText("Submit")); - expect(props.composer.onModifierSubmit).toHaveBeenCalledOnce(); - expect(props.composer.onSubmit).not.toHaveBeenCalled(); - expect(mocks.scrollToBottom).toHaveBeenCalledOnce(); + it.each([ + { + setting: false, + primaryAction: "queue", + modifierAction: "steer", + }, + { + setting: true, + primaryAction: "steer", + modifierAction: "queue", + }, + ] as const)( + "routes Enter/click to $primaryAction and Command+Enter to $modifierAction when steer-on-Enter is $setting", + ({ setting, primaryAction, modifierAction }) => { + const props = createFollowUpPromptBoxProps({ + kind: "queue", + onStop: vi.fn(), + }); + if (!props.composer) { + throw new Error("Expected follow-up composer props"); + } + props.composer.steerActiveThreadOnEnter = setting; + render(); + + fireEvent.click(screen.getByText("Submit")); + const expectedPrimary = + primaryAction === "queue" + ? props.composer.onSubmit + : props.composer.onModifierSubmit; + const expectedModifier = + modifierAction === "queue" + ? props.composer.onSubmit + : props.composer.onModifierSubmit; + expect(expectedPrimary).toHaveBeenCalledOnce(); + expect(expectedModifier).not.toHaveBeenCalled(); + expect(mocks.scrollToBottom).toHaveBeenCalledTimes( + primaryAction === "steer" ? 1 : 0, + ); - mocks.scrollToBottom.mockClear(); - fireEvent.click(screen.getByText("Modifier submit")); - expect(props.composer.onSubmit).toHaveBeenCalledOnce(); - expect(mocks.scrollToBottom).not.toHaveBeenCalled(); - }); + fireEvent.click(screen.getByText("Modifier submit")); + expect(expectedModifier).toHaveBeenCalledOnce(); + expect(mocks.scrollToBottom).toHaveBeenCalledOnce(); + }, + ); it("disables the permission picker while plan mode is active", () => { const props = createFollowUpPromptBoxProps({ diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx index d86f37b81..ffe96deb1 100644 --- a/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx +++ b/apps/app/src/components/promptbox/PromptBoxInternal.test.tsx @@ -267,6 +267,7 @@ function renderPromptBox( const changes: PromptChange[] = []; const onMentionQueryChange = vi.fn(); const onCommandQueryChange = vi.fn(); + const onSubmit = vi.fn(); const promptBoxRef = createRef(); function PromptBoxHarness() { @@ -283,7 +284,7 @@ function renderPromptBox( setValue(nextValue); setMentionRanges(nextMentions); }} - onSubmit={() => {}} + onSubmit={onSubmit} typeahead={buildTypeaheadConfig({ mentionTriggers: options.mentionTriggers, mentionSuggestions: options.mentionSuggestions, @@ -304,6 +305,7 @@ function renderPromptBox( changes, onMentionQueryChange, onCommandQueryChange, + onSubmit, promptBoxRef, }; } @@ -439,6 +441,59 @@ function mockPointerCoarse(matches: boolean): () => void { }; } +function mockNavigatorIdentity({ + userAgent, + vendor, + platform, + maxTouchPoints, +}: Pick< + Navigator, + "userAgent" | "vendor" | "platform" | "maxTouchPoints" +>): () => void { + const userAgentMock = vi + .spyOn(navigator, "userAgent", "get") + .mockReturnValue(userAgent); + const vendorMock = vi + .spyOn(navigator, "vendor", "get") + .mockReturnValue(vendor); + const platformMock = vi + .spyOn(navigator, "platform", "get") + .mockReturnValue(platform); + const maxTouchPointsDescriptor = Object.getOwnPropertyDescriptor( + navigator, + "maxTouchPoints", + ); + Object.defineProperty(navigator, "maxTouchPoints", { + configurable: true, + value: maxTouchPoints, + }); + return () => { + if (maxTouchPointsDescriptor) { + Object.defineProperty( + navigator, + "maxTouchPoints", + maxTouchPointsDescriptor, + ); + } else { + Reflect.deleteProperty(navigator, "maxTouchPoints"); + } + platformMock.mockRestore(); + vendorMock.mockRestore(); + userAgentMock.mockRestore(); + }; +} + +function mockIPadOSWebKit(): () => void { + return mockNavigatorIdentity({ + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) " + + "AppleWebKit/605.1.15 Mobile/15E148 Safari/604.1", + vendor: "Apple Computer, Inc.", + platform: "MacIntel", + maxTouchPoints: 5, + }); +} + afterEach(() => { cleanup(); resetPluginLogoStoreForTest(); @@ -1076,6 +1131,327 @@ describe("PromptBoxInternal controlled value sync", () => { }); }); +describe("PromptBoxInternal submit shortcuts", () => { + it("continues to submit unmodified Enter on a fine-pointer device", () => { + const restoreMatchMedia = mockPointerCoarse(false); + try { + const onSubmit = vi.fn(); + render( + , + ); + + const wasNotCanceled = fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + }); + + expect(wasNotCanceled).toBe(false); + expect(onSubmit).toHaveBeenCalledOnce(); + } finally { + restoreMatchMedia(); + } + }); + + it("submits a Magic Keyboard Enter on coarse-pointer iPadOS WebKit", () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + const editor = getPromptEditorElement(); + const wasNotCanceled = fireEvent.keyDown(editor, { + key: "Enter", + code: "Enter", + }); + + expect(wasNotCanceled).toBe(false); + expect(editor.getAttribute("enterkeyhint")).toBe("enter"); + expect(onSubmit).toHaveBeenCalledOnce(); + expect(onChange).not.toHaveBeenCalled(); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("keeps software-keyboard Enter as a newline on coarse-pointer iPadOS WebKit", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + const editor = getPromptEditorElement(); + fireEvent.keyDown(editor, { key: "Enter", code: "" }); + + expect(editor.getAttribute("enterkeyhint")).toBe("enter"); + expect(onSubmit).not.toHaveBeenCalled(); + await waitFor(() => + expect(onChange).toHaveBeenLastCalledWith("First line\n", []), + ); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("keeps software code=Enter as a newline on an Android coarse pointer", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockNavigatorIdentity({ + userAgent: + "Mozilla/5.0 (Linux; Android 15; Pixel Tablet) " + + "AppleWebKit/537.36 Chrome/140.0.0.0 Safari/537.36", + vendor: "Google Inc.", + platform: "Linux armv8l", + maxTouchPoints: 5, + }); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + }); + + expect(onSubmit).not.toHaveBeenCalled(); + await waitFor(() => + expect(onChange).toHaveBeenLastCalledWith("First line\n", []), + ); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("does not intercept code=Enter on a non-iPad coarse-pointer hybrid", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockNavigatorIdentity({ + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 Chrome/140.0.0.0 Safari/537.36", + vendor: "Google Inc.", + platform: "Win32", + maxTouchPoints: 10, + }); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + }); + + expect(onSubmit).not.toHaveBeenCalled(); + await waitFor(() => + expect(onChange).toHaveBeenLastCalledWith("First line\n", []), + ); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("keeps Magic Keyboard Shift+Enter as a newline on coarse-pointer iPadOS WebKit", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + shiftKey: true, + }); + + expect(onSubmit).not.toHaveBeenCalled(); + await waitFor(() => + expect(onChange).toHaveBeenLastCalledWith("First line\n", []), + ); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("routes Magic Keyboard Command+Enter to modifier submit on coarse-pointer iPadOS WebKit", () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onModifierSubmit = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + metaKey: true, + }); + + expect(onModifierSubmit).toHaveBeenCalledOnce(); + expect(onSubmit).not.toHaveBeenCalled(); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("does not submit a hardware Enter that is committing IME composition", () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onSubmit = vi.fn(); + render( + , + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + isComposing: true, + }); + + expect(onSubmit).not.toHaveBeenCalled(); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("does not submit the Enter keydown immediately following compositionend", () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const onSubmit = vi.fn(); + render( + , + ); + + const editor = getPromptEditorElement(); + fireEvent.compositionStart(editor, { data: "候補" }); + fireEvent.compositionEnd(editor, { data: "候補" }); + fireEvent.keyDown(editor, { + key: "Enter", + code: "Enter", + keyCode: 13, + }); + + expect(onSubmit).not.toHaveBeenCalled(); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + + it("keeps hardware Enter as a newline in zen mode", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + const storageKey = "bb.test.promptbox.zen-submit-shortcut"; + window.localStorage.removeItem(storageKey); + try { + const onChange = vi.fn(); + const onSubmit = vi.fn(); + render( + , + ); + fireEvent.click( + screen.getByRole("button", { name: "Make prompt box larger" }), + ); + + fireEvent.keyDown(getPromptEditorElement(), { + key: "Enter", + code: "Enter", + }); + + expect(onSubmit).not.toHaveBeenCalled(); + await waitFor(() => + expect(onChange).toHaveBeenLastCalledWith("First line\n", []), + ); + } finally { + window.localStorage.removeItem(storageKey); + restoreNavigator(); + restoreMatchMedia(); + } + }); +}); + describe("PromptBoxInternal zen mode layout", () => { it("animates the prompt box height when toggling zen mode", async () => { const storageKey = "bb.test.promptbox.zen-height-animation"; @@ -2576,6 +2952,41 @@ describe("PromptBoxInternal command typeahead submit", () => { }); describe("PromptBoxInternal command typeahead navigation", () => { + it("applies typeahead before submit for Magic Keyboard Enter on coarse-pointer iPadOS WebKit", async () => { + const restoreMatchMedia = mockPointerCoarse(true); + const restoreNavigator = mockIPadOSWebKit(); + try { + const { changes, onSubmit } = renderPromptBox("/", { + commandSuggestions: [ + { + kind: "command", + name: "review", + source: "skill", + origin: "user", + description: null, + argumentHint: null, + }, + ], + }); + const editor = getPromptEditorElement(); + editor.focus(); + await screen.findByRole("button", { name: "review" }); + expect(onSubmit).not.toHaveBeenCalled(); + + fireEvent.keyDown(editor, { key: "Enter", code: "Enter" }); + + await waitFor(() => expect(latestValue(changes)).toBe("/review ")); + expect(onSubmit).not.toHaveBeenCalled(); + expect(latestChange(changes)?.mentions[0]?.resource).toMatchObject({ + kind: "command", + name: "review", + }); + } finally { + restoreNavigator(); + restoreMatchMedia(); + } + }); + it("uses the rendered section order for Arrow keys and Enter", async () => { const { changes, promptBoxRef } = renderPromptBox("/", { commandSuggestions: [ diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.tsx index b7ac8b6bf..ea01504fd 100644 --- a/apps/app/src/components/promptbox/PromptBoxInternal.tsx +++ b/apps/app/src/components/promptbox/PromptBoxInternal.tsx @@ -1041,6 +1041,27 @@ function focusEditorAtEnd(editor: Editor): void { editor.view.focus(); } +const SAFARI_POST_COMPOSITION_KEYDOWN_WINDOW_MS = 500; + +function isIPadOSWebKit(): boolean { + if (typeof navigator === "undefined") return false; + + const isAppleWebKit = + /Apple Computer/u.test(navigator.vendor) && + /\bAppleWebKit\//u.test(navigator.userAgent); + const isIPad = + navigator.platform === "iPad" || + (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 2); + return isAppleWebKit && isIPad; +} + +function isIPadHardwareEnterCandidate(event: KeyboardEvent): boolean { + return ( + event.key === "Enter" && + (event.code === "Enter" || event.code === "NumpadEnter") + ); +} + export function PromptBoxInternal({ id, value, @@ -1108,7 +1129,11 @@ export function PromptBoxInternal({ resetOnSubmit: resetZenModeOnSubmit = false, } = zenMode; const isPointerCoarse = usePointerCoarse(); - const canSubmitWithEnterKey = !isPointerCoarse; + // Legacy iPads report an iPad platform; current iPadOS WebKit uses a + // desktop-like MacIntel platform with touch points distinguishing it from + // macOS. The value is stable for the lifetime of the page, so it does not + // need another media-query listener. + const isIPadOSWebKitDevice = useMemo(isIPadOSWebKit, []); const editorEnterKeyHint = isPointerCoarse ? "enter" : "send"; // Passive text autofocus opens the soft keyboard on coarse-pointer devices. const shouldAvoidSoftKeyboardAutofocus = isPointerCoarse; @@ -1145,8 +1170,12 @@ export function PromptBoxInternal({ const skipEditorChangeRef = useRef(false); const editorValueKeyRef = useRef(""); const triggerKeyRef = useRef(""); - const handleEditorKeyDownRef = useRef<(event: KeyboardEvent) => boolean>( - () => false, + const handleEditorKeyDownRef = useRef< + (event: KeyboardEvent, isOriginalIPadHardwareEnter?: boolean) => boolean + >(() => false); + const compositionEndedAtRef = useRef(Number.NEGATIVE_INFINITY); + const postCompositionKeyDownEventsRef = useRef( + new WeakSet(), ); // The TipTap editor is created once; its `onUpdate`/`onSelectionUpdate`/click // handlers close over the first `syncTriggerState`. `syncTriggerState` @@ -1496,6 +1525,44 @@ export function PromptBoxInternal({ }); return false; }, + compositionend: (_view, event) => { + compositionEndedAtRef.current = event.timeStamp; + return false; + }, + keydown: (_view, event) => { + if ( + !_view.editable || + !isIPadOSWebKitDevice || + !isIPadHardwareEnterCandidate(event) || + _view.composing || + event.isComposing || + event.keyCode === 229 + ) { + return false; + } + + // Match ProseMirror's Safari compositionend -> keydown safeguard. + // This custom DOM hook runs before ProseMirror's own keydown + // handler, so bypassing it here would otherwise submit an IME + // candidate confirmation. + if ( + Math.abs(event.timeStamp - compositionEndedAtRef.current) < + SAFARI_POST_COMPOSITION_KEYDOWN_WINDOW_MS + ) { + compositionEndedAtRef.current = Number.NEGATIVE_INFINITY; + postCompositionKeyDownEventsRef.current.add(event); + return false; + } + + // ProseMirror delays iOS Enter handling and later passes a + // synthetic Enter to handleKeyDown so the software keyboard can + // finish its DOM mutation. Only on the affected iPadOS WebKit path + // do we use the original event's physical code to handle a Magic + // Keyboard Enter before that fallback. Other platforms, including + // Android and coarse-pointer hybrids, stay entirely on + // ProseMirror's normal path. + return handleEditorKeyDownRef.current(event, true); + }, click: (_view, event) => { return suppressPromptEditorAnchorActivation(event); }, @@ -2475,7 +2542,16 @@ export function PromptBoxInternal({ ); const handleEditorKeyDown = useCallback( - (event: KeyboardEvent): boolean => { + (event: KeyboardEvent, isOriginalIPadHardwareEnter = false): boolean => { + if ( + event.isComposing || + event.keyCode === 229 || + postCompositionKeyDownEventsRef.current.has(event) + ) { + return false; + } + const canSubmitWithEnterKey = + !isPointerCoarse || isOriginalIPadHardwareEnter; const currentEditor = editorRef.current; const selection = currentEditor?.state.selection; const hasCollapsedSelection = Boolean(selection?.empty); @@ -2696,11 +2772,11 @@ export function PromptBoxInternal({ applyHistoryDraft, applyTrigger, canLoadMoreCommands, - canSubmitWithEnterKey, commandError, commandHasMore, commandIsLoadingMore, history, + isPointerCoarse, isZenMode, loadMoreCommands, onCommandQueryChange, @@ -2716,7 +2792,7 @@ export function PromptBoxInternal({ ], ); - useEffect(() => { + useLayoutEffect(() => { handleEditorKeyDownRef.current = handleEditorKeyDown; }, [handleEditorKeyDown]); diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md index 332b30018..324a0c915 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md @@ -56,9 +56,13 @@ message agents, or inspect projects, providers, and environments. exposes raw provider events that bb does not yet understand in packaged builds. Development builds always show those diagnostic rows. Update it with `bb settings general showUnhandledProviderEvents `. -- The `steerActiveThreadOnEnter` General preference defaults to false. Enable - it to make Enter steer a running thread and Command+Enter queue a - follow-up; when disabled, those actions are reversed. Update it with +- The `steerActiveThreadOnEnter` General preference defaults to false. Outside + an open composer typeahead menu, enable it to make Enter steer a running + thread and Command+Enter queue a follow-up; when disabled, those actions are + reversed. Shift+Enter inserts a newline, while zen mode also makes + unmodified Enter insert one. On coarse-pointer touch devices, the software + keyboard keeps Return as a newline; iPadOS WebKit preserves the Enter + shortcuts for a connected Magic Keyboard. Update the preference with `bb settings general steerActiveThreadOnEnter `. - Settings → Keyboard records server-backed per-command shortcut overrides. The `showKeyboardHints` preference controls the delayed badges shown while diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md b/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md index bab61c714..36d3c474a 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/references/app-settings.md @@ -46,8 +46,13 @@ every window and client sees the same value. - `steerActiveThreadOnEnter` defaults to false. Set it with `bb settings general steerActiveThreadOnEnter `. -- When disabled, Enter queues a follow-up and Command+Enter steers the - active turn. When enabled, those actions are reversed. +- Outside an open composer typeahead menu, disabling it makes Enter queue a + follow-up and Command+Enter steer the active turn. When enabled, those + actions are reversed. +- Shift+Enter inserts a newline. Zen mode also makes unmodified Enter insert a + newline. On coarse-pointer touch devices, the software-keyboard Return path + stays a newline; iPadOS WebKit preserves the Enter shortcuts for a connected + Magic Keyboard. ## New onboarding diff --git a/docs/configuration.md b/docs/configuration.md index 58a1735a6..bd6ba1bf0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -127,11 +127,17 @@ agent or terminal with `bb settings general showUnhandledProviderEvents `. The "Steer running threads on Enter" toggle in Settings → General changes the -active-thread composer shortcuts. It defaults to off: Enter queues and -Command+Enter steers. When enabled, Enter steers and Command+Enter queues. Set -it with +active-thread composer shortcuts when no typeahead suggestion is active. It +defaults to off: Enter queues and Command+Enter steers. When enabled, Enter +steers and Command+Enter queues. Set it with `bb settings general steerActiveThreadOnEnter `. +Outside an open typeahead menu, Shift+Enter inserts a newline. In zen mode, +unmodified Enter also inserts a newline. On coarse-pointer touch devices, the +software-keyboard Return path inserts a newline and the submit button sends. +iPadOS WebKit additionally preserves the Enter and Command+Enter shortcuts +above for a connected Magic Keyboard. + ## Keyboard Shortcuts Settings → Keyboard edits app command shortcuts. Overrides are stored in the diff --git a/packages/templates/src/generated/templates.generated.ts b/packages/templates/src/generated/templates.generated.ts index ef86736b4..2b3b6c96e 100644 --- a/packages/templates/src/generated/templates.generated.ts +++ b/packages/templates/src/generated/templates.generated.ts @@ -39,7 +39,7 @@ export const templateDefinitions = [ }, { "id": "bbGuideCustomization", - "body": "Customization commands\n\nTheming — the app-wide color palette\n\n`bb theme` controls a set of CSS-variable overrides, persisted server-side and\napplied live to every open window. This is the palette only; light/dark mode is a\nseparate per-client setting the palette layers on top of. Custom themes live on\ndisk, one folder per theme, at /theme//theme.css (the packaged\napp uses ~/.bb/theme/…). The folder name is the theme id.\n\n bb theme list Built-in and custom themes; shows the active one\n bb theme dir Print the custom-theme directory (where to author)\n bb theme set [--favicon-color ]\n Activate a theme, preserving the favicon color\n unless the flag supplies the complete selection\n bb theme show [--css] Print the active palette; --css dumps the CSS\n bb theme reset Back to the default theme; preserve favicon color\n bb theme favicon set Set favicon color; preserve the active theme\n bb theme favicon reset Reset favicon color; preserve the active theme\n\nTo author a custom theme, run `bb theme dir`, write //theme.css,\nthen `bb theme set `. The full design-token reference is in the bb-cli\nskill (references/theming.md).\n\nFavicon colors are `default`, `red`, `orange`, `yellow`, `green`, `teal`,\n`blue`, `purple`, and `pink`. Theme and favicon-only commands carry the other\nappearance value forward explicitly.\n\nAdd --json to any theme command for machine-readable output.\n\nServer-backed General settings\n\nSettings → General includes app-wide preferences stored server-side so every\nwindow and restart sees the same value. On macOS, the Caffeinate toggle asks the\nprimary host daemon to run `/usr/bin/caffeinate -i -w `, preventing\nsystem idle sleep while bb is running; turning it off stops that process. It\nonly blocks idle sleep: closing a laptop lid or choosing Sleep manually still\nsleeps the Mac. This setting is only shown when the connected primary host\ndaemon reports macOS.\n\nSettings → Keyboard also includes `showKeyboardHints`, which defaults to true.\nTurn it off to hide the delayed shortcut badges shown while holding Command or\nControl on macOS, or Control on Windows/Linux. Shortcut commands continue to\nwork.\n\nSettings → General includes `showUnhandledProviderEvents`, which defaults to\nfalse in packaged builds. Turn it on to show raw provider events bb does not yet\nunderstand; development builds always show these diagnostic rows.\n\nSettings → General also includes `steerActiveThreadOnEnter`, which defaults to\nfalse. When enabled, Enter steers a running thread and Command+Enter queues a\nfollow-up; when disabled, those actions are reversed.\n\n bb settings show\n bb settings general \n bb settings replay-onboarding\n bb settings experiment \n bb settings usage [--machine ]\n bb settings version [--force]\n bb settings reload\n\n`bb settings replay-onboarding` enables the `newOnboarding` experiment and\nclears `onboardingCompletedAt`. The first-run setup guide then shows again on\nthe next app load. The same button lives in Settings → General → Setup guide\nwhile the experiment is on.\n\nThe `newOnboarding` experiment exposes the first-run agent and project setup\nguide.\nThe `toolsHub` experiment exposes Extensions for managing skills and plugins.\nAutomations stays in the Plugins section beside threads. It does not enable or\ndisable installed skills, automation execution, plugin runtimes, CLI commands,\nor backend APIs.\n\nThread timeline windows are bounded by event count as well as user-message\ncount (`BB_FF_TIMELINE_WINDOW_EVENT_BUDGET`, default 1500), so a long thread\nstops reprojecting its whole history — and blocking the server event loop — on\nevery update. A turn still running is cut at the budget too, so a very long\nturn costs the budget per update instead of growing without limit. Older\nactivity loads automatically as you scroll toward the top.\n\nServer-backed keyboard shortcuts\n\nSettings → Keyboard records per-command shortcut overrides. They are persisted\nserver-side, applied live to every connected window, and survive restarts.\nReset removes an override and returns to bb's current default; Clear explicitly\ndisables a command. `Mod` means Command on macOS and Control on Windows/Linux.\nBindings for non-native actions apply in browser and desktop clients. Command\ncontexts and native-only availability remain server-owned, and desktop menu\naccelerators for New Thread, New Window, New Tab, Close, and Settings use the\nsame resolved bindings. The complete default table is in docs/configuration.md.\n\n bb settings keyboard list\n bb settings keyboard hints \n bb settings keyboard set \n bb settings keyboard reset [command]\n\nHost files and voice transcription\n\n bb file read|write|list|paths|mkdir|move|remove ...\n bb voice transcribe [--prompt ]\n\nVoice transcription uses the `BB_TRANSCRIPTION` model, which defaults to\n`codex/gpt-transcribe`. Override it with\n`bb-app config set BB_TRANSCRIPTION `.\n\n`bb file` supports `--host` for remote machines and `--root` on mutating\ncommands to confine access beneath an absolute directory. Use `--json` for\nmetadata and machine-readable results.\n\nClient-local UI preferences\n\nSome Settings values live only in the current browser/client. The Voice Input\nmicrophone picker stores the selected browser MediaDevices device id in\nlocalStorage as `bb.voiceInput.audioInputDeviceId`; it does not have a `bb`\ncommand and does not change the server-side transcription model.", + "body": "Customization commands\n\nTheming — the app-wide color palette\n\n`bb theme` controls a set of CSS-variable overrides, persisted server-side and\napplied live to every open window. This is the palette only; light/dark mode is a\nseparate per-client setting the palette layers on top of. Custom themes live on\ndisk, one folder per theme, at /theme//theme.css (the packaged\napp uses ~/.bb/theme/…). The folder name is the theme id.\n\n bb theme list Built-in and custom themes; shows the active one\n bb theme dir Print the custom-theme directory (where to author)\n bb theme set [--favicon-color ]\n Activate a theme, preserving the favicon color\n unless the flag supplies the complete selection\n bb theme show [--css] Print the active palette; --css dumps the CSS\n bb theme reset Back to the default theme; preserve favicon color\n bb theme favicon set Set favicon color; preserve the active theme\n bb theme favicon reset Reset favicon color; preserve the active theme\n\nTo author a custom theme, run `bb theme dir`, write //theme.css,\nthen `bb theme set `. The full design-token reference is in the bb-cli\nskill (references/theming.md).\n\nFavicon colors are `default`, `red`, `orange`, `yellow`, `green`, `teal`,\n`blue`, `purple`, and `pink`. Theme and favicon-only commands carry the other\nappearance value forward explicitly.\n\nAdd --json to any theme command for machine-readable output.\n\nServer-backed General settings\n\nSettings → General includes app-wide preferences stored server-side so every\nwindow and restart sees the same value. On macOS, the Caffeinate toggle asks the\nprimary host daemon to run `/usr/bin/caffeinate -i -w `, preventing\nsystem idle sleep while bb is running; turning it off stops that process. It\nonly blocks idle sleep: closing a laptop lid or choosing Sleep manually still\nsleeps the Mac. This setting is only shown when the connected primary host\ndaemon reports macOS.\n\nSettings → Keyboard also includes `showKeyboardHints`, which defaults to true.\nTurn it off to hide the delayed shortcut badges shown while holding Command or\nControl on macOS, or Control on Windows/Linux. Shortcut commands continue to\nwork.\n\nSettings → General includes `showUnhandledProviderEvents`, which defaults to\nfalse in packaged builds. Turn it on to show raw provider events bb does not yet\nunderstand; development builds always show these diagnostic rows.\n\nSettings → General also includes `steerActiveThreadOnEnter`, which defaults to\nfalse. Outside an open typeahead menu, enabling it makes Enter steer a running\nthread and Command+Enter queue a follow-up; when disabled, those actions are\nreversed. Shift+Enter inserts a newline, and unmodified Enter inserts a newline\nin zen mode. On coarse-pointer touch devices, the software-keyboard Return path\ninserts a newline. iPadOS WebKit preserves these Enter shortcuts for a connected\nMagic Keyboard.\n\n bb settings show\n bb settings general \n bb settings replay-onboarding\n bb settings experiment \n bb settings usage [--machine ]\n bb settings version [--force]\n bb settings reload\n\n`bb settings replay-onboarding` enables the `newOnboarding` experiment and\nclears `onboardingCompletedAt`. The first-run setup guide then shows again on\nthe next app load. The same button lives in Settings → General → Setup guide\nwhile the experiment is on.\n\nThe `newOnboarding` experiment exposes the first-run agent and project setup\nguide.\nThe `toolsHub` experiment exposes Extensions for managing skills and plugins.\nAutomations stays in the Plugins section beside threads. It does not enable or\ndisable installed skills, automation execution, plugin runtimes, CLI commands,\nor backend APIs.\n\nThread timeline windows are bounded by event count as well as user-message\ncount (`BB_FF_TIMELINE_WINDOW_EVENT_BUDGET`, default 1500), so a long thread\nstops reprojecting its whole history — and blocking the server event loop — on\nevery update. A turn still running is cut at the budget too, so a very long\nturn costs the budget per update instead of growing without limit. Older\nactivity loads automatically as you scroll toward the top.\n\nServer-backed keyboard shortcuts\n\nSettings → Keyboard records per-command shortcut overrides. They are persisted\nserver-side, applied live to every connected window, and survive restarts.\nReset removes an override and returns to bb's current default; Clear explicitly\ndisables a command. `Mod` means Command on macOS and Control on Windows/Linux.\nBindings for non-native actions apply in browser and desktop clients. Command\ncontexts and native-only availability remain server-owned, and desktop menu\naccelerators for New Thread, New Window, New Tab, Close, and Settings use the\nsame resolved bindings. The complete default table is in docs/configuration.md.\n\n bb settings keyboard list\n bb settings keyboard hints \n bb settings keyboard set \n bb settings keyboard reset [command]\n\nHost files and voice transcription\n\n bb file read|write|list|paths|mkdir|move|remove ...\n bb voice transcribe [--prompt ]\n\nVoice transcription uses the `BB_TRANSCRIPTION` model, which defaults to\n`codex/gpt-transcribe`. Override it with\n`bb-app config set BB_TRANSCRIPTION `.\n\n`bb file` supports `--host` for remote machines and `--root` on mutating\ncommands to confine access beneath an absolute directory. Use `--json` for\nmetadata and machine-readable results.\n\nClient-local UI preferences\n\nSome Settings values live only in the current browser/client. The Voice Input\nmicrophone picker stores the selected browser MediaDevices device id in\nlocalStorage as `bb.voiceInput.audioInputDeviceId`; it does not have a `bb`\ncommand and does not change the server-side transcription model.", "fileName": "bb-guide-customization.md", "kind": "instruction", "title": "bb Guide — Customization", diff --git a/packages/templates/src/templates/bb-guide-customization.md b/packages/templates/src/templates/bb-guide-customization.md index 2e425d37e..1bc151aa3 100644 --- a/packages/templates/src/templates/bb-guide-customization.md +++ b/packages/templates/src/templates/bb-guide-customization.md @@ -55,8 +55,12 @@ false in packaged builds. Turn it on to show raw provider events bb does not yet understand; development builds always show these diagnostic rows. Settings → General also includes `steerActiveThreadOnEnter`, which defaults to -false. When enabled, Enter steers a running thread and Command+Enter queues a -follow-up; when disabled, those actions are reversed. +false. Outside an open typeahead menu, enabling it makes Enter steer a running +thread and Command+Enter queue a follow-up; when disabled, those actions are +reversed. Shift+Enter inserts a newline, and unmodified Enter inserts a newline +in zen mode. On coarse-pointer touch devices, the software-keyboard Return path +inserts a newline. iPadOS WebKit preserves these Enter shortcuts for a connected +Magic Keyboard. bb settings show bb settings general