mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-07-09 17:28:45 +00:00
feat(coding-agent): add configurable chat padding
This commit is contained in:
parent
1d48616328
commit
5a64f09b41
22 changed files with 292 additions and 88 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `transcriptIndent` and `toolIndent` settings to configure interactive transcript and tool/message indentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `--session` and `SessionManager.open()` to reject non-empty invalid session files without overwriting them ([#6002](https://github.com/earendil-works/pi/issues/6002)).
|
||||
|
|
|
|||
|
|
@ -2074,6 +2074,7 @@ pi.registerTool({
|
|||
- `lastComponent` - the previously returned component for that slot, if any
|
||||
- `invalidate()` - request a rerender of this tool row
|
||||
- `toolCallId`, `cwd`, `executionStarted`, `argsComplete`, `isPartial`, `expanded`, `showImages`, `isError`
|
||||
- `toolIndent` - current tool/message indentation in terminal columns; default shells apply it automatically, self-shell renderers can opt in (use `context.toolIndent ?? 1`)
|
||||
|
||||
Use `context.state` for cross-slot shared state. Keep slot-local caches on the returned component instance when you want to reuse and mutate the same component across renders.
|
||||
|
||||
|
|
@ -2154,6 +2155,7 @@ Custom editors and `ctx.ui.custom()` components receive `keybindings: Keybinding
|
|||
#### Best Practices
|
||||
|
||||
- Use `Text` with padding `(0, 0)`. The default Box handles padding.
|
||||
- Use `context.toolIndent` only when `renderShell: "self"` needs to align with the user's tool indentation setting.
|
||||
- Use `\n` for multi-line content.
|
||||
- Handle `isPartial` for streaming progress.
|
||||
- Support `expanded` for detail on demand.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ Use `/trust` in interactive mode to save a project trust decision for future ses
|
|||
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` |
|
||||
| `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` |
|
||||
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
|
||||
| `transcriptIndent` | number | `1` | Horizontal padding for assistant transcript text (0-4) |
|
||||
| `toolIndent` | number | `1` | Horizontal padding for tool and message blocks (0-4) |
|
||||
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
|
||||
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
|
||||
|
||||
|
|
|
|||
|
|
@ -3034,6 +3034,7 @@ export class AgentSession {
|
|||
getToolDefinition: (name) => this.getToolDefinition(name),
|
||||
theme,
|
||||
cwd: this.sessionManager.getCwd(),
|
||||
toolIndent: this.settingsManager.getToolIndent(),
|
||||
});
|
||||
|
||||
return await exportSessionToHtml(this.sessionManager, this.state, {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ export interface ToolHtmlRendererDeps {
|
|||
cwd: string;
|
||||
/** Terminal width for rendering (default: 100) */
|
||||
width?: number;
|
||||
/** Tool indentation to expose to custom tool renderers (default: 1) */
|
||||
toolIndent?: number;
|
||||
}
|
||||
|
||||
export interface ToolHtmlRenderer {
|
||||
|
|
@ -56,7 +58,7 @@ function trimRenderedResultLines(lines: string[]): string[] {
|
|||
}
|
||||
|
||||
export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRenderer {
|
||||
const { getToolDefinition, theme, cwd, width = 100 } = deps;
|
||||
const { getToolDefinition, theme, cwd, width = 100, toolIndent = 1 } = deps;
|
||||
|
||||
const renderedCallComponents = new Map<string, Component>();
|
||||
const renderedResultComponents = new Map<string, Component>();
|
||||
|
|
@ -91,6 +93,7 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
|||
isPartial,
|
||||
expanded,
|
||||
showImages: false,
|
||||
toolIndent,
|
||||
isError,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -425,6 +425,8 @@ export interface ToolRenderContext<TState = any, TArgs = any> {
|
|||
expanded: boolean;
|
||||
/** Whether inline images are currently shown in the TUI. */
|
||||
showImages: boolean;
|
||||
/** Current tool/message indentation in terminal columns. Core-owned shells apply it automatically. */
|
||||
toolIndent?: number;
|
||||
/** Whether the current result is an error. */
|
||||
isError: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,15 @@ export type DefaultProjectTrust = "ask" | "always" | "never";
|
|||
|
||||
export type TransportSetting = Transport;
|
||||
|
||||
export const INDENT_VALUES = [0, 1, 2, 3, 4] as const;
|
||||
|
||||
function normalizeIndent(value: unknown): number {
|
||||
if (typeof value !== "number" || !Number.isInteger(value)) {
|
||||
return 1;
|
||||
}
|
||||
return INDENT_VALUES.includes(value as (typeof INDENT_VALUES)[number]) ? value : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package source for npm/git packages.
|
||||
* - String form: load all resources from the package
|
||||
|
|
@ -114,6 +123,8 @@ export interface Settings {
|
|||
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
|
||||
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
||||
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
|
||||
transcriptIndent?: number; // Horizontal padding for assistant transcript text (default: 1; allowed: 0-4)
|
||||
toolIndent?: number; // Horizontal padding for tool and message blocks (default: 1; allowed: 0-4)
|
||||
markdown?: MarkdownSettings;
|
||||
warnings?: WarningSettings;
|
||||
sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)
|
||||
|
|
@ -1169,6 +1180,26 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getTranscriptIndent(): number {
|
||||
return normalizeIndent(this.settings.transcriptIndent);
|
||||
}
|
||||
|
||||
setTranscriptIndent(indent: number): void {
|
||||
this.globalSettings.transcriptIndent = normalizeIndent(indent);
|
||||
this.markModified("transcriptIndent");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getToolIndent(): number {
|
||||
return normalizeIndent(this.settings.toolIndent);
|
||||
}
|
||||
|
||||
setToolIndent(indent: number): void {
|
||||
this.globalSettings.toolIndent = normalizeIndent(indent);
|
||||
this.markModified("toolIndent");
|
||||
this.save();
|
||||
}
|
||||
|
||||
getAutocompleteMaxVisible(): number {
|
||||
return this.settings.autocompleteMaxVisible ?? 5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,8 +144,8 @@ type EditCallRenderComponent = Box & {
|
|||
settledError?: boolean;
|
||||
};
|
||||
|
||||
function createEditCallRenderComponent(): EditCallRenderComponent {
|
||||
return Object.assign(new Box(1, 1, (text: string) => text), {
|
||||
function createEditCallRenderComponent(toolIndent: number): EditCallRenderComponent {
|
||||
return Object.assign(new Box(toolIndent, 1, (text: string) => text), {
|
||||
preview: undefined as EditPreview | undefined,
|
||||
previewArgsKey: undefined as string | undefined,
|
||||
previewPending: false,
|
||||
|
|
@ -153,7 +153,11 @@ function createEditCallRenderComponent(): EditCallRenderComponent {
|
|||
});
|
||||
}
|
||||
|
||||
function getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {
|
||||
function getEditCallRenderComponent(
|
||||
state: EditRenderState,
|
||||
lastComponent: unknown,
|
||||
toolIndent: number,
|
||||
): EditCallRenderComponent {
|
||||
if (lastComponent instanceof Box) {
|
||||
const component = lastComponent as EditCallRenderComponent;
|
||||
state.callComponent = component;
|
||||
|
|
@ -162,7 +166,7 @@ function getEditCallRenderComponent(state: EditRenderState, lastComponent: unkno
|
|||
if (state.callComponent) {
|
||||
return state.callComponent;
|
||||
}
|
||||
const component = createEditCallRenderComponent();
|
||||
const component = createEditCallRenderComponent(toolIndent);
|
||||
state.callComponent = component;
|
||||
return component;
|
||||
}
|
||||
|
|
@ -361,7 +365,8 @@ export function createEditToolDefinition(
|
|||
});
|
||||
},
|
||||
renderCall(args, theme, context) {
|
||||
const component = getEditCallRenderComponent(context.state, context.lastComponent);
|
||||
const toolIndent = context.toolIndent ?? 1;
|
||||
const component = getEditCallRenderComponent(context.state, context.lastComponent, toolIndent);
|
||||
const previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);
|
||||
const argsKey = previewInput
|
||||
? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })
|
||||
|
|
@ -426,7 +431,7 @@ export function createEditToolDefinition(
|
|||
return component;
|
||||
}
|
||||
component.addChild(new Spacer(1));
|
||||
component.addChild(new Text(output, 1, 0));
|
||||
component.addChild(new Text(output, context.toolIndent ?? 1, 0));
|
||||
return component;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,9 +68,11 @@ export class ArminComponent implements Component {
|
|||
private cachedWidth = 0;
|
||||
private gridVersion = 0;
|
||||
private cachedVersion = -1;
|
||||
private toolIndent: number;
|
||||
|
||||
constructor(ui: TUI) {
|
||||
constructor(ui: TUI, toolIndent = 1) {
|
||||
this.ui = ui;
|
||||
this.toolIndent = toolIndent;
|
||||
this.effect = EFFECTS[Math.floor(Math.random() * EFFECTS.length)];
|
||||
this.finalGrid = buildFinalGrid();
|
||||
this.currentGrid = this.createEmptyGrid();
|
||||
|
|
@ -88,20 +90,21 @@ export class ArminComponent implements Component {
|
|||
return this.cachedLines;
|
||||
}
|
||||
|
||||
const padding = 1;
|
||||
const availableWidth = width - padding;
|
||||
const padding = this.toolIndent;
|
||||
const leftPadding = " ".repeat(padding);
|
||||
const availableWidth = Math.max(0, width - padding);
|
||||
|
||||
this.cachedLines = this.currentGrid.map((row) => {
|
||||
// Clip row to available width before applying color
|
||||
const clipped = row.slice(0, availableWidth).join("");
|
||||
const padRight = Math.max(0, width - padding - clipped.length);
|
||||
return ` ${theme.fg("accent", clipped)}${" ".repeat(padRight)}`;
|
||||
return `${leftPadding}${theme.fg("accent", clipped)}${" ".repeat(padRight)}`;
|
||||
});
|
||||
|
||||
// Add "ARMIN SAYS HI" at the end
|
||||
const message = "ARMIN SAYS HI";
|
||||
const msgPadRight = Math.max(0, width - padding - message.length);
|
||||
this.cachedLines.push(` ${theme.fg("accent", message)}${" ".repeat(msgPadRight)}`);
|
||||
this.cachedLines.push(`${leftPadding}${theme.fg("accent", message)}${" ".repeat(msgPadRight)}`);
|
||||
|
||||
this.cachedWidth = width;
|
||||
this.cachedVersion = this.gridVersion;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class AssistantMessageComponent extends Container {
|
|||
private hideThinkingBlock: boolean;
|
||||
private markdownTheme: MarkdownTheme;
|
||||
private hiddenThinkingLabel: string;
|
||||
private transcriptIndent: number;
|
||||
private lastMessage?: AssistantMessage;
|
||||
private hasToolCalls = false;
|
||||
|
||||
|
|
@ -22,12 +23,14 @@ export class AssistantMessageComponent extends Container {
|
|||
hideThinkingBlock = false,
|
||||
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
||||
hiddenThinkingLabel = "Thinking...",
|
||||
transcriptIndent = 1,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.hideThinkingBlock = hideThinkingBlock;
|
||||
this.markdownTheme = markdownTheme;
|
||||
this.hiddenThinkingLabel = hiddenThinkingLabel;
|
||||
this.transcriptIndent = transcriptIndent;
|
||||
|
||||
// Container for text/thinking content
|
||||
this.contentContainer = new Container();
|
||||
|
|
@ -90,7 +93,9 @@ export class AssistantMessageComponent extends Container {
|
|||
if (content.type === "text" && content.text.trim()) {
|
||||
// Assistant text messages with no background - trim the text
|
||||
// Set paddingY=0 to avoid extra spacing before tool executions
|
||||
this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0, this.markdownTheme));
|
||||
this.contentContainer.addChild(
|
||||
new Markdown(content.text.trim(), this.transcriptIndent, 0, this.markdownTheme),
|
||||
);
|
||||
} else if (content.type === "thinking" && content.thinking.trim()) {
|
||||
// Add spacing only when another visible assistant content block follows.
|
||||
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
|
||||
|
|
@ -101,7 +106,7 @@ export class AssistantMessageComponent extends Container {
|
|||
if (this.hideThinkingBlock) {
|
||||
// Show static thinking label when hidden
|
||||
this.contentContainer.addChild(
|
||||
new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), 1, 0),
|
||||
new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.transcriptIndent, 0),
|
||||
);
|
||||
if (hasVisibleContentAfter) {
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
|
|
@ -109,7 +114,7 @@ export class AssistantMessageComponent extends Container {
|
|||
} else {
|
||||
// Thinking traces in thinkingText color, italic
|
||||
this.contentContainer.addChild(
|
||||
new Markdown(content.thinking.trim(), 1, 0, this.markdownTheme, {
|
||||
new Markdown(content.thinking.trim(), this.transcriptIndent, 0, this.markdownTheme, {
|
||||
color: (text: string) => theme.fg("thinkingText", text),
|
||||
italic: true,
|
||||
}),
|
||||
|
|
@ -134,7 +139,7 @@ export class AssistantMessageComponent extends Container {
|
|||
"error",
|
||||
"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
||||
),
|
||||
1,
|
||||
this.transcriptIndent,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
|
@ -145,11 +150,11 @@ export class AssistantMessageComponent extends Container {
|
|||
? message.errorMessage
|
||||
: "Operation aborted";
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), this.transcriptIndent, 0));
|
||||
} else if (message.stopReason === "error") {
|
||||
const errorMsg = message.errorMessage || "Unknown error";
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
||||
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), this.transcriptIndent, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,10 +28,12 @@ export class BashExecutionComponent extends Container {
|
|||
private fullOutputPath?: string;
|
||||
private expanded = false;
|
||||
private contentContainer: Container;
|
||||
private toolIndent: number;
|
||||
|
||||
constructor(command: string, ui: TUI, excludeFromContext = false) {
|
||||
constructor(command: string, ui: TUI, excludeFromContext = false, toolIndent = 1) {
|
||||
super();
|
||||
this.command = command;
|
||||
this.toolIndent = toolIndent;
|
||||
|
||||
// Use dim border for excluded-from-context commands (!! prefix)
|
||||
const colorKey = excludeFromContext ? "dim" : "bashMode";
|
||||
|
|
@ -48,7 +50,7 @@ export class BashExecutionComponent extends Container {
|
|||
this.addChild(this.contentContainer);
|
||||
|
||||
// Command header
|
||||
const header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0);
|
||||
const header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), this.toolIndent, 0);
|
||||
this.contentContainer.addChild(header);
|
||||
|
||||
// Loader
|
||||
|
|
@ -135,7 +137,7 @@ export class BashExecutionComponent extends Container {
|
|||
this.contentContainer.clear();
|
||||
|
||||
// Command header
|
||||
const header = new Text(theme.fg("bashMode", theme.bold(`$ ${this.command}`)), 1, 0);
|
||||
const header = new Text(theme.fg("bashMode", theme.bold(`$ ${this.command}`)), this.toolIndent, 0);
|
||||
this.contentContainer.addChild(header);
|
||||
|
||||
// Output
|
||||
|
|
@ -143,7 +145,7 @@ export class BashExecutionComponent extends Container {
|
|||
if (this.expanded) {
|
||||
// Show all lines
|
||||
const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n");
|
||||
this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
||||
this.contentContainer.addChild(new Text(`\n${displayText}`, this.toolIndent, 0));
|
||||
} else {
|
||||
// Use shared visual truncation utility with width-aware caching
|
||||
const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n");
|
||||
|
|
@ -153,7 +155,7 @@ export class BashExecutionComponent extends Container {
|
|||
this.contentContainer.addChild({
|
||||
render: (width: number) => {
|
||||
if (cachedLines === undefined || cachedWidth !== width) {
|
||||
const result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, 1);
|
||||
const result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, this.toolIndent);
|
||||
cachedLines = result.visualLines;
|
||||
cachedWidth = width;
|
||||
}
|
||||
|
|
@ -199,7 +201,7 @@ export class BashExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
if (statusParts.length > 0) {
|
||||
this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0));
|
||||
this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, this.toolIndent, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export class BranchSummaryMessageComponent extends Box {
|
|||
private message: BranchSummaryMessage;
|
||||
private markdownTheme: MarkdownTheme;
|
||||
|
||||
constructor(message: BranchSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) {
|
||||
super(1, 1, (t) => theme.bg("customMessageBg", t));
|
||||
constructor(message: BranchSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme(), toolIndent = 1) {
|
||||
super(toolIndent, 1, (t) => theme.bg("customMessageBg", t));
|
||||
this.message = message;
|
||||
this.markdownTheme = markdownTheme;
|
||||
this.updateDisplay();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export class CompactionSummaryMessageComponent extends Box {
|
|||
private message: CompactionSummaryMessage;
|
||||
private markdownTheme: MarkdownTheme;
|
||||
|
||||
constructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) {
|
||||
super(1, 1, (t) => theme.bg("customMessageBg", t));
|
||||
constructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme(), toolIndent = 1) {
|
||||
super(toolIndent, 1, (t) => theme.bg("customMessageBg", t));
|
||||
this.message = message;
|
||||
this.markdownTheme = markdownTheme;
|
||||
this.updateDisplay();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export class CustomMessageComponent extends Container {
|
|||
message: CustomMessage<unknown>,
|
||||
customRenderer?: MessageRenderer,
|
||||
markdownTheme: MarkdownTheme = getMarkdownTheme(),
|
||||
toolIndent = 1,
|
||||
) {
|
||||
super();
|
||||
this.message = message;
|
||||
|
|
@ -30,7 +31,7 @@ export class CustomMessageComponent extends Container {
|
|||
this.addChild(new Spacer(1));
|
||||
|
||||
// Create box with purple background (used for default rendering)
|
||||
this.box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
||||
this.box = new Box(toolIndent, 1, (t) => theme.bg("customMessageBg", t));
|
||||
|
||||
this.rebuild();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,17 @@ function loadImageBase64(): string | undefined {
|
|||
}
|
||||
|
||||
export class EarendilAnnouncementComponent extends Container {
|
||||
constructor() {
|
||||
constructor(toolIndent = 1) {
|
||||
super();
|
||||
const addText = (content: string) => {
|
||||
this.addChild(new Text(content, toolIndent, 0));
|
||||
};
|
||||
|
||||
this.addChild(new DynamicBorder((text) => theme.fg("accent", text)));
|
||||
this.addChild(new Text(theme.bold(theme.fg("accent", "pi has joined Earendil")), 1, 0));
|
||||
addText(theme.bold(theme.fg("accent", "pi has joined Earendil")));
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("muted", "Read the blog post:"), 1, 0));
|
||||
this.addChild(new Text(theme.fg("mdLink", BLOG_URL), 1, 0));
|
||||
addText(theme.fg("muted", "Read the blog post:"));
|
||||
addText(theme.fg("mdLink", BLOG_URL));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
const imageBase64 = loadImageBase64();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
Text,
|
||||
} from "@earendil-works/pi-tui";
|
||||
import { formatHttpIdleTimeoutMs, HTTP_IDLE_TIMEOUT_CHOICES } from "../../../core/http-dispatcher.ts";
|
||||
import type { DefaultProjectTrust, WarningSettings } from "../../../core/settings-manager.ts";
|
||||
import { type DefaultProjectTrust, INDENT_VALUES, type WarningSettings } from "../../../core/settings-manager.ts";
|
||||
import {
|
||||
getSelectListTheme,
|
||||
getSettingsListTheme,
|
||||
|
|
@ -71,6 +71,8 @@ export interface SettingsConfig {
|
|||
treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
|
||||
showHardwareCursor: boolean;
|
||||
editorPaddingX: number;
|
||||
transcriptIndent: number;
|
||||
toolIndent: number;
|
||||
autocompleteMaxVisible: number;
|
||||
quietStartup: boolean;
|
||||
defaultProjectTrust: DefaultProjectTrust;
|
||||
|
|
@ -100,6 +102,8 @@ export interface SettingsCallbacks {
|
|||
onTreeFilterModeChange: (mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all") => void;
|
||||
onShowHardwareCursorChange: (enabled: boolean) => void;
|
||||
onEditorPaddingXChange: (padding: number) => void;
|
||||
onTranscriptIndentChange: (indent: number) => void;
|
||||
onToolIndentChange: (indent: number) => void;
|
||||
onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
|
||||
onQuietStartupChange: (enabled: boolean) => void;
|
||||
onDefaultProjectTrustChange: (defaultProjectTrust: DefaultProjectTrust) => void;
|
||||
|
|
@ -676,9 +680,29 @@ export class SettingsSelectorComponent extends Container {
|
|||
values: ["0", "1", "2", "3"],
|
||||
});
|
||||
|
||||
// Autocomplete max visible toggle (insert after editor-padding)
|
||||
// Transcript indent toggle (insert after editor-padding)
|
||||
const editorPaddingIndex = items.findIndex((item) => item.id === "editor-padding");
|
||||
items.splice(editorPaddingIndex + 1, 0, {
|
||||
id: "transcript-indent",
|
||||
label: "Transcript indent",
|
||||
description: "Horizontal padding for assistant transcript text (0-4)",
|
||||
currentValue: String(config.transcriptIndent),
|
||||
values: INDENT_VALUES.map((value) => String(value)),
|
||||
});
|
||||
|
||||
// Tool indent toggle (insert after transcript-indent)
|
||||
const transcriptIndentIndex = items.findIndex((item) => item.id === "transcript-indent");
|
||||
items.splice(transcriptIndentIndex + 1, 0, {
|
||||
id: "tool-indent",
|
||||
label: "Tool indent",
|
||||
description: "Horizontal padding for tool and message blocks (0-4)",
|
||||
currentValue: String(config.toolIndent),
|
||||
values: INDENT_VALUES.map((value) => String(value)),
|
||||
});
|
||||
|
||||
// Autocomplete max visible toggle (insert after tool-indent)
|
||||
const toolIndentIndex = items.findIndex((item) => item.id === "tool-indent");
|
||||
items.splice(toolIndentIndex + 1, 0, {
|
||||
id: "autocomplete-max-visible",
|
||||
label: "Autocomplete max items",
|
||||
description: "Max visible items in autocomplete dropdown (3-20)",
|
||||
|
|
@ -782,6 +806,12 @@ export class SettingsSelectorComponent extends Container {
|
|||
case "editor-padding":
|
||||
callbacks.onEditorPaddingXChange(parseInt(newValue, 10));
|
||||
break;
|
||||
case "transcript-indent":
|
||||
callbacks.onTranscriptIndentChange(parseInt(newValue, 10));
|
||||
break;
|
||||
case "tool-indent":
|
||||
callbacks.onToolIndentChange(parseInt(newValue, 10));
|
||||
break;
|
||||
case "autocomplete-max-visible":
|
||||
callbacks.onAutocompleteMaxVisibleChange(parseInt(newValue, 10));
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ export class SkillInvocationMessageComponent extends Box {
|
|||
private skillBlock: ParsedSkillBlock;
|
||||
private markdownTheme: MarkdownTheme;
|
||||
|
||||
constructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme()) {
|
||||
super(1, 1, (t) => theme.bg("customMessageBg", t));
|
||||
constructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme(), toolIndent = 1) {
|
||||
super(toolIndent, 1, (t) => theme.bg("customMessageBg", t));
|
||||
this.skillBlock = skillBlock;
|
||||
this.markdownTheme = markdownTheme;
|
||||
this.updateDisplay();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { theme } from "../theme/theme.ts";
|
|||
export interface ToolExecutionOptions {
|
||||
showImages?: boolean;
|
||||
imageWidthCells?: number;
|
||||
toolIndent?: number;
|
||||
}
|
||||
|
||||
export class ToolExecutionComponent extends Container {
|
||||
|
|
@ -25,6 +26,7 @@ export class ToolExecutionComponent extends Container {
|
|||
private expanded = false;
|
||||
private showImages: boolean;
|
||||
private imageWidthCells: number;
|
||||
private toolIndent: number;
|
||||
private isPartial = true;
|
||||
private toolDefinition?: ToolDefinition<any, any>;
|
||||
private builtInToolDefinition?: ToolDefinition<any, any>;
|
||||
|
|
@ -57,6 +59,7 @@ export class ToolExecutionComponent extends Container {
|
|||
this.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];
|
||||
this.showImages = options.showImages ?? true;
|
||||
this.imageWidthCells = options.imageWidthCells ?? 60;
|
||||
this.toolIndent = options.toolIndent ?? 1;
|
||||
this.ui = ui;
|
||||
this.cwd = cwd;
|
||||
|
||||
|
|
@ -65,8 +68,8 @@ export class ToolExecutionComponent extends Container {
|
|||
// Always create all shell variants. contentBox is used for default renderer-based composition.
|
||||
// selfRenderContainer is used when the tool renders its own framing.
|
||||
// contentText is reserved for generic fallback rendering when no tool definition exists.
|
||||
this.contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.contentBox = new Box(this.toolIndent, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.contentText = new Text("", this.toolIndent, 1, (text: string) => theme.bg("toolPendingBg", text));
|
||||
this.selfRenderContainer = new Container();
|
||||
|
||||
if (this.hasRendererDefinition()) {
|
||||
|
|
@ -128,6 +131,7 @@ export class ToolExecutionComponent extends Container {
|
|||
isPartial: this.isPartial,
|
||||
expanded: this.expanded,
|
||||
showImages: this.showImages,
|
||||
toolIndent: this.toolIndent,
|
||||
isError: this.result?.isError ?? false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
|
|||
export class UserMessageComponent extends Container {
|
||||
private contentBox: Box;
|
||||
|
||||
constructor(text: string, markdownTheme: MarkdownTheme = getMarkdownTheme()) {
|
||||
constructor(text: string, markdownTheme: MarkdownTheme = getMarkdownTheme(), toolIndent = 1) {
|
||||
super();
|
||||
this.contentBox = new Box(1, 1, (content: string) => theme.bg("userMessageBg", content));
|
||||
this.contentBox = new Box(toolIndent, 1, (content: string) => theme.bg("userMessageBg", content));
|
||||
this.contentBox.addChild(
|
||||
new Markdown(
|
||||
text,
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ import {
|
|||
import type {
|
||||
AutocompleteItem,
|
||||
AutocompleteProvider,
|
||||
DefaultTextStyle,
|
||||
EditorComponent,
|
||||
Keybinding,
|
||||
KeyId,
|
||||
MarkdownOptions,
|
||||
MarkdownTheme,
|
||||
OverlayHandle,
|
||||
OverlayOptions,
|
||||
|
|
@ -598,13 +600,11 @@ export class InteractiveMode {
|
|||
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
||||
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
||||
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
||||
this.chatContainer.addChild(new Text(condensedText, 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(condensedText));
|
||||
} else {
|
||||
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.bold(theme.fg("accent", "What's New"))));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Markdown(this.changelogMarkdown.trim(), 1, 0, this.getMarkdownThemeWithSettings()),
|
||||
);
|
||||
this.chatContainer.addChild(this.createToolMarkdown(this.changelogMarkdown.trim()));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
|
|
@ -960,6 +960,38 @@ export class InteractiveMode {
|
|||
};
|
||||
}
|
||||
|
||||
private getTranscriptIndent(): number {
|
||||
return this.settingsManager.getTranscriptIndent();
|
||||
}
|
||||
|
||||
private getToolIndent(): number {
|
||||
return this.settingsManager.getToolIndent();
|
||||
}
|
||||
|
||||
private createToolText(text: string, paddingY = 0, customBgFn?: (text: string) => string): Text {
|
||||
return new Text(text, this.getToolIndent(), paddingY, customBgFn);
|
||||
}
|
||||
|
||||
private createToolMarkdown(
|
||||
text: string,
|
||||
paddingY = 0,
|
||||
defaultTextStyle?: DefaultTextStyle,
|
||||
options?: MarkdownOptions,
|
||||
): Markdown {
|
||||
return new Markdown(
|
||||
text,
|
||||
this.getToolIndent(),
|
||||
paddingY,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
defaultTextStyle,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
private createToolTruncatedText(text: string, paddingY = 0): TruncatedText {
|
||||
return new TruncatedText(text, this.getToolIndent(), paddingY);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Extension System
|
||||
// =========================================================================
|
||||
|
|
@ -2426,7 +2458,7 @@ export class InteractiveMode {
|
|||
*/
|
||||
private showExtensionError(extensionPath: string, error: string, stack?: string): void {
|
||||
const errorMsg = `Extension "${extensionPath}" error: ${error}`;
|
||||
const errorText = new Text(theme.fg("error", errorMsg), 1, 0);
|
||||
const errorText = this.createToolText(theme.fg("error", errorMsg));
|
||||
this.chatContainer.addChild(errorText);
|
||||
if (stack) {
|
||||
// Show stack trace in dim color, indented
|
||||
|
|
@ -2436,7 +2468,7 @@ export class InteractiveMode {
|
|||
.map((line) => theme.fg("dim", ` ${line.trim()}`))
|
||||
.join("\n");
|
||||
if (stackLines) {
|
||||
this.chatContainer.addChild(new Text(stackLines, 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(stackLines));
|
||||
}
|
||||
}
|
||||
this.ui.requestRender();
|
||||
|
|
@ -2793,6 +2825,7 @@ export class InteractiveMode {
|
|||
this.hideThinkingBlock,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.hiddenThinkingLabel,
|
||||
this.getTranscriptIndent(),
|
||||
);
|
||||
this.streamingMessage = event.message;
|
||||
this.chatContainer.addChild(this.streamingComponent);
|
||||
|
|
@ -2816,6 +2849,7 @@ export class InteractiveMode {
|
|||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
toolIndent: this.getToolIndent(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
|
|
@ -2885,6 +2919,7 @@ export class InteractiveMode {
|
|||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
toolIndent: this.getToolIndent(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(event.toolName),
|
||||
this.ui,
|
||||
|
|
@ -3000,7 +3035,7 @@ export class InteractiveMode {
|
|||
this.showError(event.errorMessage);
|
||||
} else {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", event.errorMessage), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.fg("error", event.errorMessage)));
|
||||
}
|
||||
}
|
||||
void this.flushCompactionQueue({ willRetry: event.willRetry });
|
||||
|
|
@ -3094,7 +3129,7 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
const spacer = new Spacer(1);
|
||||
const text = new Text(theme.fg("dim", message), 1, 0);
|
||||
const text = this.createToolText(theme.fg("dim", message));
|
||||
this.chatContainer.addChild(spacer);
|
||||
this.chatContainer.addChild(text);
|
||||
this.lastStatusSpacer = spacer;
|
||||
|
|
@ -3105,7 +3140,12 @@ export class InteractiveMode {
|
|||
private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void {
|
||||
switch (message.role) {
|
||||
case "bashExecution": {
|
||||
const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext);
|
||||
const component = new BashExecutionComponent(
|
||||
message.command,
|
||||
this.ui,
|
||||
message.excludeFromContext,
|
||||
this.getToolIndent(),
|
||||
);
|
||||
if (message.output) {
|
||||
component.appendOutput(message.output);
|
||||
}
|
||||
|
|
@ -3121,7 +3161,12 @@ export class InteractiveMode {
|
|||
case "custom": {
|
||||
if (message.display) {
|
||||
const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
|
||||
const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
|
||||
const component = new CustomMessageComponent(
|
||||
message,
|
||||
renderer,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
}
|
||||
|
|
@ -3129,14 +3174,22 @@ export class InteractiveMode {
|
|||
}
|
||||
case "compactionSummary": {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
|
||||
const component = new CompactionSummaryMessageComponent(
|
||||
message,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
break;
|
||||
}
|
||||
case "branchSummary": {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings());
|
||||
const component = new BranchSummaryMessageComponent(
|
||||
message,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
break;
|
||||
|
|
@ -3153,6 +3206,7 @@ export class InteractiveMode {
|
|||
const component = new SkillInvocationMessageComponent(
|
||||
skillBlock,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
|
|
@ -3162,11 +3216,16 @@ export class InteractiveMode {
|
|||
const userComponent = new UserMessageComponent(
|
||||
skillBlock.userMessage,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
}
|
||||
} else {
|
||||
const userComponent = new UserMessageComponent(textContent, this.getMarkdownThemeWithSettings());
|
||||
const userComponent = new UserMessageComponent(
|
||||
textContent,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.getToolIndent(),
|
||||
);
|
||||
this.chatContainer.addChild(userComponent);
|
||||
}
|
||||
if (options?.populateHistory) {
|
||||
|
|
@ -3181,6 +3240,7 @@ export class InteractiveMode {
|
|||
this.hideThinkingBlock,
|
||||
this.getMarkdownThemeWithSettings(),
|
||||
this.hiddenThinkingLabel,
|
||||
this.getTranscriptIndent(),
|
||||
);
|
||||
this.chatContainer.addChild(assistantComponent);
|
||||
break;
|
||||
|
|
@ -3227,6 +3287,7 @@ export class InteractiveMode {
|
|||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
toolIndent: this.getToolIndent(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
|
|
@ -3298,13 +3359,11 @@ export class InteractiveMode {
|
|||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
this.chatContainer.addChild(
|
||||
new Text(
|
||||
this.createToolText(
|
||||
theme.fg(
|
||||
"warning",
|
||||
`This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart pi.`,
|
||||
),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -3713,13 +3772,13 @@ export class InteractiveMode {
|
|||
|
||||
showError(errorMessage: string): void {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.fg("error", `Error: ${errorMessage}`)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
showWarning(warningMessage: string): void {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.fg("warning", `Warning: ${warningMessage}`)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -3736,18 +3795,18 @@ export class InteractiveMode {
|
|||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`, 1, 0),
|
||||
this.createToolText(`${theme.bold(theme.fg("warning", "Update Available"))}\n${updateInstruction}`),
|
||||
);
|
||||
if (note) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Markdown(note, 1, 0, this.getMarkdownThemeWithSettings(), {
|
||||
this.createToolMarkdown(note, 0, {
|
||||
color: (text) => theme.fg("muted", text),
|
||||
}),
|
||||
);
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
}
|
||||
this.chatContainer.addChild(new Text(changelogLine, 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(changelogLine));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -3760,10 +3819,8 @@ export class InteractiveMode {
|
|||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
this.chatContainer.addChild(
|
||||
new Text(
|
||||
this.createToolText(
|
||||
`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n${updateInstruction}\n${theme.fg("muted", "Packages:")}\n${packageLines}`,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
this.chatContainer.addChild(new DynamicBorder((text) => theme.fg("warning", text)));
|
||||
|
|
@ -3813,15 +3870,15 @@ export class InteractiveMode {
|
|||
this.pendingMessagesContainer.addChild(new Spacer(1));
|
||||
for (const message of steeringMessages) {
|
||||
const text = theme.fg("dim", `Steering: ${message}`);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
|
||||
this.pendingMessagesContainer.addChild(this.createToolTruncatedText(text));
|
||||
}
|
||||
for (const message of followUpMessages) {
|
||||
const text = theme.fg("dim", `Follow-up: ${message}`);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));
|
||||
this.pendingMessagesContainer.addChild(this.createToolTruncatedText(text));
|
||||
}
|
||||
const dequeueHint = this.getAppKeyDisplay("app.message.dequeue");
|
||||
const hintText = theme.fg("dim", `↳ ${dequeueHint} to edit all queued messages`);
|
||||
this.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
||||
this.pendingMessagesContainer.addChild(this.createToolTruncatedText(hintText));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3998,6 +4055,8 @@ export class InteractiveMode {
|
|||
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
|
||||
defaultProjectTrust: this.settingsManager.getDefaultProjectTrust(),
|
||||
editorPaddingX: this.settingsManager.getEditorPaddingX(),
|
||||
transcriptIndent: this.settingsManager.getTranscriptIndent(),
|
||||
toolIndent: this.settingsManager.getToolIndent(),
|
||||
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
|
||||
quietStartup: this.settingsManager.getQuietStartup(),
|
||||
clearOnShrink: this.settingsManager.getClearOnShrink(),
|
||||
|
|
@ -4063,12 +4122,6 @@ export class InteractiveMode {
|
|||
onHideThinkingBlockChange: (hidden) => {
|
||||
this.hideThinkingBlock = hidden;
|
||||
this.settingsManager.setHideThinkingBlock(hidden);
|
||||
for (const child of this.chatContainer.children) {
|
||||
if (child instanceof AssistantMessageComponent) {
|
||||
child.setHideThinkingBlock(hidden);
|
||||
}
|
||||
}
|
||||
this.chatContainer.clear();
|
||||
this.rebuildChatFromMessages();
|
||||
},
|
||||
onCollapseChangelogChange: (collapsed) => {
|
||||
|
|
@ -4100,6 +4153,14 @@ export class InteractiveMode {
|
|||
this.editor.setPaddingX(padding);
|
||||
}
|
||||
},
|
||||
onTranscriptIndentChange: (indent) => {
|
||||
this.settingsManager.setTranscriptIndent(indent);
|
||||
this.rebuildChatFromMessages();
|
||||
},
|
||||
onToolIndentChange: (indent) => {
|
||||
this.settingsManager.setToolIndent(indent);
|
||||
this.rebuildChatFromMessages();
|
||||
},
|
||||
onAutocompleteMaxVisibleChange: (maxVisible) => {
|
||||
this.settingsManager.setAutocompleteMaxVisible(maxVisible);
|
||||
this.defaultEditor.setAutocompleteMaxVisible(maxVisible);
|
||||
|
|
@ -5349,7 +5410,7 @@ export class InteractiveMode {
|
|||
const currentName = this.sessionManager.getSessionName();
|
||||
if (currentName) {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name: ${currentName}`), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.fg("dim", `Session name: ${currentName}`)));
|
||||
} else {
|
||||
this.showWarning("Usage: /name <name>");
|
||||
}
|
||||
|
|
@ -5363,7 +5424,7 @@ export class InteractiveMode {
|
|||
this.showWarning(`Session name was normalized from ${JSON.stringify(name)} to ${JSON.stringify(sessionName)}`);
|
||||
}
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${sessionName ?? name}`), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.fg("dim", `Session name set: ${sessionName ?? name}`)));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -5400,7 +5461,7 @@ export class InteractiveMode {
|
|||
}
|
||||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(info, 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(info));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -5418,9 +5479,9 @@ export class InteractiveMode {
|
|||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.bold(theme.fg("accent", "What's New"))));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings()));
|
||||
this.chatContainer.addChild(this.createToolMarkdown(changelogMarkdown, 1));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -5547,9 +5608,9 @@ export class InteractiveMode {
|
|||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Keyboard Shortcuts")), 1, 0));
|
||||
this.chatContainer.addChild(this.createToolText(theme.bold(theme.fg("accent", "Keyboard Shortcuts"))));
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.getMarkdownThemeWithSettings()));
|
||||
this.chatContainer.addChild(this.createToolMarkdown(hotkeys.trim(), 1));
|
||||
this.chatContainer.addChild(new DynamicBorder());
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
|
@ -5566,7 +5627,7 @@ export class InteractiveMode {
|
|||
return;
|
||||
}
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
|
||||
this.chatContainer.addChild(this.createToolText(`${theme.fg("accent", "✓ New session started")}`, 1));
|
||||
this.ui.requestRender();
|
||||
} catch (error: unknown) {
|
||||
await this.handleFatalRuntimeError("Failed to create session", error);
|
||||
|
|
@ -5601,20 +5662,20 @@ export class InteractiveMode {
|
|||
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(
|
||||
new Text(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1, 1),
|
||||
this.createToolText(`${theme.fg("accent", "✓ Debug log written")}\n${theme.fg("muted", debugLogPath)}`, 1),
|
||||
);
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private handleArminSaysHi(): void {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new ArminComponent(this.ui));
|
||||
this.chatContainer.addChild(new ArminComponent(this.ui, this.getToolIndent()));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private handleDementedDelves(): void {
|
||||
this.chatContainer.addChild(new Spacer(1));
|
||||
this.chatContainer.addChild(new EarendilAnnouncementComponent());
|
||||
this.chatContainer.addChild(new EarendilAnnouncementComponent(this.getToolIndent()));
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
|
|
@ -5646,7 +5707,7 @@ export class InteractiveMode {
|
|||
const result = eventResult.result;
|
||||
|
||||
// Create UI component for display
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.getToolIndent());
|
||||
if (this.session.isStreaming) {
|
||||
this.pendingMessagesContainer.addChild(this.bashComponent);
|
||||
this.pendingBashComponents.push(this.bashComponent);
|
||||
|
|
@ -5674,7 +5735,7 @@ export class InteractiveMode {
|
|||
|
||||
// Normal execution path (possibly with custom operations)
|
||||
const isDeferred = this.session.isStreaming;
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext);
|
||||
this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.getToolIndent());
|
||||
|
||||
if (isDeferred) {
|
||||
// Show in pending area when agent is streaming
|
||||
|
|
|
|||
|
|
@ -354,6 +354,31 @@ describe("SettingsManager", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should normalize and save transcript/tool indents", async () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
let manager = SettingsManager.create(projectDir, agentDir);
|
||||
expect(manager.getTranscriptIndent()).toBe(1);
|
||||
expect(manager.getToolIndent()).toBe(1);
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify({ transcriptIndent: 0, toolIndent: 4 }));
|
||||
manager = SettingsManager.create(projectDir, agentDir);
|
||||
expect(manager.getTranscriptIndent()).toBe(0);
|
||||
expect(manager.getToolIndent()).toBe(4);
|
||||
|
||||
writeFileSync(settingsPath, JSON.stringify({ transcriptIndent: 5, toolIndent: "2" }));
|
||||
manager = SettingsManager.create(projectDir, agentDir);
|
||||
expect(manager.getTranscriptIndent()).toBe(1);
|
||||
expect(manager.getToolIndent()).toBe(1);
|
||||
|
||||
manager.setTranscriptIndent(2);
|
||||
manager.setToolIndent(3);
|
||||
await manager.flush();
|
||||
|
||||
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||
expect(savedSettings.transcriptIndent).toBe(2);
|
||||
expect(savedSettings.toolIndent).toBe(3);
|
||||
});
|
||||
|
||||
describe("shellCommandPrefix", () => {
|
||||
it("should load shellCommandPrefix from settings", () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
|
|
|
|||
|
|
@ -67,6 +67,26 @@ describe("ToolExecutionComponent parity", () => {
|
|||
expect(rendered).toContain("custom result");
|
||||
});
|
||||
|
||||
test("applies tool indentation to default tool shells and render contexts", () => {
|
||||
const toolDefinition: ToolDefinition = {
|
||||
...createBaseToolDefinition(),
|
||||
renderCall: (_args, _theme, context) => new Text(`indent:${context.toolIndent}`, 0, 0),
|
||||
};
|
||||
|
||||
const component = new ToolExecutionComponent(
|
||||
"custom_tool",
|
||||
"tool-indent",
|
||||
{},
|
||||
{ toolIndent: 4 },
|
||||
toolDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
|
||||
const contentLine = stripAnsi(component.render(40).find((line) => line.includes("indent:")) ?? "").trimEnd();
|
||||
expect(contentLine).toBe(" indent:4");
|
||||
});
|
||||
|
||||
test("self-rendered empty tool rows take no layout space", () => {
|
||||
const toolDefinition: ToolDefinition = {
|
||||
...createBaseToolDefinition(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue