Skip to content

Commit ac79462

Browse files
TylerBarnesMastra Code (openai/gpt-5.5)
andauthored
feat(mastracode): add quiet mode (#16771)
Co-authored-by: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai>
1 parent 457143c commit ac79462

34 files changed

Lines changed: 2698 additions & 127 deletions

.changeset/quiet-mode-rollout.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'mastracode': patch
3+
'@mastra/core': patch
4+
---
5+
6+
Improved MastraCode quiet mode so terminal sessions are easier to scan.
7+
8+
- Quiet mode is now the default for new installs, and existing classic users get a one-time prompt to choose whether to enable it.
9+
- Added compact tool previews with a configurable preview-line limit, including an option to hide previews.
10+
- Improved repeated tool-call rendering, path continuation handling, task wrapping, shell/error previews, and spacing between tools, messages, plans, and completed subagents.
11+
- Added edited line ranges to workspace edit results so tool UIs can show where replacements happened.

mastracode/src/onboarding/__tests__/settings.test.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
4-
import { describe, expect, it } from 'vitest';
4+
import { describe, expect, it, vi } from 'vitest';
55

66
import {
77
getCustomProviderId,
@@ -24,6 +24,7 @@ function createSettings(overrides?: Partial<GlobalSettings>): GlobalSettings {
2424
version: 0,
2525
modePackId: null,
2626
omPackId: null,
27+
quietModePreferenceSelected: true,
2728
},
2829
models: {
2930
activeModelPackId: 'anthropic',
@@ -37,8 +38,10 @@ function createSettings(overrides?: Partial<GlobalSettings>): GlobalSettings {
3738
omCavemanObservations: null,
3839
omObserveAttachments: null,
3940
subagentModels: {},
41+
goalJudgeModel: null,
42+
goalMaxTurns: null,
4043
},
41-
preferences: { yolo: null, theme: 'auto', thinkingLevel: 'off', quietMode: false },
44+
preferences: { yolo: null, theme: 'auto', thinkingLevel: 'off', quietMode: false, quietModeMaxToolPreviewLines: 2 },
4245
storage,
4346
customProviders: [],
4447
customModelPacks: [
@@ -62,6 +65,7 @@ function createSettings(overrides?: Partial<GlobalSettings>): GlobalSettings {
6265
viewport: { width: 1280, height: 720 },
6366
stagehand: { env: 'LOCAL' },
6467
},
68+
observability: { resources: {}, localTracing: false },
6569
...overrides,
6670
};
6771
}
@@ -104,6 +108,110 @@ describe('customProviders parsing/persistence', () => {
104108

105109
expect(settings.customProviders).toEqual([]);
106110
expect(settings.preferences.thinkingLevel).toBe('off');
111+
expect(settings.preferences.quietModeMaxToolPreviewLines).toBe(2);
112+
});
113+
});
114+
115+
it('normalizes quiet mode preview line limits', () => {
116+
withTempSettingsFile(filePath => {
117+
writeFileSync(
118+
filePath,
119+
JSON.stringify({ onboarding: {}, models: {}, preferences: { quietModeMaxToolPreviewLines: 2.9 }, storage: {} }),
120+
'utf-8',
121+
);
122+
expect(loadSettings(filePath).preferences.quietModeMaxToolPreviewLines).toBe(2);
123+
124+
writeFileSync(
125+
filePath,
126+
JSON.stringify({ onboarding: {}, models: {}, preferences: { quietModeMaxToolPreviewLines: -4 }, storage: {} }),
127+
'utf-8',
128+
);
129+
expect(loadSettings(filePath).preferences.quietModeMaxToolPreviewLines).toBe(0);
130+
131+
writeFileSync(
132+
filePath,
133+
JSON.stringify({ onboarding: {}, models: {}, preferences: { quietModeMaxToolPreviewLines: 999 }, storage: {} }),
134+
'utf-8',
135+
);
136+
expect(loadSettings(filePath).preferences.quietModeMaxToolPreviewLines).toBe(8);
137+
138+
writeFileSync(filePath, '{}', 'utf-8');
139+
vi.spyOn(JSON, 'parse').mockReturnValueOnce({
140+
onboarding: { quietModePreferenceSelected: true },
141+
models: {},
142+
preferences: { quietModeMaxToolPreviewLines: Number.NaN },
143+
storage: {},
144+
});
145+
expect(loadSettings(filePath).preferences.quietModeMaxToolPreviewLines).toBe(2);
146+
vi.mocked(JSON.parse).mockRestore();
147+
148+
vi.spyOn(JSON, 'parse').mockReturnValueOnce({
149+
onboarding: { quietModePreferenceSelected: true },
150+
models: {},
151+
preferences: { quietModeMaxToolPreviewLines: Number.POSITIVE_INFINITY },
152+
storage: {},
153+
});
154+
expect(loadSettings(filePath).preferences.quietModeMaxToolPreviewLines).toBe(2);
155+
vi.mocked(JSON.parse).mockRestore();
156+
});
157+
});
158+
159+
it('defaults new installs to quiet mode with the preference selected', () => {
160+
withTempSettingsFile(filePath => {
161+
const settings = loadSettings(filePath);
162+
163+
expect(settings.preferences.quietMode).toBe(true);
164+
expect(settings.onboarding.quietModePreferenceSelected).toBe(true);
165+
});
166+
});
167+
168+
it('marks existing classic users as needing the quiet mode preference prompt', () => {
169+
withTempSettingsFile(filePath => {
170+
writeFileSync(
171+
filePath,
172+
JSON.stringify({ onboarding: {}, models: {}, preferences: { quietMode: false }, storage: {} }),
173+
'utf-8',
174+
);
175+
176+
const settings = loadSettings(filePath);
177+
178+
expect(settings.preferences.quietMode).toBe(false);
179+
expect(settings.onboarding.quietModePreferenceSelected).toBe(false);
180+
});
181+
});
182+
183+
it('does not prompt existing users who already enabled quiet mode', () => {
184+
withTempSettingsFile(filePath => {
185+
writeFileSync(
186+
filePath,
187+
JSON.stringify({ onboarding: {}, models: {}, preferences: { quietMode: true }, storage: {} }),
188+
'utf-8',
189+
);
190+
191+
const settings = loadSettings(filePath);
192+
193+
expect(settings.preferences.quietMode).toBe(true);
194+
expect(settings.onboarding.quietModePreferenceSelected).toBe(true);
195+
});
196+
});
197+
198+
it('preserves existing quiet mode preference selections', () => {
199+
withTempSettingsFile(filePath => {
200+
writeFileSync(
201+
filePath,
202+
JSON.stringify({
203+
onboarding: { quietModePreferenceSelected: true },
204+
models: {},
205+
preferences: { quietMode: false },
206+
storage: {},
207+
}),
208+
'utf-8',
209+
);
210+
211+
const settings = loadSettings(filePath);
212+
213+
expect(settings.preferences.quietMode).toBe(false);
214+
expect(settings.onboarding.quietModePreferenceSelected).toBe(true);
107215
});
108216
});
109217

mastracode/src/onboarding/settings.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export interface GlobalSettings {
119119
version: number;
120120
modePackId: string | null;
121121
omPackId: string | null;
122+
quietModePreferenceSelected: boolean;
122123
};
123124
// Global model preferences (applied to new threads)
124125
models: {
@@ -187,6 +188,8 @@ export interface GlobalSettings {
187188
thinkingLevel: ThinkingLevelSetting;
188189
/** When true, components like subagent output collapse to compact summaries on completion. */
189190
quietMode: boolean;
191+
/** Maximum quiet-mode detail preview lines for compact tool calls. Set to 0 to hide previews. */
192+
quietModeMaxToolPreviewLines: number;
190193
};
191194
// Storage backend configuration
192195
storage: StorageSettings;
@@ -238,6 +241,7 @@ const DEFAULTS: GlobalSettings = {
238241
version: 0,
239242
modePackId: null,
240243
omPackId: null,
244+
quietModePreferenceSelected: true,
241245
},
242246
models: {
243247
activeModelPackId: null,
@@ -259,6 +263,7 @@ const DEFAULTS: GlobalSettings = {
259263
theme: 'auto',
260264
thinkingLevel: 'off',
261265
quietMode: false,
266+
quietModeMaxToolPreviewLines: 2,
262267
},
263268
storage: { ...STORAGE_DEFAULTS },
264269
customModelPacks: [],
@@ -278,23 +283,51 @@ const DEFAULTS: GlobalSettings = {
278283
};
279284

280285
const THINKING_LEVEL_VALUES: ThinkingLevelSetting[] = ['off', 'low', 'medium', 'high', 'xhigh'];
286+
const QUIET_MODE_MAX_TOOL_PREVIEW_LINES_MAX = 8;
281287

282288
function parseThinkingLevel(value: unknown): ThinkingLevelSetting {
283289
return typeof value === 'string' && THINKING_LEVEL_VALUES.includes(value as ThinkingLevelSetting)
284290
? (value as ThinkingLevelSetting)
285291
: DEFAULTS.preferences.thinkingLevel;
286292
}
287293

294+
function parseQuietModeMaxToolPreviewLines(value: unknown): number {
295+
const rawValue =
296+
typeof value === 'number' && Number.isFinite(value) ? value : DEFAULTS.preferences.quietModeMaxToolPreviewLines;
297+
return Math.min(QUIET_MODE_MAX_TOOL_PREVIEW_LINES_MAX, Math.max(0, Math.floor(rawValue)));
298+
}
299+
288300
function parsePreferences(rawPreferences: unknown): GlobalSettings['preferences'] {
289301
const raw = rawPreferences && typeof rawPreferences === 'object' ? (rawPreferences as Record<string, unknown>) : {};
290302

291303
return {
292304
...DEFAULTS.preferences,
293305
...raw,
294306
thinkingLevel: parseThinkingLevel(raw.thinkingLevel),
307+
quietModeMaxToolPreviewLines: parseQuietModeMaxToolPreviewLines(raw.quietModeMaxToolPreviewLines),
295308
};
296309
}
297310

311+
function hasQuietModePreferenceSelected(rawOnboarding: unknown): boolean {
312+
return Boolean(
313+
rawOnboarding &&
314+
typeof rawOnboarding === 'object' &&
315+
Object.prototype.hasOwnProperty.call(rawOnboarding, 'quietModePreferenceSelected'),
316+
);
317+
}
318+
319+
function applyQuietModePreferenceRollout(settings: GlobalSettings, rawOnboarding: unknown): void {
320+
if (hasQuietModePreferenceSelected(rawOnboarding)) return;
321+
settings.onboarding.quietModePreferenceSelected = settings.preferences.quietMode === true;
322+
}
323+
324+
function getNewInstallDefaults(): GlobalSettings {
325+
const settings = structuredClone(DEFAULTS);
326+
settings.preferences.quietMode = true;
327+
settings.onboarding.quietModePreferenceSelected = true;
328+
return settings;
329+
}
330+
298331
export function getSettingsPath(): string {
299332
return join(getAppDataDir(), 'settings.json');
300333
}
@@ -482,6 +515,7 @@ function migrateFromAuth(settingsPath: string): boolean {
482515
browser: parseBrowserSettings(raw.browser),
483516
observability: parseObservabilitySettings(raw.observability),
484517
};
518+
applyQuietModePreferenceRollout(settings, raw.onboarding);
485519
} catch {
486520
settings = structuredClone(DEFAULTS);
487521
}
@@ -576,7 +610,7 @@ export function loadSettings(filePath: string = getSettingsPath()): GlobalSettin
576610
// One-time migration: move model data from auth.json into settings.json
577611
migrateFromAuth(filePath);
578612

579-
if (!existsSync(filePath)) return structuredClone(DEFAULTS);
613+
if (!existsSync(filePath)) return getNewInstallDefaults();
580614
try {
581615
const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
582616
// Spread raw first to preserve unknown top-level keys (forward-compatibility),
@@ -604,6 +638,10 @@ export function loadSettings(filePath: string = getSettingsPath()): GlobalSettin
604638

605639
// Migrate legacy omModelId → omModelOverride
606640
let settingsChanged = false;
641+
if (!hasQuietModePreferenceSelected(raw.onboarding)) {
642+
applyQuietModePreferenceRollout(settings, raw.onboarding);
643+
settingsChanged = true;
644+
}
607645
if (raw.models?.omModelId && !settings.models.omModelOverride) {
608646
settings.models.omModelOverride = raw.models.omModelId;
609647
settingsChanged = true;

0 commit comments

Comments
 (0)