diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 4e7e1db6f2..cd438c6c1e 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -991,6 +991,7 @@ export async function loadCliConfig(
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
contextManagement,
+ experimentalBtw: settings.experimental?.btw,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
noBrowser: !!process.env['NO_BROWSER'],
diff --git a/packages/cli/src/services/BuiltinCommandLoader.test.ts b/packages/cli/src/services/BuiltinCommandLoader.test.ts
index f166c161cd..f670882e29 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.test.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.test.ts
@@ -165,6 +165,7 @@ describe('BuiltinCommandLoader', () => {
getExtensionsEnabled: vi.fn().mockReturnValue(true),
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
isAgentsEnabled: vi.fn().mockReturnValue(false),
+ isBtwEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]),
@@ -301,6 +302,22 @@ describe('BuiltinCommandLoader', () => {
expect(planCmd).toBeUndefined();
});
+ it('should include btw command when btw is enabled', async () => {
+ mockConfig.isBtwEnabled = vi.fn().mockReturnValue(true);
+ const loader = new BuiltinCommandLoader(mockConfig);
+ const commands = await loader.loadCommands(new AbortController().signal);
+ const btwCmd = commands.find((c) => c.name === 'btw');
+ expect(btwCmd).toBeDefined();
+ });
+
+ it('should exclude btw command when btw is disabled', async () => {
+ mockConfig.isBtwEnabled = vi.fn().mockReturnValue(false);
+ const loader = new BuiltinCommandLoader(mockConfig);
+ const commands = await loader.loadCommands(new AbortController().signal);
+ const btwCmd = commands.find((c) => c.name === 'btw');
+ expect(btwCmd).toBeUndefined();
+ });
+
it('should exclude agents command when agents are disabled', async () => {
mockConfig.isAgentsEnabled = vi.fn().mockReturnValue(false);
const loader = new BuiltinCommandLoader(mockConfig);
@@ -391,6 +408,7 @@ describe('BuiltinCommandLoader profile', () => {
getExtensionsEnabled: vi.fn().mockReturnValue(true),
isSkillsSupportEnabled: vi.fn().mockReturnValue(true),
isAgentsEnabled: vi.fn().mockReturnValue(false),
+ isBtwEnabled: vi.fn().mockReturnValue(false),
getMcpEnabled: vi.fn().mockReturnValue(true),
getSkillManager: vi.fn().mockReturnValue({
getAllSkills: vi.fn().mockReturnValue([]),
diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts
index 0089a6ab65..a09d1c4e54 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.ts
@@ -121,7 +121,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
aboutCommand,
...(this.config?.isAgentsEnabled() ? [agentsCommand] : []),
authCommand,
- btwCommand,
+ ...(this.config?.isBtwEnabled() ? [btwCommand] : []),
bugCommand,
{
...chatCommand,
diff --git a/packages/cli/src/ui/commands/btwCommand.test.ts b/packages/cli/src/ui/commands/btwCommand.test.ts
index ebaed41eb0..91fb1c738e 100644
--- a/packages/cli/src/ui/commands/btwCommand.test.ts
+++ b/packages/cli/src/ui/commands/btwCommand.test.ts
@@ -54,7 +54,8 @@ describe('btwCommand', () => {
expect(result).toEqual({
type: 'message',
messageType: 'error',
- content: 'Please provide a question, e.g. /btw what is this regex doing?',
+ content:
+ 'Please provide a question, e.g. /btw what does this function do?',
});
});
diff --git a/packages/cli/src/ui/commands/btwCommand.ts b/packages/cli/src/ui/commands/btwCommand.ts
index e7e6564b88..e8e77a13fe 100644
--- a/packages/cli/src/ui/commands/btwCommand.ts
+++ b/packages/cli/src/ui/commands/btwCommand.ts
@@ -29,7 +29,7 @@ export const btwCommand: SlashCommand = {
type: 'message',
messageType: 'error',
content:
- 'Please provide a question, e.g. /btw what is this regex doing?',
+ 'Please provide a question, e.g. /btw what does this function do?',
};
}
diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx
index 084949ff77..914eb2a8a3 100644
--- a/packages/cli/src/ui/components/MainContent.tsx
+++ b/packages/cli/src/ui/components/MainContent.tsx
@@ -272,22 +272,36 @@ export const MainContent = () => {
| {
type: 'history';
item: (typeof augmentedHistory)[0]['item'];
- element: React.ReactNode;
+ isExpandable: boolean;
+ isFirstThinking: boolean;
+ isFirstAfterThinking: boolean;
+ suppressNarration: boolean;
}
> = [
{ type: 'header' as const },
- ...augmentedHistory.map((data, index) => ({
- type: 'history' as const,
- item: data.item,
- element: historyItems[index],
- })),
+ ...augmentedHistory.map(
+ ({
+ item,
+ isExpandable,
+ isFirstThinking,
+ isFirstAfterThinking,
+ suppressNarration,
+ }) => ({
+ type: 'history' as const,
+ item,
+ isExpandable,
+ isFirstThinking,
+ isFirstAfterThinking,
+ suppressNarration,
+ }),
+ ),
{ type: 'pending' as const },
];
if (uiState.btwState.isActive) {
data.push({ type: 'btw' as const });
}
return data;
- }, [augmentedHistory, historyItems, uiState.btwState.isActive]);
+ }, [augmentedHistory, uiState.btwState.isActive]);
const renderItem = useCallback(
({ item }: { item: (typeof virtualizedData)[number] }) => {
@@ -300,8 +314,25 @@ export const MainContent = () => {
/>
);
} else if (item.type === 'history') {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- return (item as any).element;
+ return (
+
+ );
} else if (item.type === 'btw') {
return <>{btwDisplayNode}>;
} else {
@@ -311,7 +342,11 @@ export const MainContent = () => {
[
showHeaderDetails,
version,
+ mainAreaWidth,
+ uiState.slashCommands,
pendingItems,
+ uiState.constrainHeight,
+ staticAreaMaxItemHeight,
btwDisplayNode,
],
);
diff --git a/packages/cli/src/ui/hooks/useBtw.test.ts b/packages/cli/src/ui/hooks/useBtw.test.ts
index 4af8495d51..21b00b110e 100644
--- a/packages/cli/src/ui/hooks/useBtw.test.ts
+++ b/packages/cli/src/ui/hooks/useBtw.test.ts
@@ -190,7 +190,7 @@ describe('useBtw', () => {
// wait for the catch/finally blocks inside submitBtw to finish
try {
await submitPromise;
- } catch (_e) {
+ } catch {
// ignore AbortError
}
});
diff --git a/packages/cli/src/ui/hooks/useBtw.ts b/packages/cli/src/ui/hooks/useBtw.ts
index a1d5ecb130..65e9cdb39d 100644
--- a/packages/cli/src/ui/hooks/useBtw.ts
+++ b/packages/cli/src/ui/hooks/useBtw.ts
@@ -81,12 +81,17 @@ export const useBtw = (
const abortControllerRef = useRef(null);
const requestIdRef = useRef(0);
+ const flushTimerRef = useRef(null);
const dismissBtw = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
+ if (flushTimerRef.current) {
+ clearTimeout(flushTimerRef.current);
+ flushTimerRef.current = null;
+ }
requestIdRef.current++;
dispatch({ type: 'DISMISS' });
}, []);
@@ -108,7 +113,6 @@ export const useBtw = (
let accumulatedResponse = '';
let lastDispatchTime = 0;
- let flushTimer: NodeJS.Timeout | null = null;
const flushResponse = () => {
if (requestIdRef.current !== requestId) return;
@@ -130,22 +134,21 @@ export const useBtw = (
case GeminiEventType.Content: {
accumulatedResponse += event.value ?? '';
const now = Date.now();
- if (now - lastDispatchTime > 50) {
- if (flushTimer) {
- clearTimeout(flushTimer);
- flushTimer = null;
- }
- flushResponse();
- } else if (!flushTimer) {
- flushTimer = setTimeout(() => {
+ if (!flushTimerRef.current) {
+ const timeSinceLastDispatch = now - lastDispatchTime;
+ if (timeSinceLastDispatch >= 50) {
flushResponse();
- flushTimer = null;
- }, 50);
+ } else {
+ flushTimerRef.current = setTimeout(() => {
+ flushResponse();
+ flushTimerRef.current = null;
+ }, 50 - timeSinceLastDispatch);
+ }
}
break;
}
case GeminiEventType.Error: {
- if (flushTimer) clearTimeout(flushTimer);
+ if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
flushResponse();
const value = event.value;
@@ -174,7 +177,7 @@ export const useBtw = (
}
case GeminiEventType.Finished:
case GeminiEventType.UserCancelled:
- if (flushTimer) clearTimeout(flushTimer);
+ if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
flushResponse();
dispatch({ type: 'FINISHED' });
break;
@@ -183,7 +186,7 @@ export const useBtw = (
}
}
} catch (err) {
- if (flushTimer) clearTimeout(flushTimer);
+ if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
flushResponse();
if (err instanceof Error && err.name === 'AbortError') {
@@ -195,7 +198,7 @@ export const useBtw = (
});
}
} finally {
- if (flushTimer) clearTimeout(flushTimer);
+ if (flushTimerRef.current) clearTimeout(flushTimerRef.current);
flushResponse();
if (requestIdRef.current === requestId) {
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 24f6f5256e..345b8eb34b 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -3067,6 +3067,21 @@ describe('Config Quota & Preview Model Access', () => {
});
});
+ describe('isBtwEnabled', () => {
+ it('should return false for isBtwEnabled by default', () => {
+ const config = new Config(baseParams);
+ expect(config.isBtwEnabled()).toBe(false);
+ });
+
+ it('should return true for isBtwEnabled when experimentalBtw is true', () => {
+ const config = new Config({
+ ...baseParams,
+ experimentalBtw: true,
+ });
+ expect(config.isBtwEnabled()).toBe(true);
+ });
+ });
+
describe('getPlanModeRoutingEnabled', () => {
it('should default to true when not provided', async () => {
const config = new Config(baseParams);
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 0edd4af7b0..0c1ed56e30 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -704,6 +704,7 @@ export interface ConfigParameters {
experimentalAgentHistoryTruncationThreshold?: number;
experimentalAgentHistoryRetainedMessages?: number;
experimentalAgentHistorySummarization?: boolean;
+ experimentalBtw?: boolean;
memoryBoundaryMarkers?: string[];
topicUpdateNarration?: boolean;
@@ -942,6 +943,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryManager: boolean;
+ private readonly experimentalBtw: boolean;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
private readonly disableLLMCorrection: boolean;
@@ -1153,6 +1155,7 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalJitContext = params.experimentalJitContext ?? false;
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
+ this.experimentalBtw = params.experimentalBtw ?? false;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
this.contextManagement = {
enabled: params.contextManagement?.enabled ?? false,
@@ -2859,6 +2862,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.planEnabled;
}
+ isBtwEnabled(): boolean {
+ return this.experimentalBtw;
+ }
+
isTrackerEnabled(): boolean {
return this.trackerEnabled;
}