Skip to content

Mode interactif TUI

源码版本v0.73.1

InteractiveMode est le mode d'exécution par défaut de pi — un TUI plein écran qui rend la conversation, l'exécution des outils, le streaming, la barre d'état, l'éditeur, le sélecteur de skills, le sélecteur de modèle et des dizaines d'autres composants dans le terminal. Ce fichier fait 5493 lignes, le plus long fichier unique de tout pi-mono, parce qu'il concentre toute la logique d'interaction UI (key bindings, dispatch de commandes, rendu d'événements, intégration UI des extensions, auto-compaction, auto-retry, collage d'image, drag-and-drop de fichiers) dans une seule classe. Cet article ne couvre que l'entrée et le tronc de rendu ; le traitement détaillé des commandes est dans les méthodes handle*Command respectives.

Responsabilités

  1. Assemblage UI : dans init(), on attache les conteneurs header / chatContainer / pendingMessagesContainer / statusContainer / editorContainer / footer au TUI, on met le focus sur editor, puis on lance ui.start(). Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650.
  2. Dispatch des soumissions de l'éditeur : defaultEditor.onSubmit est l'entrée principale de l'UI, il reconnaît les slash commandes / (/settings, /model, /export, /import, /fork, /new, /compact…), et les textes hors commande partent sur session.prompt. Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2625.
  3. Rendu des AgentSessionEvent : handleEvent fait un switch sur une dizaine de types d'événements (agent_start, queue_update, assistant_message, tool_call, tool_result, compaction, error…), met à jour les composants UI en sync. Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2980.
  4. Key bindings : setupKeyHandlers enregistre Ctrl+C, Ctrl+D, Ctrl+Z, Alt+Enter (followUp), Ctrl+P (cycle model), Ctrl+T (cycle thinking), etc. Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:2352-2417.
  5. Mode bash : le préfixe ! déclenche handleBashCommand, le résultat est rendu via BashExecutionComponent ; le préfixe !! met excludeFromContext: true. Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:5364-5450.
  6. Intégration UI des extensions : rebindSession rappelle session.bindExtensions en fournissant uiContext, les extensions peuvent injecter widget, dialog, status. Voir packages/coding-agent/src/modes/interactive/interactive-mode.ts:1812-1870.

Motifs de conception

Pourquoi ne pas découper les 5493 lignes ? Parce que cette classe est le centre de la machine à états — des dizaines de champs (streamingComponent, pendingTools, autoCompactionLoader, retryLoader, retryCountdown, pendingBashComponents, skillCommands) s'influencent mutuellement dans le même flux d'événements ; éclater en plusieurs petites classes éparpillerait l'état et la synchro inter-classes deviendrait plus dure. Le choix de pi est de concentrer tout l'état UI dans une seule classe et de tout traiter via le gros switch de handleEvent : le code est long mais les transitions d'état restent lisibles.

isExpandable, cette petite fonction utilitaire (packages/coding-agent/src/modes/interactive/interactive-mode.ts:142-144), est un check duck typing de la couche de rendu : tout composant qui a une méthode setExpanded peut être replié/déplié. Cela permet à ExpandableText, au header et aux sorties d'outil de partager une même logique de repli sans hériter d'une même classe abstraite.

La distinction defaultEditor.onSubmit vs this.editor.onSubmit : defaultEditor est l'instance fixe, this.editor est l'éditeur actuellement actif (potentiellement un éditeur custom injecté par une extension). setupEditorSubmitHandler ne monte le handler que sur defaultEditor, mais le handler lit this.editor pour obtenir le texte courant — ainsi un éditeur custom qui prend la main réutilise le même dispatch de commandes.

Fichiers clés

defaultEditor.onSubmit est le cœur du dispatch de commandes, avec une longue série de if (text === "/xxx") :

typescript
// packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2470
this.defaultEditor.onSubmit = async (text: string) => {
	text = text.trim();
	if (!text) return;

	// Handle commands
	if (text === "/settings") {
		this.showSettingsSelector();
		this.editor.setText("");
		return;
	}
	if (text === "/scoped-models") {
		this.editor.setText("");
		await this.showModelsSelector();
		return;
	}
	if (text === "/model" || text.startsWith("/model ")) {
		const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
		this.editor.setText("");
		await this.handleModelCommand(searchTerm);
		return;
	}
	// ... des dizaines d'autres branches de commande ...

Alt+Enter met en file d'attente un followUp pendant le streaming, hors streaming dégrade en submit ordinaire :

typescript
// packages/coding-agent/src/modes/interactive/interactive-mode.ts:3352-3363
if (this.session.isStreaming) {
	this.editor.addToHistory?.(text);
	this.editor.setText("");
	await this.session.prompt(text, { streamingBehavior: "followUp" });
	this.updatePendingMessagesDisplay();
	this.ui.requestRender();
}
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
else if (this.editor.onSubmit) {
	this.editor.setText("");
	this.editor.onSubmit(text);
}

Flux de données

Flux bidirectionnel entre entrée UI et rendu d'événements :

Limites et cas d'échec

Synthèse

InteractiveMode est le centre TUI de pi, 5493 lignes qui concentrent tout l'état UI et le dispatch de commandes. defaultEditor.onSubmit est l'entrée, handleEvent est le tronc de rendu. Le runtime qui l'assemble est dans switch/fork/import de session, la source d'événements AgentSession est dans couche d'orchestration AgentSession, et les modes non interactifs sont dans modes print et rpc.