@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.1
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 +35 -0
- package/dist/types/capability/rule-buckets.d.ts +30 -0
- package/dist/types/capability/rule.d.ts +7 -0
- package/dist/types/cli/completion-gen.d.ts +80 -0
- package/dist/types/commands/complete.d.ts +6 -0
- package/dist/types/commands/completions.d.ts +13 -0
- package/dist/types/commands/setup.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +170 -10
- package/dist/types/discovery/builtin-defaults.d.ts +1 -0
- package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
- package/dist/types/edit/hashline/index.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +3 -0
- package/dist/types/eval/py/runtime.d.ts +11 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/segment-track.d.ts +22 -0
- package/dist/types/modes/components/welcome.d.ts +21 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -2
- package/dist/types/modes/setup-wizard/index.d.ts +16 -0
- package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
- package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
- package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
- package/dist/types/modes/theme/shimmer.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +11 -0
- package/dist/types/modes/types.d.ts +5 -1
- package/dist/types/tiny/device.d.ts +78 -0
- package/dist/types/tiny/dtype.d.ts +85 -0
- package/dist/types/tiny/models.d.ts +6 -6
- package/dist/types/tiny/text.d.ts +15 -0
- package/dist/types/tiny/title-client.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/tui/output-block.d.ts +17 -0
- package/package.json +9 -9
- package/src/capability/rule-buckets.ts +64 -0
- package/src/capability/rule.ts +8 -0
- package/src/cli/completion-gen.ts +550 -0
- package/src/cli/setup-cli.ts +5 -3
- package/src/cli-commands.ts +2 -0
- package/src/cli.ts +1 -7
- package/src/commands/complete.ts +66 -0
- package/src/commands/completions.ts +60 -0
- package/src/commands/setup.ts +29 -4
- package/src/config/settings-schema.ts +70 -11
- package/src/discovery/builtin-defaults.ts +39 -0
- package/src/discovery/builtin-rules/index.ts +48 -0
- package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
- package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
- package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
- package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
- package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
- package/src/discovery/builtin-rules/rs-result-type.md +19 -0
- package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
- package/src/discovery/builtin-rules/ts-import-type.md +42 -0
- package/src/discovery/builtin-rules/ts-no-any.md +56 -0
- package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
- package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
- package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
- package/src/discovery/builtin-rules/ts-set-map.md +28 -0
- package/src/discovery/index.ts +1 -0
- package/src/edit/hashline/block-resolver.ts +14 -0
- package/src/edit/hashline/diff.ts +4 -1
- package/src/edit/hashline/execute.ts +2 -1
- package/src/edit/hashline/index.ts +1 -0
- package/src/eval/py/kernel.ts +37 -15
- package/src/eval/py/runtime.ts +57 -28
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/export/ttsr.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +7 -8
- package/src/main.ts +18 -1
- package/src/modes/components/hook-selector.ts +15 -17
- package/src/modes/components/index.ts +1 -0
- package/src/modes/components/segment-track.ts +52 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +5 -1
- package/src/modes/components/welcome.ts +47 -42
- package/src/modes/controllers/input-controller.ts +12 -21
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/setup-wizard/index.ts +88 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
- package/src/modes/setup-wizard/scenes/outro.ts +35 -0
- package/src/modes/setup-wizard/scenes/providers.ts +69 -0
- package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
- package/src/modes/setup-wizard/scenes/splash.ts +201 -0
- package/src/modes/setup-wizard/scenes/theme.ts +299 -0
- package/src/modes/setup-wizard/scenes/types.ts +48 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
- package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
- package/src/modes/theme/shimmer.ts +5 -0
- package/src/modes/theme/theme.ts +44 -20
- package/src/modes/types.ts +6 -1
- package/src/prompts/system/orchestrate-notice.md +1 -1
- package/src/prompts/tools/read.md +4 -0
- package/src/sdk.ts +5 -15
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/tiny/device.ts +117 -0
- package/src/tiny/dtype.ts +101 -0
- package/src/tiny/models.ts +7 -6
- package/src/tiny/text.ts +36 -1
- package/src/tiny/title-client.ts +58 -3
- package/src/tiny/worker.ts +93 -29
- package/src/tools/bash.ts +16 -13
- package/src/tools/eval.ts +9 -4
- package/src/tools/index.ts +0 -11
- package/src/tools/read.ts +1 -0
- package/src/tools/renderers.ts +0 -2
- package/src/tui/code-cell.ts +6 -1
- package/src/tui/output-block.ts +199 -38
- package/dist/types/tools/recipe/index.d.ts +0 -46
- package/dist/types/tools/recipe/render.d.ts +0 -36
- package/dist/types/tools/recipe/runner.d.ts +0 -60
- package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
- package/dist/types/tools/recipe/runners/index.d.ts +0 -2
- package/dist/types/tools/recipe/runners/just.d.ts +0 -2
- package/dist/types/tools/recipe/runners/make.d.ts +0 -2
- package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
- package/dist/types/tools/recipe/runners/task.d.ts +0 -2
- package/src/prompts/tools/recipe.md +0 -16
- package/src/tools/recipe/index.ts +0 -81
- package/src/tools/recipe/render.ts +0 -19
- package/src/tools/recipe/runner.ts +0 -219
- package/src/tools/recipe/runners/cargo.ts +0 -131
- package/src/tools/recipe/runners/index.ts +0 -8
- package/src/tools/recipe/runners/just.ts +0 -73
- package/src/tools/recipe/runners/make.ts +0 -101
- package/src/tools/recipe/runners/pkg.ts +0 -167
- package/src/tools/recipe/runners/task.ts +0 -72
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use bare `catch {` when the error binding is unused
|
|
3
|
+
condition: "catch \\(_"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use bare `catch {}` when the caught value is unused. An underscore-prefixed binding adds noise and still allocates a local name.
|
|
8
|
+
|
|
9
|
+
## Replace
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Bad
|
|
13
|
+
try {
|
|
14
|
+
await loadConfig();
|
|
15
|
+
} catch (_err) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Good
|
|
20
|
+
try {
|
|
21
|
+
await loadConfig();
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Keep a real name when used
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
try {
|
|
31
|
+
await saveConfig();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error("save failed", { err });
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Unused error? Bare `catch`. Used error? Name it for what it carries.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Use `import type`, not `import('pkg').Type` in type positions"
|
|
3
|
+
condition: "import\\("
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use top-level `import type` declarations for type-only dependencies. NEVER write `import("pkg").Type` inside source annotations.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Top-level imports expose dependencies immediately.
|
|
12
|
+
- Import sorting and deduplication can manage them.
|
|
13
|
+
- Signatures stay readable and reviewable.
|
|
14
|
+
- Re-exports do not inherit noisy inline paths.
|
|
15
|
+
|
|
16
|
+
## Avoid
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Bad — inline imports hide dependencies in signatures.
|
|
20
|
+
function run(client: import("some-sdk").Client, input: import("zod/v4").infer<Schema>): Promise<Output>;
|
|
21
|
+
|
|
22
|
+
// Bad — annotations become path dumps.
|
|
23
|
+
const options: import("some-sdk/config").ClientOptions = { ... };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import type { Client } from "some-sdk";
|
|
30
|
+
import type { ClientOptions } from "some-sdk/config";
|
|
31
|
+
import type { infer as Infer } from "zod/v4";
|
|
32
|
+
|
|
33
|
+
function run(client: Client, input: Infer<Schema>): Promise<Output>;
|
|
34
|
+
const options: ClientOptions = { ... };
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Exceptions
|
|
38
|
+
|
|
39
|
+
- Ambient `.d.ts` globals that must not become modules.
|
|
40
|
+
- Generated files whose generator owns import management.
|
|
41
|
+
|
|
42
|
+
In normal `.ts` / `.tsx` source, use `import type`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Never use `any` in TypeScript annotations or assertions — use `unknown`, generics, or the actual type"
|
|
3
|
+
condition: ": any|as any"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Never use `: any` or `as any`. They disable type checking exactly where the boundary needs precision.
|
|
8
|
+
|
|
9
|
+
## Use instead
|
|
10
|
+
|
|
11
|
+
- `unknown` for unvalidated input.
|
|
12
|
+
- A domain type when the shape is known.
|
|
13
|
+
- A generic when the caller supplies the shape.
|
|
14
|
+
- A type guard when runtime checks establish shape.
|
|
15
|
+
- `satisfies` for object literals that must match a contract.
|
|
16
|
+
|
|
17
|
+
## Parameters and returns
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// Bad
|
|
21
|
+
function readId(value: any): any {
|
|
22
|
+
return value.id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Good — validate unknown input.
|
|
26
|
+
function readId(value: unknown): string | undefined {
|
|
27
|
+
if (value && typeof value === "object" && "id" in value) {
|
|
28
|
+
const candidate = (value as { id: unknown }).id;
|
|
29
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Assertions
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Bad
|
|
38
|
+
const root = document.getElementById("root") as any;
|
|
39
|
+
root.innerText = "ready";
|
|
40
|
+
|
|
41
|
+
// Good
|
|
42
|
+
const root = document.getElementById("root") as HTMLElement | null;
|
|
43
|
+
root?.innerText = "ready";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Object literals
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// Bad
|
|
50
|
+
const config = { port: 3000 } as any as ServerConfig;
|
|
51
|
+
|
|
52
|
+
// Good
|
|
53
|
+
const config = { port: 3000 } satisfies ServerConfig;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If a library boundary truly requires an unchecked cast, use `as unknown as T` with a short reason. Never leave a bare `any`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Do not use `await import()` — use static imports unless dynamic loading is unavoidable"
|
|
3
|
+
condition: "await import\\("
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use static imports for modules known at author time. Reach for `await import()` only when the module specifier is genuinely runtime-selected.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Static imports fail during build, not under load.
|
|
12
|
+
- Bundlers, type checkers, and tree shakers see them.
|
|
13
|
+
- The dependency graph remains reviewable.
|
|
14
|
+
- Consumers keep precise module types without casts.
|
|
15
|
+
|
|
16
|
+
## Avoid
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Bad — the module path is a literal.
|
|
20
|
+
const { createClient } = await import("some-sdk");
|
|
21
|
+
|
|
22
|
+
// Bad — dynamic import followed by a shape assertion.
|
|
23
|
+
const mod = (await import("./known-module")) as { run?: unknown };
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { createClient } from "some-sdk";
|
|
30
|
+
import { run } from "./known-module";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Exceptions
|
|
34
|
+
|
|
35
|
+
- Plugin loading from a runtime registry.
|
|
36
|
+
- Platform-specific modules that do not exist everywhere.
|
|
37
|
+
- Test cases that intentionally exercise module loading boundaries.
|
|
38
|
+
|
|
39
|
+
Exception? Add a short comment naming why static import cannot work.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Do not use `ReturnType<typeof fn>` — name the type explicitly"
|
|
3
|
+
condition: "ReturnType<"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Do not publish contracts through `ReturnType<typeof fn>`. Name the type at the module that owns the value and import that name at consumers.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Named types document the contract directly.
|
|
12
|
+
- Consumers stop coupling to implementation helpers.
|
|
13
|
+
- JSDoc and changelog notes attach to the exported type.
|
|
14
|
+
- Type errors point at the intended API boundary.
|
|
15
|
+
|
|
16
|
+
## Avoid
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// Bad — opaque and coupled to implementation names.
|
|
20
|
+
type Config = Awaited<ReturnType<typeof loadConfig>>;
|
|
21
|
+
type Message = ReturnType<typeof buildMessage>["message"];
|
|
22
|
+
let service: ReturnType<typeof createService> | undefined;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Use
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// In the module that owns the function:
|
|
29
|
+
export interface LoadedConfig {
|
|
30
|
+
path: string;
|
|
31
|
+
values: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function loadConfig(path: string): Promise<LoadedConfig> { ... }
|
|
35
|
+
|
|
36
|
+
// At the consumer:
|
|
37
|
+
import type { LoadedConfig } from "./config";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Exceptions
|
|
41
|
+
|
|
42
|
+
- Timer handles: `ReturnType<typeof setTimeout>` / `setInterval`.
|
|
43
|
+
- Generic type utilities where the function is a type parameter.
|
|
44
|
+
|
|
45
|
+
Concrete function? Export a concrete type.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Do not extract 1-2 line functions that only wrap an expression — inline them"
|
|
3
|
+
condition: "\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
interruptMode: never
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Do not extract a function whose whole body is one expression or one `return`. Inline it unless the name creates a durable contract.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
- One-line wrappers hide no real behavior.
|
|
13
|
+
- Readers must jump to verify trivial code.
|
|
14
|
+
- The signature freezes a shape too early.
|
|
15
|
+
- Search and type flow work better with inline expressions.
|
|
16
|
+
|
|
17
|
+
## Avoid
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// Bad — pure rename, no behavior added.
|
|
21
|
+
function isEmpty(value: string): boolean {
|
|
22
|
+
return value.length === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getDisplayName = (user: User) => user.profile.displayName;
|
|
26
|
+
|
|
27
|
+
function double(value: number) {
|
|
28
|
+
return value * 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isEmpty(name)) { ... }
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
if (name.length === 0) { ... }
|
|
38
|
+
const displayName = user.profile.displayName;
|
|
39
|
+
const doubled = value * 2;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Allowed tiny functions
|
|
43
|
+
|
|
44
|
+
- Three or more call sites need lockstep behavior.
|
|
45
|
+
- Exported name represents a stable domain concept.
|
|
46
|
+
- Callback identity matters.
|
|
47
|
+
- Type guard preserves narrowing.
|
|
48
|
+
- Public API, test seam, or DI boundary needs indirection.
|
|
49
|
+
|
|
50
|
+
If none apply, inline it.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use Promise.withResolvers() instead of new Promise() constructor
|
|
3
|
+
condition: "new Promise\\("
|
|
4
|
+
scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use `Promise.withResolvers()` instead of `new Promise((resolve, reject) => ...)`. It keeps control flow linear and exposes typed resolver functions without callback nesting.
|
|
8
|
+
|
|
9
|
+
## Basic operation
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Bad
|
|
13
|
+
function delay(ms: number): Promise<void> {
|
|
14
|
+
return new Promise(resolve => {
|
|
15
|
+
setTimeout(resolve, ms);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Good
|
|
20
|
+
function delay(ms: number): Promise<void> {
|
|
21
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
22
|
+
setTimeout(resolve, ms);
|
|
23
|
+
return promise;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Event-based completion
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Bad
|
|
31
|
+
function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown> {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
emitter.once(event, resolve);
|
|
34
|
+
emitter.once("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Good
|
|
39
|
+
function waitForEvent(emitter: EventEmitter, event: string): Promise<unknown> {
|
|
40
|
+
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
41
|
+
emitter.once(event, resolve);
|
|
42
|
+
emitter.once("error", reject);
|
|
43
|
+
return promise;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Stored resolver
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
class Gate {
|
|
51
|
+
#promise: Promise<void>;
|
|
52
|
+
#resolve: () => void;
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
56
|
+
this.#promise = promise;
|
|
57
|
+
this.#resolve = resolve;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
open(): void { this.#resolve(); }
|
|
61
|
+
wait(): Promise<void> { return this.#promise; }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use the constructor only when an API specifically requires the executor form.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prefer Record<K, V> for small static literals; use Set/Map for anything dynamic
|
|
3
|
+
condition: "\\bnew\\s+(Set|Map)\\b"
|
|
4
|
+
scope: "tool:edit(**/*.{ts,tsx}), tool:write(**/*.{ts,tsx})"
|
|
5
|
+
interruptMode: never
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Use `Record<K, V>` / `Record<K, true>` for small, static string-keyed lookup tables.
|
|
9
|
+
|
|
10
|
+
Use `Set` / `Map` when keys are dynamic, non-string, inserted or deleted at runtime, or when code needs `.size`, `.clear()`, stable insertion order, or iterator APIs.
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Static literal → Record
|
|
14
|
+
const LABEL_BY_KIND: Record<string, string> = {
|
|
15
|
+
text: "Text",
|
|
16
|
+
json: "JSON",
|
|
17
|
+
binary: "Binary",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Dynamic membership → Set
|
|
21
|
+
const seen = new Set<string>();
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
if (seen.has(item.id)) continue;
|
|
24
|
+
seen.add(item.id);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Small fixed table? `Record`. Runtime collection? `Set` / `Map`.
|
package/src/discovery/index.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter-backed {@link BlockResolver} for the hashline `replace block N:`
|
|
3
|
+
* operator. Bridges the pure hashline seam to the native `blockRangeAt`
|
|
4
|
+
* primitive in `@oh-my-pi/pi-natives`, which infers the language from the file
|
|
5
|
+
* path and returns the 1-indexed line span of the syntactic block beginning on
|
|
6
|
+
* the requested line (or `null` when none can be resolved).
|
|
7
|
+
*/
|
|
8
|
+
import type { BlockResolver } from "@oh-my-pi/hashline";
|
|
9
|
+
import { blockRangeAt } from "@oh-my-pi/pi-natives";
|
|
10
|
+
|
|
11
|
+
export const nativeBlockResolver: BlockResolver = ({ path, text, line }) => {
|
|
12
|
+
const range = blockRangeAt({ code: text, path, line });
|
|
13
|
+
return range ? { start: range.startLine, end: range.endLine } : null;
|
|
14
|
+
};
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
23
23
|
import { generateDiffString } from "../diff";
|
|
24
24
|
import { readEditFileText } from "../read-file";
|
|
25
|
+
import { nativeBlockResolver } from "./block-resolver";
|
|
25
26
|
|
|
26
27
|
export interface HashlineDiffOptions {
|
|
27
28
|
/**
|
|
@@ -74,7 +75,9 @@ export async function computeHashlineSectionDiff(
|
|
|
74
75
|
const normalized = normalizeToLF(content);
|
|
75
76
|
const hashError = validateSectionHash(section, absolutePath, normalized, snapshots);
|
|
76
77
|
if (hashError) return { error: hashError };
|
|
77
|
-
const result = options.streaming
|
|
78
|
+
const result = options.streaming
|
|
79
|
+
? section.applyPartialTo(normalized, nativeBlockResolver)
|
|
80
|
+
: section.applyTo(normalized, nativeBlockResolver);
|
|
78
81
|
if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
|
|
79
82
|
return generateDiffString(normalized, result.text);
|
|
80
83
|
} catch (err) {
|
|
@@ -25,6 +25,7 @@ import { outputMeta } from "../../tools/output-meta";
|
|
|
25
25
|
import { generateDiffString } from "../diff";
|
|
26
26
|
import { getFileSnapshotStore } from "../file-snapshot-store";
|
|
27
27
|
import type { EditToolDetails, EditToolPerFileResult, LspBatchRequest } from "../renderer";
|
|
28
|
+
import { nativeBlockResolver } from "./block-resolver";
|
|
28
29
|
import { HashlineFilesystem } from "./filesystem";
|
|
29
30
|
import { type HashlineParams, hashlineEditParamsSchema } from "./params";
|
|
30
31
|
|
|
@@ -133,7 +134,7 @@ export async function executeHashlineSingle(
|
|
|
133
134
|
batchRequest: options.batchRequest,
|
|
134
135
|
});
|
|
135
136
|
const snapshots = getFileSnapshotStore(options.session);
|
|
136
|
-
const patcher = new Patcher({ fs, snapshots });
|
|
137
|
+
const patcher = new Patcher({ fs, snapshots, blockResolver: nativeBlockResolver });
|
|
137
138
|
|
|
138
139
|
// Single-section fast path: prepare, commit, render.
|
|
139
140
|
if (patch.sections.length === 1) {
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { Settings } from "../../config/settings";
|
|
|
17
17
|
import { type KernelDisplayOutput, renderKernelDisplay } from "./display";
|
|
18
18
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
19
19
|
import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
|
|
20
|
-
import { filterEnv, resolvePythonRuntime } from "./runtime";
|
|
20
|
+
import { enumeratePythonRuntimes, filterEnv, type PythonRuntime, resolvePythonRuntime } from "./runtime";
|
|
21
21
|
|
|
22
22
|
export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
|
|
23
23
|
export { renderKernelDisplay } from "./display";
|
|
@@ -106,6 +106,8 @@ export interface PythonKernelAvailability {
|
|
|
106
106
|
ok: boolean;
|
|
107
107
|
pythonPath?: string;
|
|
108
108
|
reason?: string;
|
|
109
|
+
/** The probed-working runtime, when one was found. */
|
|
110
|
+
runtime?: PythonRuntime;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
function getRemainingTimeMs(deadlineMs?: number): number | undefined {
|
|
@@ -134,19 +136,34 @@ export async function checkPythonKernelAvailability(cwd: string): Promise<Python
|
|
|
134
136
|
const settings = await Settings.init();
|
|
135
137
|
const { env } = settings.getShellConfig();
|
|
136
138
|
const baseEnv = filterEnv(env);
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
const runtimes = enumeratePythonRuntimes(cwd, baseEnv);
|
|
140
|
+
if (runtimes.length === 0) {
|
|
141
|
+
return { ok: false, reason: "Python executable not found on PATH" };
|
|
142
|
+
}
|
|
143
|
+
// Probe each candidate in priority order and use the first that actually
|
|
144
|
+
// runs. A managed env left behind by a removed `uv` install can exist on
|
|
145
|
+
// disk yet fail to execute; falling through to the next candidate lets a
|
|
146
|
+
// working system Python take over instead of failing the whole session.
|
|
147
|
+
const failures: string[] = [];
|
|
148
|
+
for (const runtime of runtimes) {
|
|
149
|
+
try {
|
|
150
|
+
const probe = await $`${runtime.pythonPath} -c "import sys;sys.exit(0)"`
|
|
151
|
+
.quiet()
|
|
152
|
+
.nothrow()
|
|
153
|
+
.cwd(cwd)
|
|
154
|
+
.env(runtime.env);
|
|
155
|
+
if (probe.exitCode === 0) {
|
|
156
|
+
return { ok: true, pythonPath: runtime.pythonPath, runtime };
|
|
157
|
+
}
|
|
158
|
+
failures.push(`${runtime.pythonPath} (exit code ${probe.exitCode})`);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
failures.push(`${runtime.pythonPath} (${err instanceof Error ? err.message : String(err)})`);
|
|
161
|
+
}
|
|
145
162
|
}
|
|
146
163
|
return {
|
|
147
164
|
ok: false,
|
|
148
|
-
pythonPath:
|
|
149
|
-
reason: `Python interpreter
|
|
165
|
+
pythonPath: runtimes[0].pythonPath,
|
|
166
|
+
reason: `No working Python interpreter found. Tried: ${failures.join("; ")}`,
|
|
150
167
|
};
|
|
151
168
|
} catch (err) {
|
|
152
169
|
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
@@ -207,10 +224,15 @@ export class PythonKernel {
|
|
|
207
224
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
208
225
|
}
|
|
209
226
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
// Reuse the interpreter the availability probe selected so the spawned
|
|
228
|
+
// kernel matches what we verified actually runs. The fallback computes a
|
|
229
|
+
// runtime only for the skip-check fast path (test runtime /
|
|
230
|
+
// PI_PYTHON_SKIP_CHECK), where no candidate was probed.
|
|
231
|
+
let runtime = availability.runtime;
|
|
232
|
+
if (!runtime) {
|
|
233
|
+
const { env: shellEnv } = (await Settings.init()).getShellConfig();
|
|
234
|
+
runtime = resolvePythonRuntime(options.cwd, filterEnv(shellEnv));
|
|
235
|
+
}
|
|
214
236
|
const spawnEnv: Record<string, string> = {};
|
|
215
237
|
for (const [key, value] of Object.entries(runtime.env)) {
|
|
216
238
|
if (typeof value === "string") spawnEnv[key] = value;
|
package/src/eval/py/runtime.ts
CHANGED
|
@@ -162,49 +162,78 @@ export function resolveVenvPath(cwd: string): string | undefined {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
|
-
*
|
|
165
|
+
* Apply a venv-style PATH/VIRTUAL_ENV layout onto a fresh copy of `baseEnv` for
|
|
166
|
+
* the interpreter living in `binDir`.
|
|
166
167
|
*/
|
|
167
|
-
|
|
168
|
+
function applyVenvEnv(
|
|
169
|
+
baseEnv: Record<string, string | undefined>,
|
|
170
|
+
venvPath: string,
|
|
171
|
+
binDir: string,
|
|
172
|
+
): Record<string, string | undefined> {
|
|
168
173
|
const env = { ...baseEnv };
|
|
169
|
-
|
|
174
|
+
env.VIRTUAL_ENV = venvPath;
|
|
175
|
+
const pathKey = resolvePathKey(env);
|
|
176
|
+
const currentPath = env[pathKey];
|
|
177
|
+
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
|
|
178
|
+
return env;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function venvBinDir(venvPath: string): string {
|
|
182
|
+
return process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Enumerate candidate Python runtimes in priority order: an active/project venv,
|
|
187
|
+
* the managed `~/.omp/python-env`, then the system interpreter on PATH. Every
|
|
188
|
+
* candidate that physically exists is returned so callers can probe each in turn
|
|
189
|
+
* rather than committing to the first — a managed env left behind by a removed
|
|
190
|
+
* `uv` install no longer shadows a working system Python.
|
|
191
|
+
*/
|
|
192
|
+
export function enumeratePythonRuntimes(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime[] {
|
|
193
|
+
const runtimes: PythonRuntime[] = [];
|
|
194
|
+
const seen = new Set<string>();
|
|
195
|
+
const push = (runtime: PythonRuntime): void => {
|
|
196
|
+
if (seen.has(runtime.pythonPath)) return;
|
|
197
|
+
seen.add(runtime.pythonPath);
|
|
198
|
+
runtimes.push(runtime);
|
|
199
|
+
};
|
|
170
200
|
|
|
201
|
+
const venvPath = baseEnv.VIRTUAL_ENV ?? resolveVenvPath(cwd);
|
|
171
202
|
if (venvPath) {
|
|
172
|
-
|
|
173
|
-
const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
|
|
203
|
+
const binDir = venvBinDir(venvPath);
|
|
174
204
|
const pythonCandidate = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
|
|
175
205
|
if (fs.existsSync(pythonCandidate)) {
|
|
176
|
-
|
|
177
|
-
const currentPath = env[pathKey];
|
|
178
|
-
env[pathKey] = currentPath ? `${binDir}${path.delimiter}${currentPath}` : binDir;
|
|
179
|
-
return {
|
|
180
|
-
pythonPath: pythonCandidate,
|
|
181
|
-
env,
|
|
182
|
-
venvPath,
|
|
183
|
-
};
|
|
206
|
+
push({ pythonPath: pythonCandidate, env: applyVenvEnv(baseEnv, venvPath, binDir), venvPath });
|
|
184
207
|
}
|
|
185
208
|
}
|
|
186
209
|
|
|
187
210
|
const managed = resolveManagedPythonCandidate();
|
|
188
211
|
if (fs.existsSync(managed.pythonPath)) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const currentPath = env[pathKey];
|
|
192
|
-
const managedBin =
|
|
193
|
-
process.platform === "win32" ? path.join(managed.venvPath, "Scripts") : path.join(managed.venvPath, "bin");
|
|
194
|
-
env[pathKey] = currentPath ? `${managedBin}${path.delimiter}${currentPath}` : managedBin;
|
|
195
|
-
return {
|
|
212
|
+
const managedBin = path.dirname(managed.pythonPath);
|
|
213
|
+
push({
|
|
196
214
|
pythonPath: managed.pythonPath,
|
|
197
|
-
env,
|
|
215
|
+
env: applyVenvEnv(baseEnv, managed.venvPath, managedBin),
|
|
198
216
|
venvPath: managed.venvPath,
|
|
199
|
-
};
|
|
217
|
+
});
|
|
200
218
|
}
|
|
201
219
|
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
220
|
+
const systemPath = $which("python") ?? $which("python3");
|
|
221
|
+
if (systemPath) {
|
|
222
|
+
push({ pythonPath: systemPath, env: { ...baseEnv } });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return runtimes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Resolve the highest-priority Python runtime. Prefer {@link enumeratePythonRuntimes}
|
|
230
|
+
* when you can probe candidates; this returns only the first one and throws when
|
|
231
|
+
* no interpreter exists.
|
|
232
|
+
*/
|
|
233
|
+
export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string | undefined>): PythonRuntime {
|
|
234
|
+
const [runtime] = enumeratePythonRuntimes(cwd, baseEnv);
|
|
235
|
+
if (!runtime) {
|
|
204
236
|
throw new Error("Python executable not found on PATH");
|
|
205
237
|
}
|
|
206
|
-
return
|
|
207
|
-
pythonPath,
|
|
208
|
-
env,
|
|
209
|
-
};
|
|
238
|
+
return runtime;
|
|
210
239
|
}
|