fix(tui): keep working tips out of the agent swarm progress line (#1189)

* fix(tui): keep working tips out of the agent swarm progress line

The activity loader is shared between the activity pane and the agent swarm progress status line. Tips were written into the loader, so they leaked into the swarm progress line and got squeezed against the bar. Keep the inline spinner text used by the swarm progress line free of tips, while the loader's own row in the activity pane still shows them.

* test(tui): stop moon loader timers in tests

MoonLoader starts a real setInterval in its constructor. Stop every loader created in the tests via afterEach so no live timer leaks past the file.
This commit is contained in:
liruifengv 2026-06-29 14:21:16 +08:00 committed by GitHub
parent c99bd1c10e
commit 04b3492e74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Fix working tips getting squeezed against the agent swarm progress bar.

View file

@ -20,6 +20,12 @@ export class MoonLoader extends Text {
private colorFn?: (s: string) => string;
private label: string;
private displayText = '';
// Inline text used when the spinner is embedded into another line (e.g. the
// agent-swarm progress status line). It intentionally excludes the tip: the
// tip is only rendered when the loader sits on its own row in the activity
// pane, otherwise it would get squeezed against whatever follows the inline
// spinner (like the swarm progress bar).
private inlineText = '';
private tip: string = '';
private availableWidth = 0;
@ -75,13 +81,14 @@ export class MoonLoader extends Text {
}
renderInline(): string {
return this.displayText;
return this.inlineText;
}
private updateDisplay(): void {
const frame = this.frames[this.currentFrame]!;
const coloredFrame = this.colorFn ? this.colorFn(frame) : frame;
const baseText = this.label ? `${coloredFrame} ${this.label}` : coloredFrame;
this.inlineText = baseText;
let text = baseText;
if (this.tip) {
const withTip = baseText + currentTheme.fg('textDim', this.tip);

View file

@ -0,0 +1,42 @@
import type { TUI } from '@earendil-works/pi-tui';
import { afterEach, describe, expect, it } from 'vitest';
import { MoonLoader } from '#/tui/components/chrome/moon-loader';
// MoonLoader starts a real setInterval in its constructor, so every loader
// created in these tests must be stopped to avoid leaving live timers behind.
const loaders: MoonLoader[] = [];
function createLoader(): MoonLoader {
const ui = { requestRender() {} } as unknown as TUI;
const loader = new MoonLoader(ui, 'moon');
loaders.push(loader);
return loader;
}
afterEach(() => {
for (const loader of loaders) loader.stop();
loaders.length = 0;
});
describe('MoonLoader', () => {
it('keeps the tip out of renderInline so it does not squeeze against the swarm progress bar', () => {
const loader = createLoader();
loader.setTip(' · Tip: ctrl+s: steer mid-turn');
loader.setAvailableWidth(80);
const inline = loader.renderInline();
expect(inline).not.toContain('Tip');
expect(inline).not.toContain('steer');
expect(inline.trim().length).toBeGreaterThan(0);
});
it('still shows the tip on its own row when width allows', () => {
const loader = createLoader();
loader.setTip(' · Tip: ctrl+s: steer mid-turn');
loader.setAvailableWidth(80);
const row = loader.render(80).join('\n');
expect(row).toContain('Tip: ctrl+s: steer mid-turn');
});
});