@oh-my-pi/pi-coding-agent 8.12.9 → 8.13.0
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 +14 -0
- package/package.json +7 -9
- package/src/config/settings-manager.ts +61 -3
- package/src/debug/index.ts +362 -0
- package/src/debug/profiler.ts +158 -0
- package/src/debug/report-bundle.ts +280 -0
- package/src/debug/system-info.ts +91 -0
- package/src/exec/bash-executor.ts +2 -2
- package/src/extensibility/plugins/installer.ts +2 -2
- package/src/extensibility/plugins/manager.ts +3 -3
- package/src/index.ts +0 -1
- package/src/ipy/gateway-coordinator.ts +12 -22
- package/src/ipy/kernel.ts +3 -3
- package/src/lsp/client.ts +27 -21
- package/src/lsp/index.ts +1 -0
- package/src/lsp/render.ts +13 -19
- package/src/lsp/types.ts +2 -2
- package/src/main.ts +1 -1
- package/src/modes/controllers/command-controller.ts +3 -42
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +8 -0
- package/src/modes/interactive-mode.ts +8 -5
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/theme/theme.ts +27 -47
- package/src/modes/types.ts +2 -2
- package/src/sdk.ts +1 -1
- package/src/session/agent-storage.ts +26 -2
- package/src/tools/fetch.ts +55 -44
- package/src/tools/gemini-image.ts +2 -7
- package/src/tools/read.ts +175 -55
- package/src/utils/tools-manager.ts +35 -28
- package/src/web/scrapers/github.ts +11 -14
- package/src/web/scrapers/types.ts +2 -36
- package/src/web/scrapers/utils.ts +11 -14
- package/src/web/scrapers/youtube.ts +5 -15
- package/src/web/search/providers/codex.ts +358 -0
- package/src/web/search/providers/gemini.ts +426 -0
- package/src/web/search/types.ts +1 -1
- package/src/utils/shell.ts +0 -302
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.13.0] - 2026-01-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `/debug` command with interactive menu for bug report generation:
|
|
9
|
+
- `Report: performance issue` - CPU profiling with reproduction flow
|
|
10
|
+
- `Report: dump session` - Immediate session bundle creation
|
|
11
|
+
- `Report: memory issue` - Heap snapshot with bundle
|
|
12
|
+
- `View: recent logs` - Display last 50 log entries
|
|
13
|
+
- `View: system info` - Show environment details
|
|
14
|
+
- `Clear: artifact cache` - Remove old session artifacts
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Fixed LSP server errors not being visible in `/session` output or logs when startup fails
|
|
18
|
+
|
|
5
19
|
## [8.12.7] - 2026-01-29
|
|
6
20
|
|
|
7
21
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -79,22 +79,20 @@
|
|
|
79
79
|
"test": "bun test"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@oh-my-pi/omp-stats": "8.
|
|
83
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
84
|
-
"@oh-my-pi/pi-ai": "8.
|
|
85
|
-
"@oh-my-pi/pi-natives": "8.
|
|
86
|
-
"@oh-my-pi/pi-tui": "8.
|
|
87
|
-
"@oh-my-pi/pi-utils": "8.
|
|
82
|
+
"@oh-my-pi/omp-stats": "8.13.0",
|
|
83
|
+
"@oh-my-pi/pi-agent-core": "8.13.0",
|
|
84
|
+
"@oh-my-pi/pi-ai": "8.13.0",
|
|
85
|
+
"@oh-my-pi/pi-natives": "8.13.0",
|
|
86
|
+
"@oh-my-pi/pi-tui": "8.13.0",
|
|
87
|
+
"@oh-my-pi/pi-utils": "8.13.0",
|
|
88
88
|
"@openai/agents": "^0.4.4",
|
|
89
89
|
"@sinclair/typebox": "^0.34.48",
|
|
90
90
|
"ajv": "^8.17.1",
|
|
91
91
|
"chalk": "^5.6.2",
|
|
92
|
-
"cli-highlight": "^2.1.11",
|
|
93
92
|
"diff": "^8.0.3",
|
|
94
93
|
"file-type": "^21.3.0",
|
|
95
94
|
"glob": "^13.0.0",
|
|
96
95
|
"handlebars": "^4.7.8",
|
|
97
|
-
"highlight.js": "^11.11.1",
|
|
98
96
|
"marked": "^17.0.1",
|
|
99
97
|
"nanoid": "^5.1.6",
|
|
100
98
|
"node-html-parser": "^7.0.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { YAML } from "bun";
|
|
5
5
|
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
6
6
|
import { getAgentDbPath, getAgentDir } from "../config";
|
|
@@ -461,6 +461,8 @@ export class SettingsManager {
|
|
|
461
461
|
private settings!: Settings;
|
|
462
462
|
private persist: boolean;
|
|
463
463
|
|
|
464
|
+
static #lastInstance: SettingsManager | null = null;
|
|
465
|
+
|
|
464
466
|
/**
|
|
465
467
|
* Private constructor - use static factory methods instead.
|
|
466
468
|
* @param storage - SQLite storage instance for auth/cache, or null for in-memory mode
|
|
@@ -477,6 +479,7 @@ export class SettingsManager {
|
|
|
477
479
|
initialSettings: Settings,
|
|
478
480
|
persist: boolean,
|
|
479
481
|
projectSettings: Settings,
|
|
482
|
+
private agentDir: string | null,
|
|
480
483
|
) {
|
|
481
484
|
this.storage = storage;
|
|
482
485
|
this.configPath = configPath;
|
|
@@ -517,6 +520,9 @@ export class SettingsManager {
|
|
|
517
520
|
* @returns Configured SettingsManager with merged global and user settings
|
|
518
521
|
*/
|
|
519
522
|
static async create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): Promise<SettingsManager> {
|
|
523
|
+
cwd = path.normalize(cwd);
|
|
524
|
+
agentDir = path.normalize(agentDir);
|
|
525
|
+
|
|
520
526
|
const configPath = path.join(agentDir, "config.yml");
|
|
521
527
|
const storage = await AgentStorage.open(getAgentDbPath(agentDir));
|
|
522
528
|
|
|
@@ -541,7 +547,9 @@ export class SettingsManager {
|
|
|
541
547
|
// Load project settings before construction (constructor is sync)
|
|
542
548
|
const projectSettings = await SettingsManager.loadProjectSettingsStatic(cwd);
|
|
543
549
|
|
|
544
|
-
|
|
550
|
+
const instance = new SettingsManager(storage, configPath, cwd, globalSettings, true, projectSettings, agentDir);
|
|
551
|
+
SettingsManager.#lastInstance = instance;
|
|
552
|
+
return instance;
|
|
545
553
|
}
|
|
546
554
|
|
|
547
555
|
/**
|
|
@@ -550,7 +558,7 @@ export class SettingsManager {
|
|
|
550
558
|
* @returns SettingsManager that won't persist changes to disk
|
|
551
559
|
*/
|
|
552
560
|
static inMemory(settings: Partial<Settings> = {}): SettingsManager {
|
|
553
|
-
return new SettingsManager(null, null, null, settings, false, {});
|
|
561
|
+
return new SettingsManager(null, null, null, settings, false, {}, null);
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
/**
|
|
@@ -1784,4 +1792,54 @@ export class SettingsManager {
|
|
|
1784
1792
|
await this.save();
|
|
1785
1793
|
}
|
|
1786
1794
|
}
|
|
1795
|
+
|
|
1796
|
+
_compareUniqueCtorKeys(cwd: string, agentDir: string): boolean {
|
|
1797
|
+
if (this.cwd !== cwd) {
|
|
1798
|
+
cwd = path.normalize(cwd);
|
|
1799
|
+
if (this.cwd !== cwd) {
|
|
1800
|
+
return false;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (this.agentDir !== agentDir) {
|
|
1804
|
+
agentDir = path.normalize(agentDir);
|
|
1805
|
+
if (this.agentDir !== agentDir) {
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* Acquire the last created SettingsManager instance.
|
|
1814
|
+
* If no instance exists, create a new one.
|
|
1815
|
+
* @returns The SettingsManager instance
|
|
1816
|
+
*/
|
|
1817
|
+
static acquire(
|
|
1818
|
+
cwd: string = process.cwd(),
|
|
1819
|
+
agentDir: string = getAgentDir(),
|
|
1820
|
+
): SettingsManager | Promise<SettingsManager> {
|
|
1821
|
+
const prev = SettingsManager.#lastInstance;
|
|
1822
|
+
if (prev?._compareUniqueCtorKeys(cwd, agentDir)) {
|
|
1823
|
+
return prev;
|
|
1824
|
+
}
|
|
1825
|
+
return SettingsManager.create(cwd, agentDir);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Gets the shell configuration
|
|
1830
|
+
* @returns The shell configuration
|
|
1831
|
+
*/
|
|
1832
|
+
async getShellConfig() {
|
|
1833
|
+
const shell = this.getShellPath();
|
|
1834
|
+
return procmgr.getShellConfig(shell);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Gets the shell configuration from the last created SettingsManager instance.
|
|
1839
|
+
* @returns The shell configuration
|
|
1840
|
+
*/
|
|
1841
|
+
static async getGlobalShellConfig() {
|
|
1842
|
+
const settings = await SettingsManager.acquire();
|
|
1843
|
+
return settings.getShellConfig();
|
|
1844
|
+
}
|
|
1787
1845
|
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug command handler with interactive menu.
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for debugging, bug report generation, and system diagnostics.
|
|
5
|
+
*/
|
|
6
|
+
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
7
|
+
import { getSessionsDir } from "../config";
|
|
8
|
+
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
9
|
+
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
10
|
+
import type { InteractiveModeContext } from "../modes/types";
|
|
11
|
+
import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
|
|
12
|
+
import { clearArtifactCache, createReportBundle, getArtifactCacheStats, getRecentLogs } from "./report-bundle";
|
|
13
|
+
import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
14
|
+
|
|
15
|
+
/** Debug menu options */
|
|
16
|
+
const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
17
|
+
{ value: "performance", label: "Report: performance issue", description: "Profile CPU, reproduce, then bundle" },
|
|
18
|
+
{ value: "dump", label: "Report: dump session", description: "Create report bundle immediately" },
|
|
19
|
+
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
20
|
+
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
21
|
+
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
22
|
+
{ value: "clear-cache", label: "Clear: artifact cache", description: "Remove old session artifacts" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Debug selector component.
|
|
27
|
+
*/
|
|
28
|
+
export class DebugSelectorComponent extends Container {
|
|
29
|
+
private selectList: SelectList;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private ctx: InteractiveModeContext,
|
|
33
|
+
onDone: () => void,
|
|
34
|
+
) {
|
|
35
|
+
super();
|
|
36
|
+
|
|
37
|
+
// Title
|
|
38
|
+
this.addChild(new DynamicBorder());
|
|
39
|
+
this.addChild(new Text(theme.bold(theme.fg("accent", "Debug Tools")), 1, 0));
|
|
40
|
+
this.addChild(new Spacer(1));
|
|
41
|
+
|
|
42
|
+
// Select list
|
|
43
|
+
this.selectList = new SelectList(DEBUG_MENU_ITEMS, 6, getSelectListTheme());
|
|
44
|
+
|
|
45
|
+
this.selectList.onSelect = item => {
|
|
46
|
+
onDone();
|
|
47
|
+
void this.handleSelection(item.value);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.selectList.onCancel = () => {
|
|
51
|
+
onDone();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.addChild(this.selectList);
|
|
55
|
+
this.addChild(new DynamicBorder());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleInput(keyData: string): void {
|
|
59
|
+
this.selectList.handleInput(keyData);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async handleSelection(value: string): Promise<void> {
|
|
63
|
+
switch (value) {
|
|
64
|
+
case "performance":
|
|
65
|
+
await this.handlePerformanceReport();
|
|
66
|
+
break;
|
|
67
|
+
case "dump":
|
|
68
|
+
await this.handleDumpReport();
|
|
69
|
+
break;
|
|
70
|
+
case "memory":
|
|
71
|
+
await this.handleMemoryReport();
|
|
72
|
+
break;
|
|
73
|
+
case "logs":
|
|
74
|
+
await this.handleViewLogs();
|
|
75
|
+
break;
|
|
76
|
+
case "system":
|
|
77
|
+
await this.handleViewSystemInfo();
|
|
78
|
+
break;
|
|
79
|
+
case "clear-cache":
|
|
80
|
+
await this.handleClearCache();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async handlePerformanceReport(): Promise<void> {
|
|
86
|
+
// Start profiling
|
|
87
|
+
let session: ProfilerSession;
|
|
88
|
+
try {
|
|
89
|
+
session = await startCpuProfile();
|
|
90
|
+
} catch (err) {
|
|
91
|
+
this.ctx.showError(`Failed to start profiler: ${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show message and wait for keypress
|
|
96
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
97
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
|
|
98
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
99
|
+
this.ctx.chatContainer.addChild(
|
|
100
|
+
new Text(theme.fg("muted", "Reproduce the performance issue, then press Enter to stop profiling."), 1, 0),
|
|
101
|
+
);
|
|
102
|
+
this.ctx.ui.requestRender();
|
|
103
|
+
|
|
104
|
+
// Wait for Enter keypress
|
|
105
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
106
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
107
|
+
const originalOnSubmit = this.ctx.editor.onSubmit;
|
|
108
|
+
|
|
109
|
+
this.ctx.editor.onSubmit = () => {
|
|
110
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
111
|
+
this.ctx.editor.onSubmit = originalOnSubmit;
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
this.ctx.editor.onEscape = () => {
|
|
116
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
117
|
+
this.ctx.editor.onSubmit = originalOnSubmit;
|
|
118
|
+
resolve();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
await promise;
|
|
122
|
+
|
|
123
|
+
// Stop profiling and create report
|
|
124
|
+
const loader = new Loader(
|
|
125
|
+
this.ctx.ui,
|
|
126
|
+
spinner => theme.fg("accent", spinner),
|
|
127
|
+
text => theme.fg("muted", text),
|
|
128
|
+
"Generating report...",
|
|
129
|
+
getSymbolTheme().spinnerFrames,
|
|
130
|
+
);
|
|
131
|
+
this.ctx.statusContainer.addChild(loader);
|
|
132
|
+
this.ctx.ui.requestRender();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const cpuProfile = await session.stop();
|
|
136
|
+
const result = await createReportBundle({
|
|
137
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
138
|
+
settings: this.getResolvedSettings(),
|
|
139
|
+
cpuProfile,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
loader.stop();
|
|
143
|
+
this.ctx.statusContainer.clear();
|
|
144
|
+
|
|
145
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
146
|
+
this.ctx.chatContainer.addChild(
|
|
147
|
+
new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0),
|
|
148
|
+
);
|
|
149
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
150
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
loader.stop();
|
|
153
|
+
this.ctx.statusContainer.clear();
|
|
154
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.ctx.ui.requestRender();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async handleDumpReport(): Promise<void> {
|
|
161
|
+
const loader = new Loader(
|
|
162
|
+
this.ctx.ui,
|
|
163
|
+
spinner => theme.fg("accent", spinner),
|
|
164
|
+
text => theme.fg("muted", text),
|
|
165
|
+
"Creating report bundle...",
|
|
166
|
+
getSymbolTheme().spinnerFrames,
|
|
167
|
+
);
|
|
168
|
+
this.ctx.statusContainer.addChild(loader);
|
|
169
|
+
this.ctx.ui.requestRender();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const result = await createReportBundle({
|
|
173
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
174
|
+
settings: this.getResolvedSettings(),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
loader.stop();
|
|
178
|
+
this.ctx.statusContainer.clear();
|
|
179
|
+
|
|
180
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
181
|
+
this.ctx.chatContainer.addChild(
|
|
182
|
+
new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0),
|
|
183
|
+
);
|
|
184
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
185
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
loader.stop();
|
|
188
|
+
this.ctx.statusContainer.clear();
|
|
189
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.ctx.ui.requestRender();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async handleMemoryReport(): Promise<void> {
|
|
196
|
+
const loader = new Loader(
|
|
197
|
+
this.ctx.ui,
|
|
198
|
+
spinner => theme.fg("accent", spinner),
|
|
199
|
+
text => theme.fg("muted", text),
|
|
200
|
+
"Generating heap snapshot...",
|
|
201
|
+
getSymbolTheme().spinnerFrames,
|
|
202
|
+
);
|
|
203
|
+
this.ctx.statusContainer.addChild(loader);
|
|
204
|
+
this.ctx.ui.requestRender();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const heapSnapshot = generateHeapSnapshotData();
|
|
208
|
+
loader.setText("Creating report bundle...");
|
|
209
|
+
|
|
210
|
+
const result = await createReportBundle({
|
|
211
|
+
sessionFile: this.ctx.sessionManager.getSessionFile(),
|
|
212
|
+
settings: this.getResolvedSettings(),
|
|
213
|
+
heapSnapshot,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
loader.stop();
|
|
217
|
+
this.ctx.statusContainer.clear();
|
|
218
|
+
|
|
219
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
220
|
+
this.ctx.chatContainer.addChild(
|
|
221
|
+
new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0),
|
|
222
|
+
);
|
|
223
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
224
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
loader.stop();
|
|
227
|
+
this.ctx.statusContainer.clear();
|
|
228
|
+
this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.ctx.ui.requestRender();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async handleViewLogs(): Promise<void> {
|
|
235
|
+
try {
|
|
236
|
+
const logs = await getRecentLogs(50);
|
|
237
|
+
if (!logs) {
|
|
238
|
+
this.ctx.showWarning("No log entries found for today.");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
243
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
244
|
+
this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Recent Logs")), 1, 0));
|
|
245
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
246
|
+
|
|
247
|
+
// Display logs with dim styling
|
|
248
|
+
const lines = logs.split("\n").slice(-50);
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
if (line.trim()) {
|
|
251
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this.ctx.showError(`Failed to read logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
this.ctx.ui.requestRender();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async handleViewSystemInfo(): Promise<void> {
|
|
264
|
+
try {
|
|
265
|
+
const info = await collectSystemInfo();
|
|
266
|
+
const formatted = formatSystemInfo(info);
|
|
267
|
+
|
|
268
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
269
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
270
|
+
this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
|
|
271
|
+
this.ctx.chatContainer.addChild(new DynamicBorder());
|
|
272
|
+
} catch (err) {
|
|
273
|
+
this.ctx.showError(`Failed to collect system info: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.ctx.ui.requestRender();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async handleClearCache(): Promise<void> {
|
|
280
|
+
const sessionsDir = getSessionsDir();
|
|
281
|
+
|
|
282
|
+
// Get stats first
|
|
283
|
+
const stats = await getArtifactCacheStats(sessionsDir);
|
|
284
|
+
|
|
285
|
+
if (stats.count === 0) {
|
|
286
|
+
this.ctx.showStatus("Artifact cache is empty.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sizeStr = formatBytes(stats.totalSize);
|
|
291
|
+
const oldestStr = stats.oldestDate ? stats.oldestDate.toLocaleDateString() : "unknown";
|
|
292
|
+
|
|
293
|
+
// Show confirmation
|
|
294
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
295
|
+
"Clear Artifact Cache",
|
|
296
|
+
`Found ${stats.count} artifact files (${sizeStr})\nOldest: ${oldestStr}\n\nRemove artifacts older than 30 days?`,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (!confirmed) {
|
|
300
|
+
this.ctx.showStatus("Cache clear cancelled.");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Clear cache
|
|
305
|
+
const loader = new Loader(
|
|
306
|
+
this.ctx.ui,
|
|
307
|
+
spinner => theme.fg("accent", spinner),
|
|
308
|
+
text => theme.fg("muted", text),
|
|
309
|
+
"Clearing artifact cache...",
|
|
310
|
+
getSymbolTheme().spinnerFrames,
|
|
311
|
+
);
|
|
312
|
+
this.ctx.statusContainer.addChild(loader);
|
|
313
|
+
this.ctx.ui.requestRender();
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const result = await clearArtifactCache(sessionsDir, 30);
|
|
317
|
+
|
|
318
|
+
loader.stop();
|
|
319
|
+
this.ctx.statusContainer.clear();
|
|
320
|
+
|
|
321
|
+
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
322
|
+
this.ctx.chatContainer.addChild(
|
|
323
|
+
new Text(
|
|
324
|
+
theme.fg("success", `${theme.status.success} Cleared ${result.removed} artifact directories`),
|
|
325
|
+
1,
|
|
326
|
+
0,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
loader.stop();
|
|
331
|
+
this.ctx.statusContainer.clear();
|
|
332
|
+
this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.ctx.ui.requestRender();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private getResolvedSettings(): Record<string, unknown> {
|
|
339
|
+
// Extract key settings for the report
|
|
340
|
+
return {
|
|
341
|
+
model: this.ctx.session.model?.id,
|
|
342
|
+
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
343
|
+
planModeEnabled: this.ctx.planModeEnabled,
|
|
344
|
+
toolOutputExpanded: this.ctx.toolOutputExpanded,
|
|
345
|
+
hideThinkingBlock: this.ctx.hideThinkingBlock,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function formatBytes(bytes: number): string {
|
|
351
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
352
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
353
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Show the debug selector.
|
|
358
|
+
*/
|
|
359
|
+
export function showDebugSelector(ctx: InteractiveModeContext, done: () => void): DebugSelectorComponent {
|
|
360
|
+
const selector = new DebugSelectorComponent(ctx, done);
|
|
361
|
+
return selector;
|
|
362
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU and heap profiling wrappers for debug reports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface CpuProfile {
|
|
6
|
+
data: string;
|
|
7
|
+
markdown: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProfilerSession {
|
|
11
|
+
/** Stop profiling and return the profile data */
|
|
12
|
+
stop(): Promise<CpuProfile>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** V8 CPU Profile node structure */
|
|
16
|
+
interface CpuProfileNode {
|
|
17
|
+
id: number;
|
|
18
|
+
callFrame?: {
|
|
19
|
+
functionName?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
lineNumber?: number;
|
|
22
|
+
};
|
|
23
|
+
hitCount?: number;
|
|
24
|
+
children?: number[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** V8 CPU Profile structure */
|
|
28
|
+
interface CpuProfileData {
|
|
29
|
+
nodes?: CpuProfileNode[];
|
|
30
|
+
samples?: number[];
|
|
31
|
+
timeDeltas?: number[];
|
|
32
|
+
startTime?: number;
|
|
33
|
+
endTime?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format CPU profile data as markdown for LLM analysis.
|
|
38
|
+
* Extracts top functions by self time and call counts.
|
|
39
|
+
*/
|
|
40
|
+
function formatProfileAsMarkdown(profileJson: string): string {
|
|
41
|
+
try {
|
|
42
|
+
const profile = JSON.parse(profileJson) as CpuProfileData;
|
|
43
|
+
const nodes = profile.nodes ?? [];
|
|
44
|
+
|
|
45
|
+
interface NodeInfo {
|
|
46
|
+
id: number;
|
|
47
|
+
functionName: string;
|
|
48
|
+
url: string;
|
|
49
|
+
lineNumber: number;
|
|
50
|
+
selfTime: number;
|
|
51
|
+
hitCount: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nodeMap = new Map<number, NodeInfo>();
|
|
55
|
+
for (const node of nodes) {
|
|
56
|
+
nodeMap.set(node.id, {
|
|
57
|
+
id: node.id,
|
|
58
|
+
functionName: node.callFrame?.functionName ?? "(anonymous)",
|
|
59
|
+
url: node.callFrame?.url ?? "",
|
|
60
|
+
lineNumber: node.callFrame?.lineNumber ?? 0,
|
|
61
|
+
selfTime: 0,
|
|
62
|
+
hitCount: node.hitCount ?? 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Distribute sample times to nodes
|
|
67
|
+
const samples = profile.samples ?? [];
|
|
68
|
+
const timeDeltas = profile.timeDeltas ?? [];
|
|
69
|
+
for (let i = 0; i < samples.length; i++) {
|
|
70
|
+
const nodeId = samples[i];
|
|
71
|
+
const info = nodeId !== undefined ? nodeMap.get(nodeId) : undefined;
|
|
72
|
+
const delta = timeDeltas[i] ?? 0;
|
|
73
|
+
if (info) {
|
|
74
|
+
info.selfTime += delta;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Sort by self time and get top functions
|
|
79
|
+
const sorted = Array.from(nodeMap.values())
|
|
80
|
+
.filter(n => n.selfTime > 0 && n.functionName !== "(root)" && n.functionName !== "(idle)")
|
|
81
|
+
.sort((a, b) => b.selfTime - a.selfTime)
|
|
82
|
+
.slice(0, 30);
|
|
83
|
+
|
|
84
|
+
if (sorted.length === 0) {
|
|
85
|
+
return "# CPU Profile Summary\n\nNo significant CPU activity recorded.";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const totalTime = sorted.reduce((sum, n) => sum + n.selfTime, 0);
|
|
89
|
+
|
|
90
|
+
const lines = ["# CPU Profile Summary", ""];
|
|
91
|
+
lines.push(`Total profiled time: ${(totalTime / 1000).toFixed(1)}ms`);
|
|
92
|
+
lines.push("");
|
|
93
|
+
lines.push("## Top Functions by Self Time");
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("| Function | Self Time (ms) | % | Location |");
|
|
96
|
+
lines.push("|----------|----------------|---|----------|");
|
|
97
|
+
|
|
98
|
+
for (const node of sorted) {
|
|
99
|
+
const selfMs = (node.selfTime / 1000).toFixed(1);
|
|
100
|
+
const pct = ((node.selfTime / totalTime) * 100).toFixed(1);
|
|
101
|
+
const location = node.url ? `${node.url}:${node.lineNumber}` : "-";
|
|
102
|
+
lines.push(`| ${node.functionName} | ${selfMs} | ${pct}% | ${location} |`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
} catch {
|
|
107
|
+
return "# CPU Profile Summary\n\nFailed to parse profile data.";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Start CPU profiling.
|
|
113
|
+
* Returns a session that can be stopped to get the profile data.
|
|
114
|
+
*/
|
|
115
|
+
export async function startCpuProfile(): Promise<ProfilerSession> {
|
|
116
|
+
const v8 = await import("node:v8");
|
|
117
|
+
v8.setFlagsFromString("--allow-natives-syntax");
|
|
118
|
+
|
|
119
|
+
const { Session } = await import("node:inspector/promises");
|
|
120
|
+
const session = new Session();
|
|
121
|
+
session.connect();
|
|
122
|
+
|
|
123
|
+
await session.post("Profiler.enable");
|
|
124
|
+
await session.post("Profiler.start");
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
async stop(): Promise<CpuProfile> {
|
|
128
|
+
const result = await session.post("Profiler.stop");
|
|
129
|
+
await session.post("Profiler.disable");
|
|
130
|
+
session.disconnect();
|
|
131
|
+
|
|
132
|
+
const data = JSON.stringify(result.profile, null, 2);
|
|
133
|
+
const markdown = formatProfileAsMarkdown(data);
|
|
134
|
+
|
|
135
|
+
return { data, markdown };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface HeapSnapshot {
|
|
141
|
+
data: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generate a heap snapshot.
|
|
146
|
+
* Uses Bun's built-in generateHeapSnapshot.
|
|
147
|
+
*/
|
|
148
|
+
export function generateHeapSnapshotData(): HeapSnapshot {
|
|
149
|
+
// Force GC before snapshot
|
|
150
|
+
Bun.gc(true);
|
|
151
|
+
|
|
152
|
+
// Use V8 format for Chrome DevTools compatibility
|
|
153
|
+
const snapshot = Bun.generateHeapSnapshot("v8");
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
data: snapshot,
|
|
157
|
+
};
|
|
158
|
+
}
|