@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.
- package/CHANGELOG.md +32 -0
- package/package.json +4 -4
- package/src/capability/rule.ts +4 -0
- package/src/core/agent-session.ts +92 -1
- package/src/core/sdk.ts +27 -0
- package/src/core/session-manager.ts +60 -4
- package/src/core/settings-manager.ts +101 -0
- package/src/core/system-prompt.ts +15 -0
- package/src/core/title-generator.ts +28 -6
- package/src/core/tools/index.ts +6 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/ttsr.ts +211 -0
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/cline.ts +2 -0
- package/src/discovery/cursor.ts +2 -0
- package/src/discovery/windsurf.ts +3 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +297 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +477 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/footer.ts +15 -1
- package/src/modes/interactive/components/settings-defs.ts +31 -31
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/components/ttsr-notification.ts +82 -0
- package/src/modes/interactive/interactive-mode.ts +54 -314
- package/src/modes/print-mode.ts +34 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExtensionDashboard - Tabbed layout for the Extension Control Center.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - Top: Horizontal tab bar for provider selection
|
|
6
|
+
* - Body: 2-column grid (inventory list | preview panel)
|
|
7
|
+
*
|
|
8
|
+
* Navigation:
|
|
9
|
+
* - TAB/Shift+TAB: Cycle through provider tabs
|
|
10
|
+
* - Up/Down/j/k: Navigate list
|
|
11
|
+
* - Space: Toggle selected item (or master switch)
|
|
12
|
+
* - Esc: Close dashboard (clears search first if active)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type Component,
|
|
17
|
+
Container,
|
|
18
|
+
isCtrlC,
|
|
19
|
+
isEscape,
|
|
20
|
+
isShiftTab,
|
|
21
|
+
isTab,
|
|
22
|
+
Spacer,
|
|
23
|
+
Text,
|
|
24
|
+
truncateToWidth,
|
|
25
|
+
visibleWidth,
|
|
26
|
+
} from "@oh-my-pi/pi-tui";
|
|
27
|
+
import type { SettingsManager } from "../../../../core/settings-manager";
|
|
28
|
+
import { theme } from "../../theme/theme";
|
|
29
|
+
import { DynamicBorder } from "../dynamic-border";
|
|
30
|
+
import { ExtensionList } from "./extension-list";
|
|
31
|
+
import { InspectorPanel } from "./inspector-panel";
|
|
32
|
+
import { applyFilter, createInitialState, filterByProvider, refreshState, toggleProvider } from "./state-manager";
|
|
33
|
+
import type { DashboardState } from "./types";
|
|
34
|
+
|
|
35
|
+
export class ExtensionDashboard extends Container {
|
|
36
|
+
private state: DashboardState;
|
|
37
|
+
private mainList: ExtensionList;
|
|
38
|
+
private inspector: InspectorPanel;
|
|
39
|
+
private settingsManager: SettingsManager | null;
|
|
40
|
+
private cwd: string;
|
|
41
|
+
|
|
42
|
+
public onClose?: () => void;
|
|
43
|
+
|
|
44
|
+
constructor(cwd: string, settingsManager: SettingsManager | null = null) {
|
|
45
|
+
super();
|
|
46
|
+
this.cwd = cwd;
|
|
47
|
+
this.settingsManager = settingsManager;
|
|
48
|
+
const disabledIds = settingsManager?.getDisabledExtensions() ?? [];
|
|
49
|
+
this.state = createInitialState(cwd, disabledIds);
|
|
50
|
+
|
|
51
|
+
// Create main list - always focused
|
|
52
|
+
this.mainList = new ExtensionList(this.state.searchFiltered, {
|
|
53
|
+
onSelectionChange: (ext) => {
|
|
54
|
+
this.state.selected = ext;
|
|
55
|
+
this.inspector.setExtension(ext);
|
|
56
|
+
},
|
|
57
|
+
onToggle: (extensionId, enabled) => {
|
|
58
|
+
this.handleExtensionToggle(extensionId, enabled);
|
|
59
|
+
},
|
|
60
|
+
onMasterToggle: (providerId) => {
|
|
61
|
+
this.handleProviderToggle(providerId);
|
|
62
|
+
},
|
|
63
|
+
masterSwitchProvider: this.getActiveProviderId(),
|
|
64
|
+
});
|
|
65
|
+
this.mainList.setFocused(true);
|
|
66
|
+
|
|
67
|
+
// Create inspector
|
|
68
|
+
this.inspector = new InspectorPanel();
|
|
69
|
+
if (this.state.selected) {
|
|
70
|
+
this.inspector.setExtension(this.state.selected);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.buildLayout();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private getActiveProviderId(): string | null {
|
|
77
|
+
const tab = this.state.tabs[this.state.activeTabIndex];
|
|
78
|
+
return tab && tab.id !== "all" ? tab.id : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private buildLayout(): void {
|
|
82
|
+
this.clear();
|
|
83
|
+
|
|
84
|
+
// Top border
|
|
85
|
+
this.addChild(new DynamicBorder());
|
|
86
|
+
|
|
87
|
+
// Title
|
|
88
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", " Extension Control Center")), 0, 0));
|
|
89
|
+
|
|
90
|
+
// Tab bar
|
|
91
|
+
this.addChild(new Text(this.renderTabBar(), 0, 0));
|
|
92
|
+
this.addChild(new Spacer(1));
|
|
93
|
+
|
|
94
|
+
// Help text
|
|
95
|
+
// 2-column body
|
|
96
|
+
this.addChild(new TwoColumnBody(this.mainList, this.inspector));
|
|
97
|
+
|
|
98
|
+
this.addChild(new Spacer(1));
|
|
99
|
+
this.addChild(new Text(theme.fg("dim", " ↑/↓: navigate Space: toggle Tab: next provider Esc: close"), 0, 0));
|
|
100
|
+
|
|
101
|
+
// Bottom border
|
|
102
|
+
this.addChild(new DynamicBorder());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private renderTabBar(): string {
|
|
106
|
+
const parts: string[] = [" "];
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < this.state.tabs.length; i++) {
|
|
109
|
+
const tab = this.state.tabs[i];
|
|
110
|
+
const isActive = i === this.state.activeTabIndex;
|
|
111
|
+
const isEmpty = tab.count === 0 && tab.id !== "all";
|
|
112
|
+
const isDisabled = !tab.enabled && tab.id !== "all";
|
|
113
|
+
|
|
114
|
+
// Build label with count
|
|
115
|
+
let label = tab.label;
|
|
116
|
+
if (tab.count > 0) {
|
|
117
|
+
label += ` (${tab.count})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Apply strikethrough for disabled providers
|
|
121
|
+
const displayLabel = isDisabled ? `${label.split("").join("\u0336")}\u0336` : label;
|
|
122
|
+
|
|
123
|
+
if (isActive) {
|
|
124
|
+
// Active tab: background highlight
|
|
125
|
+
parts.push(theme.bg("selectedBg", ` ${displayLabel} `));
|
|
126
|
+
} else if (isDisabled) {
|
|
127
|
+
// Disabled provider: strikethrough + dim
|
|
128
|
+
parts.push(theme.fg("dim", ` ${displayLabel} `));
|
|
129
|
+
} else if (isEmpty) {
|
|
130
|
+
// Empty enabled provider: very dim, unselectable
|
|
131
|
+
parts.push(`\x1b[38;5;238m ${label} \x1b[0m`);
|
|
132
|
+
} else {
|
|
133
|
+
// Normal enabled provider
|
|
134
|
+
parts.push(theme.fg("muted", ` ${label} `));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return parts.join("");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private handleProviderToggle(providerId: string): void {
|
|
142
|
+
toggleProvider(providerId);
|
|
143
|
+
this.refreshFromState();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private handleExtensionToggle(extensionId: string, enabled: boolean): void {
|
|
147
|
+
if (!this.settingsManager) return;
|
|
148
|
+
|
|
149
|
+
if (enabled) {
|
|
150
|
+
this.settingsManager.enableExtension(extensionId);
|
|
151
|
+
} else {
|
|
152
|
+
this.settingsManager.disableExtension(extensionId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.refreshFromState();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private refreshFromState(): void {
|
|
159
|
+
// Remember current tab ID before refresh
|
|
160
|
+
const currentTabId = this.state.tabs[this.state.activeTabIndex]?.id;
|
|
161
|
+
|
|
162
|
+
const disabledIds = this.settingsManager?.getDisabledExtensions() ?? [];
|
|
163
|
+
this.state = refreshState(this.state, this.cwd, disabledIds);
|
|
164
|
+
|
|
165
|
+
// Find the same tab in the new (re-sorted) list
|
|
166
|
+
if (currentTabId) {
|
|
167
|
+
const newIndex = this.state.tabs.findIndex((t) => t.id === currentTabId);
|
|
168
|
+
if (newIndex >= 0) {
|
|
169
|
+
this.state.activeTabIndex = newIndex;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.mainList.setExtensions(this.state.searchFiltered);
|
|
174
|
+
this.mainList.setMasterSwitchProvider(this.getActiveProviderId());
|
|
175
|
+
|
|
176
|
+
if (this.state.selected) {
|
|
177
|
+
this.inspector.setExtension(this.state.selected);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.buildLayout();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private switchTab(direction: 1 | -1): void {
|
|
184
|
+
const numTabs = this.state.tabs.length;
|
|
185
|
+
if (numTabs === 0) return;
|
|
186
|
+
|
|
187
|
+
// Find next selectable tab (skip empty+enabled providers)
|
|
188
|
+
let nextIndex = this.state.activeTabIndex;
|
|
189
|
+
for (let i = 0; i < numTabs; i++) {
|
|
190
|
+
nextIndex = (nextIndex + direction + numTabs) % numTabs;
|
|
191
|
+
const tab = this.state.tabs[nextIndex];
|
|
192
|
+
const isEmptyEnabled = tab.count === 0 && tab.enabled && tab.id !== "all";
|
|
193
|
+
if (!isEmptyEnabled) break;
|
|
194
|
+
}
|
|
195
|
+
this.state.activeTabIndex = nextIndex;
|
|
196
|
+
|
|
197
|
+
// Re-filter for new tab
|
|
198
|
+
const tab = this.state.tabs[this.state.activeTabIndex];
|
|
199
|
+
this.state.tabFiltered = filterByProvider(this.state.extensions, tab.id);
|
|
200
|
+
this.state.searchFiltered = applyFilter(this.state.tabFiltered, this.state.searchQuery);
|
|
201
|
+
this.state.listIndex = 0;
|
|
202
|
+
this.state.scrollOffset = 0;
|
|
203
|
+
this.state.selected = this.state.searchFiltered[0] ?? null;
|
|
204
|
+
|
|
205
|
+
// Update list
|
|
206
|
+
this.mainList.setExtensions(this.state.searchFiltered);
|
|
207
|
+
this.mainList.setMasterSwitchProvider(this.getActiveProviderId());
|
|
208
|
+
this.mainList.resetSelection();
|
|
209
|
+
|
|
210
|
+
if (this.state.selected) {
|
|
211
|
+
this.inspector.setExtension(this.state.selected);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
this.buildLayout();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
handleInput(data: string): void {
|
|
218
|
+
// Ctrl+C - close immediately
|
|
219
|
+
if (isCtrlC(data)) {
|
|
220
|
+
this.onClose?.();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Escape - clear search first, then close
|
|
225
|
+
if (isEscape(data)) {
|
|
226
|
+
if (this.state.searchQuery.length > 0) {
|
|
227
|
+
this.state.searchQuery = "";
|
|
228
|
+
this.state.searchFiltered = this.state.tabFiltered;
|
|
229
|
+
this.mainList.setExtensions(this.state.searchFiltered);
|
|
230
|
+
this.mainList.clearSearch();
|
|
231
|
+
this.buildLayout();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.onClose?.();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Tab/Shift+Tab: Cycle through tabs
|
|
239
|
+
if (isTab(data)) {
|
|
240
|
+
this.switchTab(1);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (isShiftTab(data)) {
|
|
244
|
+
this.switchTab(-1);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// All other input goes to the list
|
|
249
|
+
this.mainList.handleInput(data);
|
|
250
|
+
|
|
251
|
+
// Sync search query back to state
|
|
252
|
+
const query = this.mainList.getSearchQuery();
|
|
253
|
+
if (query !== this.state.searchQuery) {
|
|
254
|
+
this.state.searchQuery = query;
|
|
255
|
+
this.state.searchFiltered = applyFilter(this.state.tabFiltered, query);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Two-column body component for side-by-side rendering.
|
|
262
|
+
*/
|
|
263
|
+
class TwoColumnBody implements Component {
|
|
264
|
+
private leftPane: ExtensionList;
|
|
265
|
+
private rightPane: InspectorPanel;
|
|
266
|
+
|
|
267
|
+
constructor(left: ExtensionList, right: InspectorPanel) {
|
|
268
|
+
this.leftPane = left;
|
|
269
|
+
this.rightPane = right;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
render(width: number): string[] {
|
|
273
|
+
const leftWidth = Math.floor(width * 0.5);
|
|
274
|
+
const rightWidth = width - leftWidth - 3;
|
|
275
|
+
|
|
276
|
+
const leftLines = this.leftPane.render(leftWidth);
|
|
277
|
+
const rightLines = this.rightPane.render(rightWidth);
|
|
278
|
+
|
|
279
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
280
|
+
const combined: string[] = [];
|
|
281
|
+
const separator = theme.fg("dim", " │ ");
|
|
282
|
+
|
|
283
|
+
for (let i = 0; i < maxLines; i++) {
|
|
284
|
+
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
285
|
+
const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
|
|
286
|
+
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
|
287
|
+
combined.push(leftPadded + separator + right);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return combined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
invalidate(): void {
|
|
294
|
+
this.leftPane.invalidate?.();
|
|
295
|
+
this.rightPane.invalidate?.();
|
|
296
|
+
}
|
|
297
|
+
}
|