@oh-my-pi/pi-coding-agent 12.4.0 → 12.5.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 +51 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +4 -1
- package/src/config/settings-schema.ts +58 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/main.ts +7 -1
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +10 -0
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +82 -9
- package/src/session/agent-session.ts +137 -29
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/task/executor.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.5.0] - 2026-02-15
|
|
6
|
+
### Breaking Changes
|
|
7
|
+
|
|
8
|
+
- Replaced `theme` setting with `theme.dark` and `theme.light` (auto-migrated)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `previewTheme()` function for non-destructive theme preview during settings browsing
|
|
13
|
+
- Added animated microphone icon with color cycling during voice recording
|
|
14
|
+
- Added support for discovering skills via symbolic links in skill directories
|
|
15
|
+
- Added `abort_and_prompt` RPC command for atomic abort-and-reprompt without race conditions ([#357](https://github.com/can1357/oh-my-pi/pull/357))
|
|
16
|
+
- Added automatic dark/light theme switching via SIGWINCH with separate `theme.dark`/`theme.light` settings, replacing the single `theme` setting ([#65](https://github.com/can1357/oh-my-pi/issues/65))
|
|
17
|
+
- Added speech-to-text (STT) feature with `Alt+H` keybinding and `/stt` slash command
|
|
18
|
+
- Added cross-platform audio recording: SoX, FFmpeg, arecord (Linux), PowerShell mciSendString (Windows fallback)
|
|
19
|
+
- Added recording tool fallback chain — automatically tries each available tool in order
|
|
20
|
+
- Added Python openai-whisper integration for transcription with automatic `pip install`
|
|
21
|
+
- Added custom WAV-to-numpy pipeline in `transcribe.py` bypassing ffmpeg dependency
|
|
22
|
+
- Added STT settings: `stt.enabled`, `stt.language`, `stt.modelName`
|
|
23
|
+
- Added STT status line segment showing recording/transcribing state
|
|
24
|
+
- Added `/stt` command with `on`, `off`, `status`, `setup` subcommands
|
|
25
|
+
- Added auto-download of recording tools (best-effort FFmpeg via winget on Windows)
|
|
26
|
+
- Added interactive debug log viewer with selection, copy, and expand/collapse controls
|
|
27
|
+
- Added inline filtering and count display to the debug log viewer
|
|
28
|
+
- Added pid filter toggle and load-older pagination controls to the debug log viewer
|
|
29
|
+
- Enabled loading older debug logs from archived files in viewer
|
|
30
|
+
- Added file hyperlinks for debug report paths in viewer
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Changed theme preview to support asynchronous theme loading with request deduplication to prevent race conditions
|
|
35
|
+
- Enhanced theme preview cancellation to restore the previously active theme instead of the last selected value
|
|
36
|
+
- Refactored file discovery to use native glob with gitignore support instead of manual directory traversal, improving performance and consistency
|
|
37
|
+
- Updated dependencies: glob to ^13.0.3, marked to ^17.0.2, puppeteer to ^24.37.3
|
|
38
|
+
- Optimized skill and file discovery using native glob (Rust ignore crate) — reduces startup time by ~80% (1254ms → 6ms for skills)
|
|
39
|
+
- Enhanced hashline reference parsing to handle prefixes like `>>>` and `>>` in line references
|
|
40
|
+
- Strengthened type safety in hashline edit formatting with defensive null checks for incomplete edits
|
|
41
|
+
- Changed STT status messages to display via state change callbacks instead of explicit status calls
|
|
42
|
+
- Changed cursor visibility behavior during voice recording to hide hardware and terminal cursors
|
|
43
|
+
|
|
44
|
+
### Removed
|
|
45
|
+
|
|
46
|
+
- Removed dedicated STT status line segment in favor of animated cursor-based feedback
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
- Fixed theme preview updates being applied out-of-order when rapidly browsing theme options
|
|
51
|
+
- Fixed skill discovery to correctly extract skill names from directory paths when frontmatter name is missing
|
|
52
|
+
- Fixed `session.abort()` not clearing `promptInFlight` flag due to microtask ordering, which blocked subsequent prompts
|
|
53
|
+
- Sanitized debug log display to strip control codes, normalize tabs, and trim width
|
|
54
|
+
|
|
5
55
|
## [12.4.0] - 2026-02-14
|
|
6
56
|
### Changed
|
|
7
57
|
|
|
@@ -62,6 +112,7 @@
|
|
|
62
112
|
- Improved error reporting in fetch tool to include HTTP status codes when URL fetching fails
|
|
63
113
|
- Fixed fetch tool to preserve actual response metadata (finalUrl, contentType) instead of defaults when requests fail
|
|
64
114
|
|
|
115
|
+
||||||| parent of a70a34c8b (fix(coding-agent/debug): Sanitized debug log rendering)
|
|
65
116
|
## [12.1.0] - 2026-02-13
|
|
66
117
|
|
|
67
118
|
### Added
|
package/docs/custom-tools.md
CHANGED
|
@@ -324,19 +324,34 @@ async execute(toolCallId, params, onUpdate, ctx, signal) {
|
|
|
324
324
|
Tools can implement `onSession` to react to session changes:
|
|
325
325
|
|
|
326
326
|
```typescript
|
|
327
|
-
|
|
328
|
-
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
type CustomToolSessionEvent =
|
|
328
|
+
| { reason: "start" | "switch" | "branch" | "tree" | "shutdown"; previousSessionFile: string | undefined }
|
|
329
|
+
| { reason: "auto_compaction_start"; trigger: "threshold" | "overflow" }
|
|
330
|
+
| {
|
|
331
|
+
reason: "auto_compaction_end";
|
|
332
|
+
result: CompactionResult | undefined;
|
|
333
|
+
aborted: boolean;
|
|
334
|
+
willRetry: boolean;
|
|
335
|
+
errorMessage?: string;
|
|
336
|
+
}
|
|
337
|
+
| { reason: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
338
|
+
| { reason: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
339
|
+
| { reason: "ttsr_triggered"; rules: Rule[] }
|
|
340
|
+
| { reason: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number };
|
|
331
341
|
```
|
|
332
342
|
|
|
333
343
|
**Reasons:**
|
|
334
|
-
|
|
335
|
-
- `start`: Initial session load on startup
|
|
344
|
+
- `start`: Initial session load (fresh start or resuming an existing session) - use to reconstruct state from session entries
|
|
336
345
|
- `switch`: User started a new session (`/new`) or switched to a different session (`/resume`)
|
|
337
346
|
- `branch`: User branched from a previous message (`/branch`)
|
|
338
347
|
- `tree`: User navigated to a different point in the session tree (`/tree`)
|
|
339
348
|
- `shutdown`: Process is exiting (Ctrl+C, Ctrl+D, or SIGTERM) - use to cleanup resources
|
|
349
|
+
- `auto_compaction_start`: Auto-compaction kicked off (`threshold` or `overflow`)
|
|
350
|
+
- `auto_compaction_end`: Auto-compaction finished (includes result/abort/error metadata)
|
|
351
|
+
- `auto_retry_start`: Automatic retry scheduled after an assistant error
|
|
352
|
+
- `auto_retry_end`: Automatic retry completed/failed/cancelled
|
|
353
|
+
- `ttsr_triggered`: Time-travel stream rule interrupted generation
|
|
354
|
+
- `todo_reminder`: Todo reminder fired with outstanding items
|
|
340
355
|
|
|
341
356
|
To check if a session is fresh (no messages), use `ctx.sessionManager.getEntries().length === 0`.
|
|
342
357
|
|
package/docs/extensions.md
CHANGED
|
@@ -499,6 +499,26 @@ pi.on("turn_end", async (event, ctx) => {
|
|
|
499
499
|
|
|
500
500
|
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
501
501
|
|
|
502
|
+
#### Runtime reliability events
|
|
503
|
+
|
|
504
|
+
Fired for internal recovery/continuation mechanics:
|
|
505
|
+
|
|
506
|
+
- `auto_compaction_start` / `auto_compaction_end`
|
|
507
|
+
- `auto_retry_start` / `auto_retry_end`
|
|
508
|
+
- `ttsr_triggered`
|
|
509
|
+
- `todo_reminder`
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
pi.on("todo_reminder", async (event, _ctx) => {
|
|
513
|
+
// event.todos, event.attempt, event.maxAttempts
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
pi.on("auto_retry_start", async (event, _ctx) => {
|
|
517
|
+
// event.attempt, event.maxAttempts, event.delayMs, event.errorMessage
|
|
518
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
|
|
502
522
|
#### context
|
|
503
523
|
|
|
504
524
|
Fired before each LLM call. Modify messages non-destructively.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.5.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -78,31 +78,31 @@
|
|
|
78
78
|
"scripts": {
|
|
79
79
|
"check": "tsgo -p tsconfig.json",
|
|
80
80
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
81
|
-
"build:binary": "cd ../.. && bun --cwd=packages/natives run embed:native && bun build --compile --define PI_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset",
|
|
81
|
+
"build:binary": "cd ../.. && bun --cwd=packages/stats scripts/generate-client-bundle.ts && bun --cwd=packages/natives run embed:native && bun build --compile --define PI_COMPILED=true --root . ./packages/coding-agent/src/cli.ts --outfile packages/coding-agent/dist/omp && bun --cwd=packages/natives run embed:native --reset && bun --cwd=packages/stats scripts/generate-client-bundle.ts --reset",
|
|
82
82
|
"generate-template": "bun scripts/generate-template.ts",
|
|
83
83
|
"test": "bun test"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@mozilla/readability": "0.6.0",
|
|
87
|
-
"@oh-my-pi/omp-stats": "12.
|
|
88
|
-
"@oh-my-pi/pi-agent-core": "12.
|
|
89
|
-
"@oh-my-pi/pi-ai": "12.
|
|
90
|
-
"@oh-my-pi/pi-natives": "12.
|
|
91
|
-
"@oh-my-pi/pi-tui": "12.
|
|
92
|
-
"@oh-my-pi/pi-utils": "12.
|
|
87
|
+
"@oh-my-pi/omp-stats": "12.5.0",
|
|
88
|
+
"@oh-my-pi/pi-agent-core": "12.5.0",
|
|
89
|
+
"@oh-my-pi/pi-ai": "12.5.0",
|
|
90
|
+
"@oh-my-pi/pi-natives": "12.5.0",
|
|
91
|
+
"@oh-my-pi/pi-tui": "12.5.0",
|
|
92
|
+
"@oh-my-pi/pi-utils": "12.5.0",
|
|
93
93
|
"@sinclair/typebox": "^0.34.48",
|
|
94
94
|
"@xterm/headless": "^6.0.0",
|
|
95
|
-
"ajv": "^8.
|
|
95
|
+
"ajv": "^8.18.0",
|
|
96
96
|
"chalk": "^5.6.2",
|
|
97
97
|
"diff": "^8.0.3",
|
|
98
98
|
"file-type": "^21.3.0",
|
|
99
|
-
"glob": "^13.0.
|
|
99
|
+
"glob": "^13.0.3",
|
|
100
100
|
"handlebars": "^4.7.8",
|
|
101
101
|
"ignore": "^7.0.5",
|
|
102
102
|
"linkedom": "^0.18.12",
|
|
103
|
-
"marked": "^17.0.
|
|
103
|
+
"marked": "^17.0.2",
|
|
104
104
|
"node-html-parser": "^7.0.2",
|
|
105
|
-
"puppeteer": "^24.37.
|
|
105
|
+
"puppeteer": "^24.37.3",
|
|
106
106
|
"smol-toml": "^1.6.0",
|
|
107
107
|
"zod": "^4.3.6"
|
|
108
108
|
},
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { $ } from "bun";
|
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { theme } from "../modes/theme/theme";
|
|
11
11
|
|
|
12
|
-
export type SetupComponent = "python";
|
|
12
|
+
export type SetupComponent = "python" | "stt";
|
|
13
13
|
|
|
14
14
|
export interface SetupCommandArgs {
|
|
15
15
|
component: SetupComponent;
|
|
@@ -19,7 +19,7 @@ export interface SetupCommandArgs {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const VALID_COMPONENTS: SetupComponent[] = ["python"];
|
|
22
|
+
const VALID_COMPONENTS: SetupComponent[] = ["python", "stt"];
|
|
23
23
|
|
|
24
24
|
const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
|
|
25
25
|
const MANAGED_PYTHON_ENV = getPythonEnvDir();
|
|
@@ -206,6 +206,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
206
206
|
case "python":
|
|
207
207
|
await handlePythonSetup(cmd.flags);
|
|
208
208
|
break;
|
|
209
|
+
case "stt":
|
|
210
|
+
await handleSttSetup(cmd.flags);
|
|
211
|
+
break;
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
|
|
@@ -292,6 +295,60 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
|
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
|
|
299
|
+
const { checkDependencies, formatDependencyStatus } = await import("../stt/setup");
|
|
300
|
+
const status = await checkDependencies();
|
|
301
|
+
|
|
302
|
+
if (flags.json) {
|
|
303
|
+
console.log(JSON.stringify(status, null, 2));
|
|
304
|
+
if (!status.recorder.available || !status.python.available || !status.whisper.available) process.exit(1);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(formatDependencyStatus(status));
|
|
309
|
+
|
|
310
|
+
if (status.recorder.available && status.python.available && status.whisper.available) {
|
|
311
|
+
console.log(chalk.green(`\n${theme.status.success} Speech-to-text is ready`));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (flags.check) {
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!status.python.available) {
|
|
320
|
+
console.error(chalk.red(`\n${theme.status.error} Python not found`));
|
|
321
|
+
console.error(chalk.dim("Install Python 3.8+ and ensure it's in your PATH"));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!status.recorder.available) {
|
|
326
|
+
console.error(chalk.yellow(`\n${theme.status.warning} No recording tool found`));
|
|
327
|
+
console.error(chalk.dim(status.recorder.installHint));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!status.whisper.available) {
|
|
331
|
+
console.log(chalk.dim(`\nInstalling openai-whisper...`));
|
|
332
|
+
const { resolvePython } = await import("../stt/transcriber");
|
|
333
|
+
const pythonCmd = resolvePython()!;
|
|
334
|
+
const result = await $`${pythonCmd} -m pip install -q openai-whisper`.nothrow();
|
|
335
|
+
if (result.exitCode !== 0) {
|
|
336
|
+
console.error(chalk.red(`\n${theme.status.error} Failed to install openai-whisper`));
|
|
337
|
+
console.error(chalk.dim("Try manually: pip install openai-whisper"));
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const recheck = await checkDependencies();
|
|
343
|
+
if (recheck.recorder.available && recheck.python.available && recheck.whisper.available) {
|
|
344
|
+
console.log(chalk.green(`\n${theme.status.success} Speech-to-text is ready`));
|
|
345
|
+
} else {
|
|
346
|
+
console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
|
|
347
|
+
console.log(formatDependencyStatus(recheck));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
295
352
|
/**
|
|
296
353
|
* Print setup command help.
|
|
297
354
|
*/
|
|
@@ -303,6 +360,7 @@ ${chalk.bold("Usage:")}
|
|
|
303
360
|
|
|
304
361
|
${chalk.bold("Components:")}
|
|
305
362
|
python Install Jupyter kernel dependencies for Python code execution
|
|
363
|
+
stt Install speech-to-text dependencies (openai-whisper, recording tools)
|
|
306
364
|
Packages: ${PYTHON_PACKAGES.join(", ")}
|
|
307
365
|
|
|
308
366
|
${chalk.bold("Options:")}
|
|
@@ -311,6 +369,8 @@ ${chalk.bold("Options:")}
|
|
|
311
369
|
|
|
312
370
|
${chalk.bold("Examples:")}
|
|
313
371
|
${APP_NAME} setup python Install Python execution dependencies
|
|
372
|
+
${APP_NAME} setup stt Install speech-to-text dependencies
|
|
373
|
+
${APP_NAME} setup stt --check Check if STT dependencies are available
|
|
314
374
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
315
375
|
`);
|
|
316
376
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Args, Command, Flags, renderCommandHelp } from "@oh-my-pi/pi-utils/cli"
|
|
|
5
5
|
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
|
6
6
|
import { initTheme } from "../modes/theme/theme";
|
|
7
7
|
|
|
8
|
-
const COMPONENTS: SetupComponent[] = ["python"];
|
|
8
|
+
const COMPONENTS: SetupComponent[] = ["python", "stt"];
|
|
9
9
|
|
|
10
10
|
export default class Setup extends Command {
|
|
11
11
|
static description = "Install dependencies for optional features";
|
|
@@ -35,7 +35,8 @@ export type AppAction =
|
|
|
35
35
|
| "newSession"
|
|
36
36
|
| "tree"
|
|
37
37
|
| "fork"
|
|
38
|
-
| "resume"
|
|
38
|
+
| "resume"
|
|
39
|
+
| "toggleSTT";
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* All configurable actions.
|
|
@@ -74,6 +75,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
|
|
|
74
75
|
tree: [],
|
|
75
76
|
fork: [],
|
|
76
77
|
resume: [],
|
|
78
|
+
toggleSTT: "alt+h",
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
/**
|
|
@@ -107,6 +109,7 @@ const APP_ACTIONS: AppAction[] = [
|
|
|
107
109
|
"tree",
|
|
108
110
|
"fork",
|
|
109
111
|
"resume",
|
|
112
|
+
"toggleSTT",
|
|
110
113
|
];
|
|
111
114
|
|
|
112
115
|
function isAppAction(action: string): action is AppAction {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The Settings singleton provides type-safe path-based access:
|
|
9
9
|
* settings.get("compaction.enabled") // => boolean
|
|
10
|
-
* settings.set("theme", "
|
|
10
|
+
* settings.set("theme.dark", "titanium") // sync, saves in background
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -141,10 +141,25 @@ export const SETTINGS_SCHEMA = {
|
|
|
141
141
|
// Top-level settings
|
|
142
142
|
// ─────────────────────────────────────────────────────────────────────────
|
|
143
143
|
lastChangelogVersion: { type: "string", default: undefined },
|
|
144
|
-
theme: {
|
|
144
|
+
"theme.dark": {
|
|
145
145
|
type: "string",
|
|
146
|
-
default:
|
|
147
|
-
ui: {
|
|
146
|
+
default: "titanium",
|
|
147
|
+
ui: {
|
|
148
|
+
tab: "display",
|
|
149
|
+
label: "Dark theme",
|
|
150
|
+
description: "Theme used when terminal has dark background",
|
|
151
|
+
submenu: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"theme.light": {
|
|
155
|
+
type: "string",
|
|
156
|
+
default: "light",
|
|
157
|
+
ui: {
|
|
158
|
+
tab: "display",
|
|
159
|
+
label: "Light theme",
|
|
160
|
+
description: "Theme used when terminal has light background",
|
|
161
|
+
submenu: true,
|
|
162
|
+
},
|
|
148
163
|
},
|
|
149
164
|
symbolPreset: {
|
|
150
165
|
type: "enum",
|
|
@@ -756,6 +771,36 @@ export const SETTINGS_SCHEMA = {
|
|
|
756
771
|
},
|
|
757
772
|
},
|
|
758
773
|
|
|
774
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
775
|
+
// STT settings
|
|
776
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
777
|
+
"stt.enabled": {
|
|
778
|
+
type: "boolean",
|
|
779
|
+
default: false,
|
|
780
|
+
ui: { tab: "input", label: "Speech-to-text", description: "Enable speech-to-text input via microphone" },
|
|
781
|
+
},
|
|
782
|
+
"stt.language": {
|
|
783
|
+
type: "string",
|
|
784
|
+
default: "en",
|
|
785
|
+
ui: {
|
|
786
|
+
tab: "input",
|
|
787
|
+
label: "STT language",
|
|
788
|
+
description: "Language code for transcription (e.g., en, es, fr)",
|
|
789
|
+
submenu: true,
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
"stt.modelName": {
|
|
793
|
+
type: "enum",
|
|
794
|
+
values: ["tiny", "tiny.en", "base", "base.en", "small", "small.en", "medium", "medium.en", "large"] as const,
|
|
795
|
+
default: "base.en",
|
|
796
|
+
ui: {
|
|
797
|
+
tab: "input",
|
|
798
|
+
label: "STT model",
|
|
799
|
+
description: "Whisper model size (larger = more accurate but slower)",
|
|
800
|
+
submenu: true,
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
|
|
759
804
|
// ─────────────────────────────────────────────────────────────────────────
|
|
760
805
|
// Edit settings
|
|
761
806
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1061,6 +1106,14 @@ export interface ThinkingBudgetsSettings {
|
|
|
1061
1106
|
high: number;
|
|
1062
1107
|
}
|
|
1063
1108
|
|
|
1109
|
+
export interface SttSettings {
|
|
1110
|
+
enabled: boolean;
|
|
1111
|
+
language: string | undefined;
|
|
1112
|
+
modelName: string;
|
|
1113
|
+
whisperPath: string | undefined;
|
|
1114
|
+
modelPath: string | undefined;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1064
1117
|
export interface BashInterceptorRule {
|
|
1065
1118
|
pattern: string;
|
|
1066
1119
|
flags?: string;
|
|
@@ -1081,6 +1134,7 @@ export interface GroupTypeMap {
|
|
|
1081
1134
|
exa: ExaSettings;
|
|
1082
1135
|
statusLine: StatusLineSettings;
|
|
1083
1136
|
thinkingBudgets: ThinkingBudgetsSettings;
|
|
1137
|
+
stt: SttSettings;
|
|
1084
1138
|
modelRoles: Record<string, string>;
|
|
1085
1139
|
}
|
|
1086
1140
|
|
package/src/config/settings.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import { settings } from "./settings";
|
|
6
6
|
*
|
|
7
7
|
* const enabled = settings.get("compaction.enabled"); // sync read
|
|
8
|
-
* settings.set("theme", "
|
|
8
|
+
* settings.set("theme.dark", "titanium"); // sync write, saves in background
|
|
9
9
|
*
|
|
10
10
|
* For tests:
|
|
11
11
|
* const isolated = Settings.isolated({ "compaction.enabled": false });
|
|
@@ -19,7 +19,7 @@ import { YAML } from "bun";
|
|
|
19
19
|
import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
|
|
20
20
|
import type { ModelRole } from "../config/model-registry";
|
|
21
21
|
import { loadCapability } from "../discovery";
|
|
22
|
-
import { setColorBlindMode, setSymbolPreset
|
|
22
|
+
import { isLightTheme, setAutoThemeMapping, setColorBlindMode, setSymbolPreset } from "../modes/theme/theme";
|
|
23
23
|
import { type EditMode, normalizeEditMode } from "../patch";
|
|
24
24
|
import { AgentStorage } from "../session/agent-storage";
|
|
25
25
|
import { withFileLock } from "./file-lock";
|
|
@@ -81,7 +81,7 @@ export interface SettingsOptions {
|
|
|
81
81
|
/**
|
|
82
82
|
* Parse a dotted path into segments.
|
|
83
83
|
* "compaction.enabled" → ["compaction", "enabled"]
|
|
84
|
-
* "theme" → ["theme"]
|
|
84
|
+
* "theme.dark" → ["theme", "dark"]
|
|
85
85
|
*/
|
|
86
86
|
function parsePath(path: string): string[] {
|
|
87
87
|
return path.split(".");
|
|
@@ -531,6 +531,19 @@ export class Settings {
|
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
+
// Migrate old flat "theme" string to nested theme.dark/theme.light
|
|
535
|
+
if (typeof raw.theme === "string") {
|
|
536
|
+
const oldTheme = raw.theme;
|
|
537
|
+
if (oldTheme === "light" || oldTheme === "dark") {
|
|
538
|
+
// Built-in defaults — just remove, let new defaults apply
|
|
539
|
+
delete raw.theme;
|
|
540
|
+
} else {
|
|
541
|
+
// Custom theme — detect luminance to place in correct slot
|
|
542
|
+
const slot = isLightTheme(oldTheme) ? "light" : "dark";
|
|
543
|
+
raw.theme = { [slot]: oldTheme };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
534
547
|
return raw;
|
|
535
548
|
}
|
|
536
549
|
|
|
@@ -628,13 +641,14 @@ export class Settings {
|
|
|
628
641
|
type SettingHook<P extends SettingPath> = (value: SettingValue<P>, prev: SettingValue<P>) => void;
|
|
629
642
|
|
|
630
643
|
const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
631
|
-
theme: value => {
|
|
632
|
-
// Theme loading is async, but we call it synchronously here.
|
|
633
|
-
// The hook fires immediately, and the theme system handles async loading internally.
|
|
644
|
+
"theme.dark": value => {
|
|
634
645
|
if (typeof value === "string") {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
setAutoThemeMapping("dark", value);
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
"theme.light": value => {
|
|
650
|
+
if (typeof value === "string") {
|
|
651
|
+
setAutoThemeMapping("light", value);
|
|
638
652
|
}
|
|
639
653
|
},
|
|
640
654
|
symbolPreset: value => {
|
package/src/debug/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Provides tools for debugging, bug report generation, and system diagnostics.
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as url from "node:url";
|
|
7
8
|
import { getWorkProfile } from "@oh-my-pi/pi-natives";
|
|
8
9
|
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
9
10
|
import { getSessionsDir } from "@oh-my-pi/pi-utils/dirs";
|
|
@@ -11,8 +12,9 @@ import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
|
11
12
|
import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
|
|
12
13
|
import type { InteractiveModeContext } from "../modes/types";
|
|
13
14
|
import { openPath } from "../utils/open";
|
|
15
|
+
import { DebugLogViewerComponent } from "./log-viewer";
|
|
14
16
|
import { generateHeapSnapshotData, type ProfilerSession, startCpuProfile } from "./profiler";
|
|
15
|
-
import { clearArtifactCache, createReportBundle, getArtifactCacheStats
|
|
17
|
+
import { clearArtifactCache, createDebugLogSource, createReportBundle, getArtifactCacheStats } from "./report-bundle";
|
|
16
18
|
import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
|
17
19
|
|
|
18
20
|
/** Debug menu options */
|
|
@@ -27,6 +29,11 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
27
29
|
{ value: "clear-cache", label: "Clear: artifact cache", description: "Remove old session artifacts" },
|
|
28
30
|
];
|
|
29
31
|
|
|
32
|
+
const formatFileHyperlink = (path: string): string => {
|
|
33
|
+
const fileUrl = url.pathToFileURL(path).href;
|
|
34
|
+
return `\x1b]8;;${fileUrl}\x07${path}\x1b]8;;\x07`;
|
|
35
|
+
};
|
|
36
|
+
|
|
30
37
|
/**
|
|
31
38
|
* Debug selector component.
|
|
32
39
|
*/
|
|
@@ -159,7 +166,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
159
166
|
this.ctx.chatContainer.addChild(
|
|
160
167
|
new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0),
|
|
161
168
|
);
|
|
162
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
169
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
163
170
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
164
171
|
} catch (err) {
|
|
165
172
|
loader.stop();
|
|
@@ -220,7 +227,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
220
227
|
this.ctx.chatContainer.addChild(
|
|
221
228
|
new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0),
|
|
222
229
|
);
|
|
223
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
230
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
224
231
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
225
232
|
} catch (err) {
|
|
226
233
|
loader.stop();
|
|
@@ -259,7 +266,7 @@ export class DebugSelectorComponent extends Container {
|
|
|
259
266
|
this.ctx.chatContainer.addChild(
|
|
260
267
|
new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0),
|
|
261
268
|
);
|
|
262
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", result.path), 1, 0));
|
|
269
|
+
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
|
|
263
270
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
|
|
264
271
|
} catch (err) {
|
|
265
272
|
loader.stop();
|
|
@@ -272,26 +279,26 @@ export class DebugSelectorComponent extends Container {
|
|
|
272
279
|
|
|
273
280
|
async #handleViewLogs(): Promise<void> {
|
|
274
281
|
try {
|
|
275
|
-
const
|
|
276
|
-
|
|
282
|
+
const logSource = await createDebugLogSource();
|
|
283
|
+
const logs = await logSource.getInitialText();
|
|
284
|
+
if (!logs && !logSource.hasOlderLogs()) {
|
|
277
285
|
this.ctx.showWarning("No log entries found for today.");
|
|
278
286
|
return;
|
|
279
287
|
}
|
|
280
288
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
289
|
+
const viewer = new DebugLogViewerComponent({
|
|
290
|
+
logs,
|
|
291
|
+
terminalRows: this.ctx.ui.terminal.rows,
|
|
292
|
+
onExit: () => this.ctx.showDebugSelector(),
|
|
293
|
+
onStatus: message => this.ctx.showStatus(message, { dim: true }),
|
|
294
|
+
onError: message => this.ctx.showError(message),
|
|
295
|
+
onUpdate: () => this.ctx.ui.requestRender(),
|
|
296
|
+
logSource,
|
|
297
|
+
});
|
|
293
298
|
|
|
294
|
-
this.ctx.
|
|
299
|
+
this.ctx.editorContainer.clear();
|
|
300
|
+
this.ctx.editorContainer.addChild(viewer);
|
|
301
|
+
this.ctx.ui.setFocus(viewer);
|
|
295
302
|
} catch (err) {
|
|
296
303
|
this.ctx.showError(`Failed to read logs: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
304
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
3
|
+
|
|
4
|
+
export function formatDebugLogLine(line: string, maxWidth: number): string {
|
|
5
|
+
const sanitized = sanitizeText(line);
|
|
6
|
+
const normalized = replaceTabs(sanitized);
|
|
7
|
+
const width = Math.max(1, maxWidth);
|
|
8
|
+
return truncateToWidth(normalized, width);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatDebugLogExpandedLines(line: string, maxWidth: number): string[] {
|
|
12
|
+
const sanitized = sanitizeText(line);
|
|
13
|
+
const normalized = replaceTabs(sanitized);
|
|
14
|
+
const width = Math.max(1, maxWidth);
|
|
15
|
+
|
|
16
|
+
if (normalized.length === 0) {
|
|
17
|
+
return [""];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalized
|
|
21
|
+
.split("\n")
|
|
22
|
+
.flatMap(segment => Bun.wrapAnsi(segment, width, { hard: true, trim: false, wordWrap: true }).split("\n"));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseDebugLogTimestampMs(line: string): number | undefined {
|
|
26
|
+
try {
|
|
27
|
+
const parsed: unknown = JSON.parse(line);
|
|
28
|
+
if (!parsed || typeof parsed !== "object") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const timestamp = (parsed as { timestamp?: unknown }).timestamp;
|
|
33
|
+
if (typeof timestamp !== "string") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const timestampMs = Date.parse(timestamp);
|
|
38
|
+
return Number.isFinite(timestampMs) ? timestampMs : undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseDebugLogPid(line: string): number | undefined {
|
|
45
|
+
try {
|
|
46
|
+
const parsed: unknown = JSON.parse(line);
|
|
47
|
+
if (!parsed || typeof parsed !== "object") {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const pid = (parsed as { pid?: unknown }).pid;
|
|
52
|
+
if (typeof pid !== "number") {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Number.isFinite(pid) ? pid : undefined;
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|