refactor(cli): make /btw non-blocking with fire-and-forget API call

Run the /btw API call as fire-and-forget in interactive mode so the
main conversation is not blocked while waiting for the answer. The
action now returns immediately after setting the pending item, and
the background promise updates the UI when the answer arrives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Shaojin Wen 2026-03-14 17:41:09 +08:00
parent b1b5f72507
commit 1b651d5c4f
2 changed files with 60 additions and 29 deletions

View file

@ -110,6 +110,10 @@ describe('btwCommand', () => {
});
describe('interactive mode', () => {
// Helper to flush microtask queue so fire-and-forget promises settle.
const flushPromises = () =>
new Promise<void>((resolve) => setTimeout(resolve, 0));
it('should set pending item and add completed item on success', async () => {
mockGenerateContent.mockResolvedValue({
candidates: [
@ -123,6 +127,7 @@ describe('btwCommand', () => {
await btwCommand.action!(mockContext, 'what is the meaning of life?');
// Action returns immediately; pending item is set synchronously
expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith({
type: MessageType.BTW,
btw: {
@ -132,6 +137,9 @@ describe('btwCommand', () => {
},
});
// Wait for background promise to settle
await flushPromises();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.BTW,
@ -158,6 +166,7 @@ describe('btwCommand', () => {
});
await btwCommand.action!(mockContext, 'my question');
await flushPromises();
expect(mockGenerateContent).toHaveBeenCalledWith(
[
@ -181,6 +190,7 @@ describe('btwCommand', () => {
mockGenerateContent.mockRejectedValue(new Error('API error'));
await btwCommand.action!(mockContext, 'test question');
await flushPromises();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@ -197,6 +207,7 @@ describe('btwCommand', () => {
mockGenerateContent.mockRejectedValue('string error');
await btwCommand.action!(mockContext, 'test question');
await flushPromises();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@ -255,6 +266,7 @@ describe('btwCommand', () => {
});
await btwCommand.action!(abortContext, 'test question');
await flushPromises();
expect(abortContext.ui.addItem).not.toHaveBeenCalled();
expect(abortContext.ui.setPendingItem).toHaveBeenLastCalledWith(null);
@ -266,6 +278,7 @@ describe('btwCommand', () => {
});
await btwCommand.action!(mockContext, 'test question');
await flushPromises();
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
@ -279,6 +292,25 @@ describe('btwCommand', () => {
expect.any(Number),
);
});
it('should return void immediately without blocking', async () => {
mockGenerateContent.mockResolvedValue({
candidates: [{ content: { parts: [{ text: 'answer' }] } }],
});
const result = await btwCommand.action!(mockContext, 'test question');
// Action should return void (not awaiting the API call)
expect(result).toBeUndefined();
// addItem not yet called — background promise hasn't settled
expect(mockContext.ui.addItem).not.toHaveBeenCalled();
await flushPromises();
// Now the background work has completed
expect(mockContext.ui.addItem).toHaveBeenCalled();
});
});
describe('non-interactive mode', () => {