@oh-my-pi/pi-coding-agent 3.3.1337 → 3.5.1337

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Types for the Extension Control Center dashboard.
3
+ */
4
+
5
+ import type { SourceMeta } from "../../../../capability/types";
6
+
7
+ /**
8
+ * Extension kinds matching capability types.
9
+ */
10
+ export type ExtensionKind =
11
+ | "skill"
12
+ | "rule"
13
+ | "tool"
14
+ | "mcp"
15
+ | "prompt"
16
+ | "instruction"
17
+ | "context-file"
18
+ | "hook"
19
+ | "slash-command";
20
+
21
+ /**
22
+ * Extension state (active, disabled, or shadowed).
23
+ */
24
+ export type ExtensionState = "active" | "disabled" | "shadowed";
25
+
26
+ /**
27
+ * Reason why an extension is disabled.
28
+ */
29
+ export type DisabledReason = "provider-disabled" | "item-disabled" | "shadowed";
30
+
31
+ /**
32
+ * Unified extension representation for the dashboard.
33
+ * Normalizes all capability types into a common shape.
34
+ */
35
+ export interface Extension {
36
+ /** Unique ID: `${kind}:${name}` */
37
+ id: string;
38
+ /** Extension kind */
39
+ kind: ExtensionKind;
40
+ /** Extension name */
41
+ name: string;
42
+ /** Display name (may differ from name) */
43
+ displayName: string;
44
+ /** Description if available */
45
+ description?: string;
46
+ /** Trigger pattern (slash command, glob, regex) */
47
+ trigger?: string;
48
+ /** Absolute path to source file */
49
+ path: string;
50
+ /** Source metadata */
51
+ source: {
52
+ provider: string;
53
+ providerName: string;
54
+ level: "user" | "project" | "native";
55
+ };
56
+ /** Current state */
57
+ state: ExtensionState;
58
+ /** Reason for disabled state */
59
+ disabledReason?: DisabledReason;
60
+ /** If shadowed, what shadows it */
61
+ shadowedBy?: string;
62
+ /** Raw item data for inspector */
63
+ raw: unknown;
64
+ }
65
+
66
+ /**
67
+ * Tree node types for sidebar hierarchy.
68
+ */
69
+ export type TreeNodeType = "provider" | "kind" | "item";
70
+
71
+ /**
72
+ * Sidebar tree node.
73
+ */
74
+ export interface TreeNode {
75
+ /** Unique ID */
76
+ id: string;
77
+ /** Display label */
78
+ label: string;
79
+ /** Node type (provider can be toggled, kind groups items) */
80
+ type: TreeNodeType;
81
+ /** Whether this node/provider is enabled */
82
+ enabled: boolean;
83
+ /** Whether collapsed */
84
+ collapsed: boolean;
85
+ /** Child nodes */
86
+ children: TreeNode[];
87
+ /** Extension count (for display) */
88
+ count?: number;
89
+ }
90
+
91
+ /**
92
+ * Flattened tree item for navigation.
93
+ */
94
+ export interface FlatTreeItem {
95
+ node: TreeNode;
96
+ depth: number;
97
+ index: number;
98
+ }
99
+
100
+ /**
101
+ * Focus region in the tabbed dashboard.
102
+ */
103
+ export type FocusRegion = "tabs" | "list";
104
+
105
+ /**
106
+ * Provider tab representation.
107
+ */
108
+ export interface ProviderTab {
109
+ /** Provider ID (or "all" for the ALL tab) */
110
+ id: string;
111
+ /** Display label */
112
+ label: string;
113
+ /** Whether provider is enabled (always true for "all") */
114
+ enabled: boolean;
115
+ /** Extension count for this provider */
116
+ count: number;
117
+ }
118
+
119
+ /**
120
+ * Tabbed dashboard state.
121
+ */
122
+ export interface DashboardState {
123
+ /** Provider tabs */
124
+ tabs: ProviderTab[];
125
+ /** Active tab index */
126
+ activeTabIndex: number;
127
+
128
+ /** All extensions (unfiltered) */
129
+ extensions: Extension[];
130
+ /** Extensions filtered by active tab */
131
+ tabFiltered: Extension[];
132
+ /** Extensions filtered by search (applied after tab filter) */
133
+ searchFiltered: Extension[];
134
+ /** Current search query */
135
+ searchQuery: string;
136
+
137
+ /** Selected index in main list */
138
+ listIndex: number;
139
+ /** Scroll offset for main list */
140
+ scrollOffset: number;
141
+
142
+ /** Currently selected extension for inspector */
143
+ selected: Extension | null;
144
+ }
145
+
146
+ /**
147
+ * @deprecated Use FocusRegion instead
148
+ */
149
+ export type FocusPane = "sidebar" | "main" | "inspector";
150
+
151
+ /**
152
+ * Callbacks from dashboard to parent.
153
+ */
154
+ export interface DashboardCallbacks {
155
+ /** Called when provider is toggled */
156
+ onProviderToggle: (providerId: string, enabled: boolean) => void;
157
+ /** Called when extension item is toggled */
158
+ onExtensionToggle: (extensionId: string, enabled: boolean) => void;
159
+ /** Called when dashboard is closed */
160
+ onClose: () => void;
161
+ }
162
+
163
+ /**
164
+ * Create extension ID from kind and name.
165
+ */
166
+ export function makeExtensionId(kind: ExtensionKind, name: string): string {
167
+ return `${kind}:${name}`;
168
+ }
169
+
170
+ /**
171
+ * Parse extension ID into kind and name.
172
+ */
173
+ export function parseExtensionId(id: string): { kind: ExtensionKind; name: string } | null {
174
+ const colonIdx = id.indexOf(":");
175
+ if (colonIdx === -1) return null;
176
+ return {
177
+ kind: id.slice(0, colonIdx) as ExtensionKind,
178
+ name: id.slice(colonIdx + 1),
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Map SourceMeta to extension source shape.
184
+ */
185
+ export function sourceFromMeta(meta: SourceMeta): Extension["source"] {
186
+ return {
187
+ provider: meta.provider,
188
+ providerName: meta.providerName,
189
+ level: meta.level,
190
+ };
191
+ }
@@ -58,6 +58,10 @@ export class FooterComponent implements Component {
58
58
  private autoCompactEnabled: boolean = true;
59
59
  private hookStatuses: Map<string, string> = new Map();
60
60
 
61
+ // Git status caching (1s TTL to avoid excessive subprocess spawns)
62
+ private cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
63
+ private gitStatusLastFetch = 0;
64
+
61
65
  constructor(session: AgentSession) {
62
66
  this.session = session;
63
67
  }
@@ -165,8 +169,14 @@ export class FooterComponent implements Component {
165
169
  /**
166
170
  * Get git status indicators (staged, unstaged, untracked counts).
167
171
  * Returns null if not in a git repo.
172
+ * Cached for 1s to avoid excessive subprocess spawns.
168
173
  */
169
174
  private getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
175
+ const now = Date.now();
176
+ if (now - this.gitStatusLastFetch < 1000) {
177
+ return this.cachedGitStatus;
178
+ }
179
+
170
180
  try {
171
181
  const output = execSync("git status --porcelain 2>/dev/null", {
172
182
  encoding: "utf8",
@@ -200,8 +210,12 @@ export class FooterComponent implements Component {
200
210
  }
201
211
  }
202
212
 
203
- return { staged, unstaged, untracked };
213
+ this.cachedGitStatus = { staged, unstaged, untracked };
214
+ this.gitStatusLastFetch = now;
215
+ return this.cachedGitStatus;
204
216
  } catch {
217
+ this.cachedGitStatus = null;
218
+ this.gitStatusLastFetch = now;
205
219
  return null;
206
220
  }
207
221
  }
@@ -11,7 +11,6 @@
11
11
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
12
12
  import { getCapabilities } from "@oh-my-pi/pi-tui";
13
13
  import type { SettingsManager } from "../../../core/settings-manager";
14
- import { getAllProvidersInfo, isProviderEnabled } from "../../../discovery";
15
14
 
16
15
  // Setting value types
17
16
  export type SettingValue = boolean | string;
@@ -155,6 +154,35 @@ export const SETTINGS_DEFS: SettingDef[] = [
155
154
  get: (sm) => sm.getEditFuzzyMatch(),
156
155
  set: (sm, v) => sm.setEditFuzzyMatch(v),
157
156
  },
157
+ {
158
+ id: "ttsrEnabled",
159
+ tab: "config",
160
+ type: "boolean",
161
+ label: "TTSR enabled",
162
+ description: "Time Traveling Stream Rules: interrupt agent when output matches rule patterns",
163
+ get: (sm) => sm.getTtsrEnabled(),
164
+ set: (sm, v) => sm.setTtsrEnabled(v),
165
+ },
166
+ {
167
+ id: "ttsrContextMode",
168
+ tab: "config",
169
+ type: "enum",
170
+ label: "TTSR context mode",
171
+ description: "What to do with partial output when TTSR triggers",
172
+ values: ["discard", "keep"],
173
+ get: (sm) => sm.getTtsrContextMode(),
174
+ set: (sm, v) => sm.setTtsrContextMode(v as "keep" | "discard"),
175
+ },
176
+ {
177
+ id: "ttsrRepeatMode",
178
+ tab: "config",
179
+ type: "enum",
180
+ label: "TTSR repeat mode",
181
+ description: "How rules can repeat: once per session or after a message gap",
182
+ values: ["once", "after-gap"],
183
+ get: (sm) => sm.getTtsrRepeatMode(),
184
+ set: (sm, v) => sm.setTtsrRepeatMode(v as "once" | "after-gap"),
185
+ },
158
186
  {
159
187
  id: "thinkingLevel",
160
188
  tab: "config",
@@ -268,38 +296,10 @@ export const SETTINGS_DEFS: SettingDef[] = [
268
296
  ];
269
297
 
270
298
  /**
271
- * Get discovery provider settings dynamically.
272
- * These are generated at runtime from getAllProvidersInfo().
273
- */
274
- function getDiscoverySettings(): SettingDef[] {
275
- const providers = getAllProvidersInfo();
276
- const settings: SettingDef[] = [];
277
-
278
- for (const provider of providers) {
279
- // Skip native provider - it can't be disabled
280
- if (provider.id === "native") {
281
- continue;
282
- }
283
-
284
- settings.push({
285
- id: `discovery.${provider.id}`,
286
- tab: "discovery",
287
- type: "boolean",
288
- label: provider.displayName,
289
- description: provider.description,
290
- get: () => isProviderEnabled(provider.id),
291
- set: () => {}, // Handled in interactive-mode.ts
292
- });
293
- }
294
-
295
- return settings;
296
- }
297
-
298
- /**
299
- * All settings with dynamic discovery settings merged in.
299
+ * All settings. Discovery settings have been moved to /extensions dashboard.
300
300
  */
301
301
  function getAllSettings(): SettingDef[] {
302
- return [...SETTINGS_DEFS, ...getDiscoverySettings()];
302
+ return SETTINGS_DEFS;
303
303
  }
304
304
 
305
305
  /** Get settings for a specific tab */
@@ -99,7 +99,6 @@ const SETTINGS_TABS: Tab[] = [
99
99
  { id: "config", label: "Config" },
100
100
  { id: "lsp", label: "LSP" },
101
101
  { id: "exa", label: "Exa" },
102
- { id: "discovery", label: "Discovery" },
103
102
  { id: "plugins", label: "Plugins" },
104
103
  ];
105
104
 
@@ -0,0 +1,82 @@
1
+ import { Box, Container, Spacer, Text } from "@oh-my-pi/pi-tui";
2
+ import type { Rule } from "../../../capability/rule";
3
+ import { theme } from "../theme/theme";
4
+
5
+ /**
6
+ * Component that renders a TTSR (Time Traveling Stream Rules) notification.
7
+ * Shows when a rule violation is detected and the stream is being rewound.
8
+ */
9
+ export class TtsrNotificationComponent extends Container {
10
+ private rules: Rule[];
11
+ private box: Box;
12
+ private _expanded = false;
13
+
14
+ constructor(rules: Rule[]) {
15
+ super();
16
+ this.rules = rules;
17
+
18
+ this.addChild(new Spacer(1));
19
+
20
+ // Use inverse warning color for yellow background effect
21
+ this.box = new Box(1, 1, (t) => theme.inverse(theme.fg("warning", t)));
22
+ this.addChild(this.box);
23
+
24
+ this.rebuild();
25
+ }
26
+
27
+ setExpanded(expanded: boolean): void {
28
+ if (this._expanded !== expanded) {
29
+ this._expanded = expanded;
30
+ this.rebuild();
31
+ }
32
+ }
33
+
34
+ isExpanded(): boolean {
35
+ return this._expanded;
36
+ }
37
+
38
+ private rebuild(): void {
39
+ this.box.clear();
40
+
41
+ // Build header: ⚠ Injecting <bold>rule-name</bold> ↩
42
+ const ruleNames = this.rules.map((r) => theme.bold(r.name)).join(", ");
43
+ const label = this.rules.length === 1 ? "rule" : "rules";
44
+ const header = `\u26A0 Injecting ${label}: ${ruleNames}`;
45
+
46
+ // Create header with rewind icon on the right
47
+ const rewindIcon = "\u21A9"; // ↩
48
+
49
+ this.box.addChild(new Text(`${header} ${rewindIcon}`, 0, 0));
50
+
51
+ // Show description(s) - italic and truncated
52
+ for (const rule of this.rules) {
53
+ const desc = rule.description || rule.content;
54
+ if (desc) {
55
+ this.box.addChild(new Spacer(1));
56
+
57
+ let displayText = desc.trim();
58
+ if (!this._expanded) {
59
+ // Truncate to first 2 lines
60
+ const lines = displayText.split("\n");
61
+ if (lines.length > 2) {
62
+ displayText = `${lines.slice(0, 2).join("\n")}...`;
63
+ }
64
+ }
65
+
66
+ // Use italic for subtle distinction (fg colors conflict with inverse)
67
+ this.box.addChild(new Text(theme.italic(displayText), 0, 0));
68
+ }
69
+ }
70
+
71
+ // Show expand hint if collapsed and there's more content
72
+ if (!this._expanded) {
73
+ const hasMoreContent = this.rules.some((r) => {
74
+ const desc = r.description || r.content;
75
+ return desc && desc.split("\n").length > 2;
76
+ });
77
+ if (hasMoreContent) {
78
+ this.box.addChild(new Text(theme.italic(" (ctrl+o to expand)"), 0, 0));
79
+ }
80
+ }
81
+ }
82
+ }