Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 14 additions & 110 deletions apps/cli/src/ui/interactive-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ import {
openBrowser,
openStepSession,
ProjectTrustStore,
type PromptOptions,
parseChangelog,
parseGitUrl,
parseSkillBlock,
Expand Down Expand Up @@ -170,7 +169,7 @@ import {
registerTaskUpdateCall,
resetTaskUpdateTranscript,
} from "./runtime/task-update-transcript.ts";
import { FooterComponent, formatTokens, warningListHint } from "./view/chrome/footer.ts";
import { FooterComponent, formatTokens } from "./view/chrome/footer.ts";
import {
BranchSummaryStatusIndicator,
IdleStatus,
Expand Down Expand Up @@ -201,7 +200,6 @@ import { ThinkingSelectorComponent } from "./view/dialogs/thinking-selector.ts";
import { TreeSelectorComponent } from "./view/dialogs/tree-selector.ts";
import { TrustSelectorComponent } from "./view/dialogs/trust-selector.ts";
import { UserMessageSelectorComponent } from "./view/dialogs/user-message-selector.ts";
import { type SessionWarning, WarningListComponent, warningListCapacity } from "./view/dialogs/warning-list.ts";
import { CustomEntryComponent } from "./view/editor/custom-entry.ts";
import { STEP_EDITOR_PLACEHOLDER, StepEditor } from "./view/editor/step-editor.ts";
import { AssistantMessageComponent } from "./view/transcript/assistant-message.ts";
Expand Down Expand Up @@ -373,9 +371,6 @@ type LoginProviderCompletionOption = {

const AUTH_TYPE_ORDER = { oauth: 0, api_key: 1 } satisfies Record<AuthSelectorProvider["authType"], number>;

/** Warnings one host keeps; older ones fall off so a long session cannot grow this list. */
const WARNING_LOG_LIMIT = 50;

function createFuzzyAutocompleteItems<T>(
items: T[],
prefix: string,
Expand Down Expand Up @@ -630,14 +625,6 @@ export class InteractiveMode {
// Shutdown state
private shutdownRequested = false;

// Warnings the Step footer counts. They never enter the transcript or the
// session file: the badge and its list are the only place they surface.
private warningLog: Array<SessionWarning & { sessionId: string }> = [];
// User inputs the kernel is still accepting (a command's handler, the input
// hooks before a turn). Warnings they publish are replies and stay inline;
// see promptUserInput and showExtensionNotify.
private acceptingUserInput = 0;

// Extension UI state
private extensionDialogsBlocked = false;
private extensionSelector: ExtensionSelectorComponent | undefined = undefined;
Expand Down Expand Up @@ -796,8 +783,6 @@ export class InteractiveMode {
options.tuiStyle === "step" && this.session.extensionRunner.getCommand("permissions")
? keyText("app.thinking.cycle")
: undefined,
warningCount: () => this.warningsForCurrentSession().length,
warningListKey: () => warningListHint(this.keybindings.getKeys("app.warnings.open")),
});
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
this.footerContainer = new Container();
Expand Down Expand Up @@ -1369,8 +1354,8 @@ export class InteractiveMode {
}

if (mcpImportNotice) this.showNotice(mcpImportNotice);
if (mcpImportError) this.recordWarning(mcpImportError);
if (themePromptError) this.recordWarning(themePromptError);
if (mcpImportError) this.showWarning(mcpImportError);
if (themePromptError) this.showWarning(themePromptError);

if (!this.options.disableBackgroundServices) {
const controller = new AbortController();
Expand Down Expand Up @@ -1399,7 +1384,7 @@ export class InteractiveMode {
// Check tmux keyboard setup asynchronously
this.checkTmuxKeyboardSetup().then((warning) => {
if (warning) {
this.recordWarning(warning);
this.showWarning(warning);
}
});

Expand All @@ -1417,14 +1402,14 @@ export class InteractiveMode {
if (diagnostic.type === "error") {
this.showError(diagnostic.message);
} else if (diagnostic.type === "warning") {
this.recordWarning(diagnostic.message);
this.showWarning(diagnostic.message);
} else {
this.showStatus(diagnostic.message);
}
}

if (migratedProviders && migratedProviders.length > 0) {
this.recordWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
this.showWarning(`Migrated credentials to auth.json: ${migratedProviders.join(", ")}`);
}

const modelsJsonError = this.session.modelRuntime.getError();
Expand All @@ -1433,16 +1418,15 @@ export class InteractiveMode {
}

if (modelFallbackMessage) {
this.recordWarning(modelFallbackMessage);
this.showWarning(modelFallbackMessage);
}

void this.maybeWarnAboutAnthropicSubscriptionAuth();

// Process initial messages. They are the user's input from the command
// line, so their replies answer inline like anything typed in the editor.
// Process initial messages
if (initialMessage) {
try {
await this.promptUserInput(initialMessage, { images: initialImages });
await this.session.prompt(initialMessage, { images: initialImages });
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
this.showError(errorMessage);
Expand All @@ -1452,7 +1436,7 @@ export class InteractiveMode {
if (initialMessages) {
for (const message of initialMessages) {
try {
await this.promptUserInput(message);
await this.session.prompt(message);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
this.showError(errorMessage);
Expand All @@ -1469,7 +1453,7 @@ export class InteractiveMode {
const { text, images } = await resolvePastedImages(this.pastedImages, userInput, {
autoResizeImages: this.settingsManager.getImageAutoResize(),
});
await this.promptUserInput(text, { images: images.length ? images : undefined });
await this.session.prompt(text, { images: images.length ? images : undefined });
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
this.showError(errorMessage);
Expand Down Expand Up @@ -3287,11 +3271,7 @@ export class InteractiveMode {
if (type === "error") {
this.showError(message);
} else if (type === "warning") {
// A warning raised while the kernel accepts the user's input is its answer
// ("Usage: ...", a refusal), so it belongs where the user is looking.
// Anything later arrived on its own and only counts in the badge.
if (this.acceptingUserInput > 0) this.showWarning(message);
else this.recordWarning(message);
this.showWarning(message);
} else if (options?.echoesInput) {
this.showInputEcho(message);
} else {
Expand Down Expand Up @@ -4151,7 +4131,7 @@ export class InteractiveMode {
if (this.isExtensionCommand(text)) {
this.editor.addToHistory?.(text);
this.editor.setText("");
await this.promptUserInput(text);
await this.session.prompt(text);
} else {
const { text: message, images } = await resolvePastedImages(this.pastedImages, text, {
autoResizeImages: this.settingsManager.getImageAutoResize(),
Expand All @@ -4169,7 +4149,7 @@ export class InteractiveMode {
const { text: message, images } = await resolvePastedImages(this.pastedImages, text, {
autoResizeImages: this.settingsManager.getImageAutoResize(),
});
await this.promptUserInput(message, {
await this.session.prompt(message, {
streamingBehavior: "followUp",
images: images.length ? images : undefined,
});
Expand Down Expand Up @@ -4329,80 +4309,6 @@ export class InteractiveMode {
this.redraw.requestRender();
}

/**
* Report a warning nobody asked for: startup diagnostics, a background
* failure. Step counts it in the footer badge so it does not push the
* conversation up the screen; the Pi presentation keeps its transcript row.
* A warning that answers the user's own action uses showWarning instead.
*/
recordWarning(warningMessage: string): void {
if (this.presentation !== "step") {
this.showWarning(warningMessage);
return;
}
this.warningLog.push({ message: warningMessage, at: Date.now(), sessionId: this.sessionManager.getSessionId() });
if (this.warningLog.length > WARNING_LOG_LIMIT) {
this.warningLog.splice(0, this.warningLog.length - WARNING_LOG_LIMIT);
}
this.redraw.requestRender();
}

async promptUserInput(text: string, options?: PromptOptions): Promise<void> {
// The kernel reports acceptance once a command's handler returned, or once
// a message passed its input hooks and is about to run. Until then every
// warning answers this input; the turn that follows is ambient.
this.acceptingUserInput += 1;
let accepting = true;
const accepted = () => {
if (!accepting) return;
accepting = false;
this.acceptingUserInput -= 1;
};
try {
await this.session.prompt(text, {
...options,
preflightResult: (success) => {
accepted();
options?.preflightResult?.(success);
},
});
} finally {
accepted();
}
}

/** The warnings the footer badge counts and the list dialog shows. */
private warningsForCurrentSession(): SessionWarning[] {
const sessionId = this.sessionManager.getSessionId();
return this.warningLog
.filter((entry) => entry.sessionId === sessionId)
.map(({ message, at }) => ({ message, at }));
}

showWarningList(): void {
if (this.warningsForCurrentSession().length === 0) {
this.showStatus("No warnings");
return;
}
const sessionId = this.sessionManager.getSessionId();
this.showSelector((done) => {
const list = new WarningListComponent({
getWarnings: () => this.warningsForCurrentSession(),
maxVisible: warningListCapacity(this.ui.terminal.rows),
onClose: () => {
done();
this.redraw.requestRender();
},
onClear: () => {
this.warningLog = this.warningLog.filter((entry) => entry.sessionId !== sessionId);
done();
this.redraw.requestRender();
},
});
return { component: list, focus: list };
});
}

/**
* A thing that succeeded and is worth a line in the transcript.
*
Expand Down Expand Up @@ -6699,7 +6605,6 @@ export class InteractiveMode {
const cycleModelForward = this.getAppKeyDisplay("app.model.cycleForward");
const selectModel = this.getAppKeyDisplay("app.model.select");
const expandTools = this.getAppKeyDisplay("app.tools.expand");
const openWarnings = this.getAppKeyDisplay("app.warnings.open");
const toggleThinking = this.getAppKeyDisplay("app.thinking.toggle");
const externalEditor = this.getAppKeyDisplay("app.editor.external");
const cycleModelBackward = this.getAppKeyDisplay("app.model.cycleBackward");
Expand Down Expand Up @@ -6746,7 +6651,6 @@ export class InteractiveMode {
| \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
| \`${selectModel}\` | Open model selector |
| \`${expandTools}\` | Toggle tool output expansion |
| \`${openWarnings}\` | Show the warnings collected in this session |
| \`${toggleThinking}\` | Toggle thinking block visibility |
| \`${externalEditor}\` | Edit message in external editor |
| \`${copyMessage}\` | Copy last assistant message |
Expand Down
9 changes: 0 additions & 9 deletions apps/cli/src/ui/runtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import type {
ExtensionUIDialogOptions,
InteractiveModeOptions,
MarkdownTransformer,
PromptOptions,
SessionEntry,
} from "@step-harness/coding-agent";
import type { Container, EditorComponent, MarkdownTheme, TUI, TuiAltScreen, TuiMainScreen } from "@step-harness/pi-tui";
Expand Down Expand Up @@ -188,14 +187,6 @@ export interface RuntimeContext {
showStatus(message: string): void;
showTrustSelector(): void;
showWarning(warningMessage: string): void;
/** Open the session's collected warnings behind the footer badge. */
showWarningList(): void;
/**
* Submit what the user typed. Until the kernel has accepted it (a command's
* handler returned, a message passed its input hooks), warnings it publishes
* are its reply and stay in the transcript instead of the badge.
*/
promptUserInput(text: string, options?: PromptOptions): Promise<void>;
toggleThinkingBlockVisibility(): void;
toggleToolOutputExpansion(): void;
updatePendingMessagesDisplay(): void;
Expand Down
17 changes: 3 additions & 14 deletions apps/cli/src/ui/runtime/input-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,7 @@ export function wireKeyHandlers(ctx: RuntimeContext): void {
// Shift+Tab for its permission preset cycle. The command is handled by
// the Step extension; ordinary pi sessions retain thinking-level cycling.
if (ctx.options.tuiStyle === "step" && ctx.session.extensionRunner.getCommand("permissions")) {
// The key press is the user's input: a refusal ("Full access was not
// enabled.") answers it inline instead of only counting in the badge.
void ctx.promptUserInput("/permissions --cycle");
void ctx.session.prompt("/permissions --cycle");
return;
}
ctx.cycleThinkingLevel();
Expand Down Expand Up @@ -217,7 +215,6 @@ export function wireKeyHandlers(ctx: RuntimeContext): void {
return steering.length > 0 || followUp.length > 0;
};
ctx.defaultEditor.onAction("app.message.dequeue", () => ctx.handleDequeue());
ctx.defaultEditor.onAction("app.warnings.open", () => ctx.showWarningList());
ctx.defaultEditor.onAction("app.session.new", () => ctx.handleClearCommand());
ctx.defaultEditor.onAction("app.session.tree", () => ctx.showTreeSelector());
ctx.defaultEditor.onAction("app.session.fork", () => ctx.showUserMessageSelector());
Expand Down Expand Up @@ -335,11 +332,6 @@ export function wireSubmitHandler(ctx: RuntimeContext): void {
ctx.editor.setText("");
return;
}
if (text === "/warnings") {
ctx.editor.setText("");
ctx.showWarningList();
return;
}
if (text === "/fork") {
ctx.showUserMessageSelector();
ctx.editor.setText("");
Expand Down Expand Up @@ -442,7 +434,7 @@ export function wireSubmitHandler(ctx: RuntimeContext): void {
if (ctx.isExtensionCommand(text)) {
ctx.editor.addToHistory?.(text);
ctx.editor.setText("");
await ctx.promptUserInput(text);
await ctx.session.prompt(text);
} else {
const { text: message, images } = await resolvePastedImages(ctx.pastedImages, text, {
autoResizeImages: ctx.settingsManager.getImageAutoResize(),
Expand All @@ -460,10 +452,7 @@ export function wireSubmitHandler(ctx: RuntimeContext): void {
const { text: message, images } = await resolvePastedImages(ctx.pastedImages, text, {
autoResizeImages: ctx.settingsManager.getImageAutoResize(),
});
await ctx.promptUserInput(message, {
streamingBehavior: "steer",
images: images.length ? images : undefined,
});
await ctx.session.prompt(message, { streamingBehavior: "steer", images: images.length ? images : undefined });
ctx.updatePendingMessagesDisplay();
ctx.redraw.requestRender();
return;
Expand Down
Loading
Loading