feat(coding-agent): added git isolation and keybinding utilities for autoresearch
- Added git branch isolation for autoresearch sessions with automatic branch creation, reuse, and worktree safety checks. - Added scope definition sections (Files in Scope, Off Limits, Constraints) to autoresearch template for explicit session boundaries. - Added keybinding matcher utilities for consistent escape/cancel key handling across interactive components. - Added ASI metadata validation (hypothesis and rollback context) in log_experiment tool for experiment tracking. - Refactored keybinding logic across 13 components to use centralized matcher functions instead of inline key checks.
This commit is contained in:
@@ -50,6 +50,7 @@ import { discoverAgents } from "../../task/discovery";
|
||||
import type { AgentDefinition, AgentSource } from "../../task/types";
|
||||
import { shortenPath } from "../../tools/render-utils";
|
||||
import { theme } from "../theme/theme";
|
||||
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
type SourceTabId = "all" | AgentSource;
|
||||
@@ -993,7 +994,7 @@ export class AgentDashboard extends Container {
|
||||
}
|
||||
|
||||
if (this.#createSpec) {
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
if (matchesAppInterrupt(data)) {
|
||||
this.#clearCreateFlow();
|
||||
this.#buildLayout();
|
||||
return;
|
||||
@@ -1017,7 +1018,7 @@ export class AgentDashboard extends Container {
|
||||
}
|
||||
|
||||
if (this.#createInput || this.#createGenerating) {
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
if (matchesAppInterrupt(data)) {
|
||||
if (!this.#createGenerating) {
|
||||
this.#clearCreateFlow();
|
||||
this.#buildLayout();
|
||||
@@ -1037,7 +1038,7 @@ export class AgentDashboard extends Container {
|
||||
}
|
||||
|
||||
if (this.#editInput) {
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
if (matchesAppInterrupt(data)) {
|
||||
this.#cancelModelEdit();
|
||||
return;
|
||||
}
|
||||
@@ -1048,7 +1049,7 @@ export class AgentDashboard extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
if (matchesAppInterrupt(data)) {
|
||||
if (this.#searchQuery.length > 0) {
|
||||
this.#searchQuery = "";
|
||||
this.#applyFilters();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { Settings } from "../../../config/settings";
|
||||
import { DynamicBorder } from "../../../modes/components/dynamic-border";
|
||||
import { theme } from "../../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../../modes/utils/keybinding-matchers";
|
||||
import { ExtensionList } from "./extension-list";
|
||||
import { InspectorPanel } from "./inspector-panel";
|
||||
import { applyFilter, createInitialState, filterByProvider, refreshState, toggleProvider } from "./state-manager";
|
||||
@@ -251,7 +252,7 @@ export class ExtensionDashboard extends Container {
|
||||
}
|
||||
|
||||
// Escape - clear search first, then close
|
||||
if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
if (matchesAppInterrupt(data)) {
|
||||
if (this.#state.searchQuery.length > 0) {
|
||||
this.#state.searchQuery = "";
|
||||
this.#state.searchFiltered = this.#state.tabFiltered;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
visibleWidth,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import type { HistoryEntry, HistoryStorage } from "../../session/history-storage";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -137,7 +138,7 @@ export class HistorySearchComponent extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
if (matchesAppInterrupt(keyData)) {
|
||||
this.#onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { Container, Editor, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
||||
import { getEditorTheme, theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -67,7 +68,7 @@ export class HookEditorComponent extends Container {
|
||||
}
|
||||
|
||||
// Escape to cancel
|
||||
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
if (matchesAppInterrupt(keyData)) {
|
||||
this.#onCancelCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { Container, Input, Markdown, matchesKey, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import { CountdownTimer } from "./countdown-timer";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -65,7 +66,7 @@ export class HookInputComponent extends Container {
|
||||
this.#countdown?.reset();
|
||||
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
||||
this.#onSubmitCallback(this.#input.getValue());
|
||||
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
} else if (matchesAppInterrupt(keyData)) {
|
||||
this.#onCancelCallback();
|
||||
} else {
|
||||
this.#input.handleInput(keyData);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
visibleWidth,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
||||
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
||||
import { CountdownTimer } from "./countdown-timer";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
@@ -173,7 +174,7 @@ export class HookSelectorComponent extends Container {
|
||||
this.#onLeftCallback?.();
|
||||
} else if (matchesKey(keyData, "right")) {
|
||||
this.#onRightCallback?.();
|
||||
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
||||
} else if (matchesSelectCancel(keyData)) {
|
||||
this.#onCancelCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { analyzeAuthError, discoverOAuthEndpoints } from "../../mcp/oauth-discov
|
||||
import type { MCPHttpServerConfig, MCPServerConfig, MCPSseServerConfig, MCPStdioServerConfig } from "../../mcp/types";
|
||||
import { shortenPath } from "../../tools/render-utils";
|
||||
import { theme } from "../theme/theme";
|
||||
import { matchesAppInterrupt } from "../utils/keybinding-matchers";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
type TransportType = "stdio" | "http" | "sse";
|
||||
@@ -452,7 +453,7 @@ export class MCPAddWizard extends Container {
|
||||
}
|
||||
|
||||
// Handle Escape (always handled by wizard)
|
||||
if (matchesKey(keyData, "escape")) {
|
||||
if (matchesAppInterrupt(keyData)) {
|
||||
if (this.#currentStep === "name") {
|
||||
// Cancel wizard
|
||||
this.#onCancelCallback();
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
|
||||
import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
import {
|
||||
Container,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
Spacer,
|
||||
type Tab,
|
||||
TabBar,
|
||||
Text,
|
||||
type TUI,
|
||||
visibleWidth,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
|
||||
import { resolveModelRoleValue } from "../../config/model-resolver";
|
||||
import type { Settings } from "../../config/settings";
|
||||
@@ -647,7 +658,7 @@ export class ModelSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
// Escape or Ctrl+C - close selector
|
||||
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
||||
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
||||
this.#onCancelCallback();
|
||||
return;
|
||||
}
|
||||
@@ -698,7 +709,7 @@ export class ModelSelectorComponent extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
||||
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
||||
if (this.#menuStep === "thinking" && this.#menuSelectedRole !== null) {
|
||||
this.#menuStep = "role";
|
||||
const roleIndex = MENU_ROLE_ACTIONS.findIndex(action => action.role === this.#menuSelectedRole);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getOAuthProviders, type OAuthProviderInfo } from "@oh-my-pi/pi-ai";
|
||||
import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
||||
import type { AuthStorage } from "../../session/auth-storage";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
/**
|
||||
@@ -202,7 +203,7 @@ export class OAuthSelectorComponent extends Container {
|
||||
}
|
||||
}
|
||||
// Escape or Ctrl+C
|
||||
else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
|
||||
else if (matchesSelectCancel(keyData)) {
|
||||
this.stopValidation();
|
||||
this.#onCancelCallback();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
visibleWidth,
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import type { SessionInfo } from "../../session/session-manager";
|
||||
import { fuzzyFilter } from "../../utils/fuzzy";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
@@ -219,7 +220,7 @@ class SessionList implements Component {
|
||||
return;
|
||||
}
|
||||
// Escape - cancel
|
||||
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
if (matchesAppInterrupt(keyData)) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
} from "../../config/settings-schema";
|
||||
import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
|
||||
import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import { getTabBarTheme } from "../shared";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
import { PluginSettingsComponent } from "./plugin-settings";
|
||||
@@ -521,7 +522,7 @@ export class SettingsSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
// Escape at top level cancels
|
||||
if ((matchesKey(data, "escape") || matchesKey(data, "esc")) && !this.#currentSubmenu) {
|
||||
if (matchesAppInterrupt(data) && !this.#currentSubmenu) {
|
||||
this.callbacks.onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { Container, matchesKey, padding } from "@oh-my-pi/pi-tui";
|
||||
import type { StatusLineSegmentId } from "../../config/settings-schema";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import { ALL_SEGMENT_IDS } from "./status-line/segments";
|
||||
|
||||
// Segment display names and short descriptions
|
||||
@@ -239,7 +240,7 @@ export class StatusLineSegmentEditorComponent extends Container {
|
||||
const left = this.#getSegmentsForColumn("left").map(s => s.id);
|
||||
const right = this.#getSegmentsForColumn("right").map(s => s.id);
|
||||
this.callbacks.onSave(left, right);
|
||||
} else if (matchesKey(data, "escape") || matchesKey(data, "esc")) {
|
||||
} else if (matchesAppInterrupt(data)) {
|
||||
this.callbacks.onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "@oh-my-pi/pi-tui";
|
||||
import type { TreeFilterMode } from "../../config/settings-schema";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
|
||||
import type { SessionTreeNode } from "../../session/session-manager";
|
||||
import { shortenPath } from "../../tools/render-utils";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
@@ -702,7 +703,7 @@ class TreeList implements Component {
|
||||
if (selected && this.onSelect) {
|
||||
this.onSelect(selected.node.entry.id);
|
||||
}
|
||||
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
} else if (matchesAppInterrupt(keyData)) {
|
||||
if (this.#searchQuery) {
|
||||
this.#searchQuery = "";
|
||||
this.#applyFilter();
|
||||
@@ -807,7 +808,7 @@ class LabelInput implements Component {
|
||||
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
||||
const value = this.#input.getValue().trim();
|
||||
this.onSubmit?.(this.entryId, value || undefined);
|
||||
} else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
} else if (matchesAppInterrupt(keyData)) {
|
||||
this.onCancel?.();
|
||||
} else {
|
||||
this.#input.handleInput(keyData);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import { matchesSelectCancel } from "../../modes/utils/keybinding-matchers";
|
||||
import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
interface UserMessageItem {
|
||||
@@ -91,14 +92,8 @@ class UserMessageList implements Component {
|
||||
this.onSelect(selected.id);
|
||||
}
|
||||
}
|
||||
// Escape - cancel
|
||||
else if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
}
|
||||
// Ctrl+C - cancel
|
||||
else if (matchesKey(keyData, "ctrl+c")) {
|
||||
// Escape / cancel
|
||||
else if (matchesSelectCancel(keyData)) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user