@oh-my-pi/pi-coding-agent 16.4.2 → 16.4.3
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 +42 -0
- package/dist/cli.js +17514 -17799
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +10 -22
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/status-line/component.d.ts +3 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/task/executor.d.ts +31 -0
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +1 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +16 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/vibe.d.ts +161 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/dist/types/vibe/runtime.d.ts +124 -0
- package/dist/types/vibe/state.d.ts +4 -0
- package/dist/types/web/search/provider.d.ts +2 -0
- package/dist/types/web/search/providers/bing.d.ts +14 -0
- package/dist/types/web/search/providers/browser-headers.d.ts +9 -0
- package/dist/types/web/search/providers/browser-page.d.ts +32 -0
- package/dist/types/web/search/providers/ecosia.d.ts +14 -0
- package/dist/types/web/search/providers/google.d.ts +13 -0
- package/dist/types/web/search/providers/mojeek.d.ts +14 -0
- package/dist/types/web/search/providers/public.d.ts +37 -0
- package/dist/types/web/search/providers/startpage.d.ts +14 -0
- package/dist/types/web/search/providers/yahoo.d.ts +14 -0
- package/dist/types/web/search/types.d.ts +28 -0
- package/package.json +15 -18
- package/scripts/build-binary.ts +56 -73
- package/scripts/bundle-dist.ts +36 -40
- package/scripts/compile-binary.ts +68 -0
- package/scripts/generate-docs-index.ts +3 -93
- package/scripts/legacy-pi-virtual-module.ts +192 -0
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/edit/index.ts +10 -76
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +328 -162
- package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
- package/src/internal-urls/docs-index.ts +2 -1
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +65 -1
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/status-line/component.ts +6 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/interactive-mode.ts +117 -1
- package/src/modes/types.ts +2 -0
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -2
- package/src/prompts/system/vibe-mode-active.md +23 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/vibe-kill.md +3 -0
- package/src/prompts/tools/vibe-list.md +3 -0
- package/src/prompts/tools/vibe-send.md +9 -0
- package/src/prompts/tools/vibe-spawn.md +10 -0
- package/src/prompts/tools/vibe-turn-result.md +19 -0
- package/src/prompts/tools/vibe-wait.md +8 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +103 -13
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/slash-commands/builtin-registry.ts +24 -8
- package/src/task/agents.ts +0 -2
- package/src/task/executor.ts +105 -0
- package/src/tools/__tests__/vibe-render.test.ts +210 -0
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +46 -48
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +264 -129
- package/src/tools/glob.ts +20 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +17 -9
- package/src/tools/renderers.ts +6 -0
- package/src/tools/vibe.ts +608 -0
- package/src/tools/write.ts +15 -2
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +70 -7
- package/src/vibe/runtime.ts +710 -0
- package/src/vibe/state.ts +4 -0
- package/src/web/search/index.ts +14 -6
- package/src/web/search/provider.ts +37 -1
- package/src/web/search/providers/bing.ts +197 -0
- package/src/web/search/providers/browser-headers.ts +92 -0
- package/src/web/search/providers/browser-page.ts +123 -0
- package/src/web/search/providers/duckduckgo.ts +13 -34
- package/src/web/search/providers/ecosia.ts +178 -0
- package/src/web/search/providers/google.ts +193 -0
- package/src/web/search/providers/mojeek.ts +206 -0
- package/src/web/search/providers/public.ts +201 -0
- package/src/web/search/providers/startpage.ts +213 -0
- package/src/web/search/providers/yahoo.ts +179 -0
- package/src/web/search/types.ts +35 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +0 -10
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +0 -10
- package/scripts/generate-legacy-pi-bundled-registry.ts +0 -420
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +0 -1011
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +0 -3430
- package/src/internal-urls/docs-index.generated.txt +0 -2
- package/src/prompts/agents/plan.md +0 -47
|
@@ -16,7 +16,8 @@ import * as path from "node:path";
|
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
import { gunzip } from "node:zlib";
|
|
18
18
|
import { Glob } from "bun";
|
|
19
|
-
|
|
19
|
+
|
|
20
|
+
const docsEmbed = process.env.PI_DOCS_EMBED ?? "";
|
|
20
21
|
|
|
21
22
|
const gunzipAsync = promisify(gunzip);
|
|
22
23
|
|
|
@@ -72,7 +72,7 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
72
72
|
throw new Error("Path traversal is not allowed");
|
|
73
73
|
}
|
|
74
74
|
} else {
|
|
75
|
-
targetPath = skill.filePath;
|
|
75
|
+
targetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
let stats: fsTypes.Stats;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { DeferredDiagnosticsEntry, ToolSession } from "../tools";
|
|
2
|
+
import { getDiagnosticsLedger } from "./diagnostics-ledger";
|
|
3
|
+
import type { FileDiagnosticsResult, WritethroughDeferredHandle } from "./index";
|
|
4
|
+
|
|
5
|
+
/** Coordinates late LSP diagnostics for one mutation tool instance. */
|
|
6
|
+
export class DeferredDiagnostics {
|
|
7
|
+
readonly #pendingFetches = new Map<string, AbortController>();
|
|
8
|
+
readonly #fallbackVersions = new Map<string, number>();
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly session: ToolSession,
|
|
12
|
+
private readonly deduplicate: boolean,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
/** Begin a file mutation and return the handle consumed by LSP writethrough. */
|
|
16
|
+
begin(path: string): WritethroughDeferredHandle {
|
|
17
|
+
const existing = this.#pendingFetches.get(path);
|
|
18
|
+
if (existing) {
|
|
19
|
+
existing.abort();
|
|
20
|
+
this.#pendingFetches.delete(path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const mutationVersion = this.#bumpVersion(path);
|
|
25
|
+
return {
|
|
26
|
+
onDeferredDiagnostics: diagnostics => {
|
|
27
|
+
this.#pendingFetches.delete(path);
|
|
28
|
+
this.#inject(path, diagnostics, mutationVersion);
|
|
29
|
+
},
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
finalize: diagnostics => {
|
|
32
|
+
if (!diagnostics) {
|
|
33
|
+
this.#pendingFetches.set(path, controller);
|
|
34
|
+
} else {
|
|
35
|
+
controller.abort();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#inject(path: string, diagnostics: FileDiagnosticsResult, mutationVersion: number): void {
|
|
42
|
+
const effective = this.deduplicate ? getDiagnosticsLedger(this.session).reduce(path, diagnostics) : diagnostics;
|
|
43
|
+
if (this.deduplicate && effective.messages.length === 0) return;
|
|
44
|
+
|
|
45
|
+
const entry: DeferredDiagnosticsEntry = {
|
|
46
|
+
path,
|
|
47
|
+
summary: effective.summary ?? "",
|
|
48
|
+
messages: effective.messages ?? [],
|
|
49
|
+
errored: effective.errored,
|
|
50
|
+
isStale: () => this.#version(path) !== mutationVersion,
|
|
51
|
+
};
|
|
52
|
+
this.session.queueDeferredDiagnostics?.(entry);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#bumpVersion(path: string): number {
|
|
56
|
+
if (this.session.bumpFileMutationVersion) return this.session.bumpFileMutationVersion(path);
|
|
57
|
+
const next = (this.#fallbackVersions.get(path) ?? 0) + 1;
|
|
58
|
+
this.#fallbackVersions.set(path, next);
|
|
59
|
+
return next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#version(path: string): number {
|
|
63
|
+
if (this.session.getFileMutationVersion) return this.session.getFileMutationVersion(path);
|
|
64
|
+
return this.#fallbackVersions.get(path) ?? 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -78,9 +78,10 @@ import { concreteThinkingLevel, parseConfiguredThinkingLevel } from "./thinking"
|
|
|
78
78
|
import type { LspStartupServerInfo } from "./tools";
|
|
79
79
|
import {
|
|
80
80
|
getChangelogPath,
|
|
81
|
-
getNewEntries,
|
|
82
81
|
parseChangelog,
|
|
82
|
+
parseChangelogVersion,
|
|
83
83
|
readLastChangelogVersion,
|
|
84
|
+
selectStartupChangelog,
|
|
84
85
|
writeLastChangelogVersion,
|
|
85
86
|
} from "./utils/changelog";
|
|
86
87
|
import { EventBus } from "./utils/event-bus";
|
|
@@ -610,6 +611,11 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
const lastVersion = await readLastChangelogVersion();
|
|
614
|
+
const parsedLastVersion = parseChangelogVersion(lastVersion);
|
|
615
|
+
if (!parsedLastVersion) {
|
|
616
|
+
await writeLastChangelogVersion(VERSION);
|
|
617
|
+
return undefined;
|
|
618
|
+
}
|
|
613
619
|
if (lastVersion === VERSION) {
|
|
614
620
|
// Steady state: user already saw the current version's changelog. Skip the file read + parse.
|
|
615
621
|
return undefined;
|
|
@@ -617,18 +623,12 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
617
623
|
|
|
618
624
|
const changelogPath = getChangelogPath();
|
|
619
625
|
const entries = await parseChangelog(changelogPath);
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
} else {
|
|
627
|
-
const newEntries = getNewEntries(entries, lastVersion);
|
|
628
|
-
if (newEntries.length > 0) {
|
|
629
|
-
await writeLastChangelogVersion(VERSION);
|
|
630
|
-
return newEntries.map(e => e.content).join("\n\n");
|
|
631
|
-
}
|
|
626
|
+
const startupChangelog = selectStartupChangelog(entries, lastVersion, VERSION);
|
|
627
|
+
if (startupChangelog.persistCurrentVersion) {
|
|
628
|
+
await writeLastChangelogVersion(VERSION);
|
|
629
|
+
}
|
|
630
|
+
if (startupChangelog.markdown) {
|
|
631
|
+
return startupChangelog.markdown;
|
|
632
632
|
}
|
|
633
633
|
|
|
634
634
|
return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
1
|
+
import { describe, expect, it, spyOn } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import { resolveStdioSpawnCommand, StdioTransport } from "./stdio";
|
|
4
4
|
|
|
@@ -56,6 +56,50 @@ describe("resolveStdioSpawnCommand", () => {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
describe("StdioTransport.connect", () => {
|
|
60
|
+
it("passes argv as Bun.spawn's first argument and process options as the second", async () => {
|
|
61
|
+
const cwd = process.cwd();
|
|
62
|
+
const envValue = "stdio-spawn-shape";
|
|
63
|
+
const argv = [process.execPath, "-e", "process.exit(0)"];
|
|
64
|
+
const transport = new StdioTransport({
|
|
65
|
+
command: argv[0],
|
|
66
|
+
args: argv.slice(1),
|
|
67
|
+
cwd,
|
|
68
|
+
env: {
|
|
69
|
+
OMP_STDIO_SPAWN_SHAPE: envValue,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const spawnSpy = spyOn(Bun, "spawn");
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await transport.connect();
|
|
76
|
+
|
|
77
|
+
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
|
78
|
+
const call = spawnSpy.mock.calls[0];
|
|
79
|
+
if (!call) throw new Error("expected StdioTransport.connect() to spawn exactly one subprocess");
|
|
80
|
+
|
|
81
|
+
const [spawnArgv, spawnOptions] = call;
|
|
82
|
+
expect(spawnArgv).toEqual(argv);
|
|
83
|
+
expect(spawnOptions).toEqual(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
cwd,
|
|
86
|
+
detached: !(process.platform === "darwin" || process.platform === "win32"),
|
|
87
|
+
env: expect.objectContaining({
|
|
88
|
+
OMP_STDIO_SPAWN_SHAPE: envValue,
|
|
89
|
+
}),
|
|
90
|
+
stderr: "pipe",
|
|
91
|
+
stdin: "pipe",
|
|
92
|
+
stdout: "pipe",
|
|
93
|
+
windowsHide: process.platform === "win32" ? expect.any(Boolean) : undefined,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
} finally {
|
|
97
|
+
await transport.close();
|
|
98
|
+
spawnSpy.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
59
103
|
// Regression for #3945: request() awaited stdin.write/flush, so a child that
|
|
60
104
|
// stops draining stdin would park the async fn past the timeout timer and past
|
|
61
105
|
// `return promise`, orphaning the deferred rejection and hanging the caller
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { getProjectDir, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
11
|
-
import {
|
|
11
|
+
import type { Subprocess } from "bun";
|
|
12
12
|
import { hostHasInheritableConsole } from "../../eval/py/spawn-options";
|
|
13
13
|
import type {
|
|
14
14
|
JsonRpcError,
|
|
@@ -376,8 +376,11 @@ export class StdioTransport implements MCPTransport {
|
|
|
376
376
|
// macOS stays attached so TCC can prompt for Apple Events automation;
|
|
377
377
|
// Windows stays attached, and only hides the child when the host has no
|
|
378
378
|
// console to share. See `StdioSpawnCommand`.
|
|
379
|
-
this
|
|
380
|
-
|
|
379
|
+
// Keep this on Bun's argv-first overload. The eval JS kernel path that
|
|
380
|
+
// triggers macOS Apple Events TCC prompts uses the same shape; the
|
|
381
|
+
// one-object `{ cmd }` overload timed out before prompting for `mcpbridge`
|
|
382
|
+
// even with `detached: false` (#5085).
|
|
383
|
+
this.#process = Bun.spawn(spawnCommand.cmd, {
|
|
381
384
|
cwd,
|
|
382
385
|
env,
|
|
383
386
|
stdin: "pipe",
|
|
@@ -129,6 +129,15 @@ type PromptLifecycleError = Error & { readonly code: "ACP_SESSION_CLOSED" };
|
|
|
129
129
|
type PromptTurnState = {
|
|
130
130
|
cancelRequested: boolean;
|
|
131
131
|
settled: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Delivery of streamed assistant `error` chunks this turn (the mapper
|
|
134
|
+
* surfaces them as `agent_message_chunk`s). Resolves `true` once at least
|
|
135
|
+
* one error chunk reached the client — the `agent_end` error fallback in
|
|
136
|
+
* {@link AcpAgent##flushUnreportedTurnError} awaits it and stays silent on
|
|
137
|
+
* success, so a fallback racing an in-flight delivery can neither duplicate
|
|
138
|
+
* the error nor drop it when delivery fails.
|
|
139
|
+
*/
|
|
140
|
+
errorTextDelivery: Promise<boolean> | undefined;
|
|
132
141
|
/**
|
|
133
142
|
* `abort()` is in-flight (or its bounded-timeout race). `undefined` while the turn is
|
|
134
143
|
* running normally and after cleanup completes. The turn occupies `record.promptTurn`
|
|
@@ -684,6 +693,7 @@ export class AcpAgent implements Agent {
|
|
|
684
693
|
record.promptTurn = {
|
|
685
694
|
cancelRequested: false,
|
|
686
695
|
settled: false,
|
|
696
|
+
errorTextDelivery: undefined,
|
|
687
697
|
cleanup: undefined,
|
|
688
698
|
usageBaseline: this.#cloneUsageStatistics(record.session.sessionManager.getUsageStatistics()),
|
|
689
699
|
unsubscribe: undefined,
|
|
@@ -1200,6 +1210,10 @@ export class AcpAgent implements Agent {
|
|
|
1200
1210
|
imageDataCache.set(key, resolved);
|
|
1201
1211
|
return resolved;
|
|
1202
1212
|
};
|
|
1213
|
+
const streamedAssistantError =
|
|
1214
|
+
event.type === "message_update" &&
|
|
1215
|
+
event.message.role === "assistant" &&
|
|
1216
|
+
event.assistantMessageEvent.type === "error";
|
|
1203
1217
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
1204
1218
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1205
1219
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
@@ -1207,7 +1221,18 @@ export class AcpAgent implements Agent {
|
|
|
1207
1221
|
cwd: record.session.sessionManager.getCwd(),
|
|
1208
1222
|
resolveImageData: resolveImageDataForAcp,
|
|
1209
1223
|
})) {
|
|
1210
|
-
|
|
1224
|
+
const delivery = this.#connection.sessionUpdate(notification);
|
|
1225
|
+
if (streamedAssistantError) {
|
|
1226
|
+
// Resolves true only once the error chunk actually reached the
|
|
1227
|
+
// client — a failed delivery keeps the agent_end fallback armed.
|
|
1228
|
+
const outcome = delivery.then(
|
|
1229
|
+
() => true,
|
|
1230
|
+
() => false,
|
|
1231
|
+
);
|
|
1232
|
+
const prior = promptTurn.errorTextDelivery;
|
|
1233
|
+
promptTurn.errorTextDelivery = prior ? Promise.all([prior, outcome]).then(([a, b]) => a || b) : outcome;
|
|
1234
|
+
}
|
|
1235
|
+
await delivery;
|
|
1211
1236
|
}
|
|
1212
1237
|
if (event.type === "tool_execution_end") {
|
|
1213
1238
|
record.toolArgsById.delete(event.toolCallId);
|
|
@@ -1216,6 +1241,7 @@ export class AcpAgent implements Agent {
|
|
|
1216
1241
|
|
|
1217
1242
|
if (event.type === "agent_end") {
|
|
1218
1243
|
await this.#flushMissedFinalAssistantText(record, event);
|
|
1244
|
+
await this.#flushUnreportedTurnError(record, event);
|
|
1219
1245
|
await this.#emitEndOfTurnUpdates(record);
|
|
1220
1246
|
await this.#waitForAcpPromptIdle(record);
|
|
1221
1247
|
record.liveMessageId = undefined;
|
|
@@ -1272,6 +1298,44 @@ export class AcpAgent implements Agent {
|
|
|
1272
1298
|
});
|
|
1273
1299
|
}
|
|
1274
1300
|
|
|
1301
|
+
/**
|
|
1302
|
+
* Surface a turn-fatal provider error that never reached the client. A
|
|
1303
|
+
* request that fails before streaming any assistant events — e.g. GitHub
|
|
1304
|
+
* Copilot's `HTTP 400 model_not_supported` after retries — emits only
|
|
1305
|
+
* `agent_end` with an empty assistant message carrying `errorMessage`
|
|
1306
|
+
* (`Agent#runLoop`'s catch), so no `message_update`/`message_end` ever maps
|
|
1307
|
+
* to a session update and the client sees the turn end silently. Errors
|
|
1308
|
+
* that did stream are tracked via {@link PromptTurnState.errorTextDelivery};
|
|
1309
|
+
* the fallback awaits that delivery and re-sends only when it failed.
|
|
1310
|
+
*/
|
|
1311
|
+
async #flushUnreportedTurnError(
|
|
1312
|
+
record: ManagedSessionRecord,
|
|
1313
|
+
event: Extract<AgentSessionEvent, { type: "agent_end" }>,
|
|
1314
|
+
): Promise<void> {
|
|
1315
|
+
const streamedDelivery = record.promptTurn?.errorTextDelivery;
|
|
1316
|
+
if (streamedDelivery && (await streamedDelivery)) {
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const lastAssistant = [...event.messages]
|
|
1320
|
+
.reverse()
|
|
1321
|
+
.find((message): message is AssistantMessage => message.role === "assistant");
|
|
1322
|
+
if (lastAssistant?.stopReason !== "error") {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
const errorMessage = lastAssistant.errorMessage;
|
|
1326
|
+
if (!errorMessage || isSilentAbort(lastAssistant)) {
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
await this.#connection.sessionUpdate({
|
|
1330
|
+
sessionId: record.session.sessionId,
|
|
1331
|
+
update: {
|
|
1332
|
+
sessionUpdate: "agent_message_chunk",
|
|
1333
|
+
content: { type: "text", text: errorMessage },
|
|
1334
|
+
messageId: record.liveMessageId ?? crypto.randomUUID(),
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1275
1339
|
async #waitForAcpPromptIdle(record: ManagedSessionRecord): Promise<void> {
|
|
1276
1340
|
for (let pass = 0; pass < ACP_ASYNC_DELIVERY_DRAIN_MAX_PASSES; pass++) {
|
|
1277
1341
|
await record.session.waitForIdle();
|
|
@@ -284,6 +284,11 @@ function mapAssistantMessageUpdate(
|
|
|
284
284
|
case "error":
|
|
285
285
|
sessionUpdate = "agent_message_chunk";
|
|
286
286
|
text = event.assistantMessageEvent.error.errorMessage ?? "Unknown error";
|
|
287
|
+
// The surfaced error is the message's visible text: keeps the
|
|
288
|
+
// message_end / agent_end fallbacks from emitting again.
|
|
289
|
+
if (text.length > 0 && progress) {
|
|
290
|
+
progress.textEmitted = true;
|
|
291
|
+
}
|
|
287
292
|
break;
|
|
288
293
|
default:
|
|
289
294
|
return [];
|
|
@@ -14,6 +14,17 @@ export function createAcpConnection(
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export async function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never> {
|
|
17
|
+
// Humans who run `omp acp` by hand see a silent process and assume it is
|
|
18
|
+
// broken (stdout is the JSON-RPC transport, so nothing may be printed
|
|
19
|
+
// there). When stdin is a TTY no ACP client is attached — say so on stderr
|
|
20
|
+
// before the transport starts.
|
|
21
|
+
if (process.stdin.isTTY) {
|
|
22
|
+
process.stderr.write(
|
|
23
|
+
"omp acp: ACP server speaking JSON-RPC over stdio.\n" +
|
|
24
|
+
'This command is meant to be spawned by an ACP client (e.g. Zed\'s "agent_servers" config), not run directly.\n' +
|
|
25
|
+
"Waiting for protocol frames on stdin; logs: ~/.omp/logs/\n",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
17
28
|
const input = stream.Writable.toWeb(process.stdout);
|
|
18
29
|
const output = stream.Readable.toWeb(process.stdin);
|
|
19
30
|
const transport = ndJsonStream(input, output);
|
|
@@ -3,12 +3,14 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as fsp from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { visibleWidth } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Settings } from "../../../config/settings";
|
|
7
8
|
import { getThemeByName, setThemeInstance, type Theme } from "../../theme/theme";
|
|
8
9
|
import { MoveOverlay, type MoveOverlayResult, resolveExistingDirectory, resolveMovePath } from "../move-overlay";
|
|
9
10
|
|
|
10
11
|
// Strip SGR colors so assertions see visible text only.
|
|
11
|
-
const
|
|
12
|
+
const stripAnsi = (text: string): string => text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
+
const strip = (lines: readonly string[]): string => lines.map(stripAnsi).join("\n");
|
|
12
14
|
|
|
13
15
|
describe("resolveMovePath", () => {
|
|
14
16
|
it("expands ~ to homedir", () => {
|
|
@@ -82,6 +84,19 @@ describe("MoveOverlay", () => {
|
|
|
82
84
|
expect(text).toContain("Path:");
|
|
83
85
|
});
|
|
84
86
|
|
|
87
|
+
it("renders every frame row at the assigned overlay width", () => {
|
|
88
|
+
const overlay = new MoveOverlay(cwd, () => {});
|
|
89
|
+
const lines = overlay.render(72);
|
|
90
|
+
const plainLines = lines.map(stripAnsi);
|
|
91
|
+
|
|
92
|
+
expect(lines.map(line => visibleWidth(line))).toEqual(Array(lines.length).fill(72));
|
|
93
|
+
expect(plainLines[0]!.endsWith(uiTheme.boxRound.topRight)).toBe(true);
|
|
94
|
+
expect(plainLines.at(-1)!.endsWith(uiTheme.boxRound.bottomRight)).toBe(true);
|
|
95
|
+
for (const line of plainLines.slice(1, -1)) {
|
|
96
|
+
expect(line.endsWith(uiTheme.boxRound.vertical)).toBe(true);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
85
100
|
it("lists child directories (excluding hidden and files) on empty input", () => {
|
|
86
101
|
const overlay = new MoveOverlay(cwd, () => {});
|
|
87
102
|
const text = strip(overlay.render(80));
|
|
@@ -82,9 +82,9 @@ function previewLine(text: string | undefined): string {
|
|
|
82
82
|
return first.length > PREVIEW_WIDTH ? `${first.slice(0, PREVIEW_WIDTH - 1)}…` : first;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
85
|
+
/** Omitted means default read/grep/glob; an explicit empty set means no tools. */
|
|
86
86
|
function commitTools(selected: ReadonlySet<string>, all: readonly string[]): string[] | undefined {
|
|
87
|
-
if (selected.size === 0) return
|
|
87
|
+
if (selected.size === 0) return [];
|
|
88
88
|
if (selected.size === ADVISOR_DEFAULT_TOOL_NAMES.size) {
|
|
89
89
|
let matchesDefault = true;
|
|
90
90
|
for (const name of ADVISOR_DEFAULT_TOOL_NAMES) {
|
|
@@ -98,6 +98,11 @@ function commitTools(selected: ReadonlySet<string>, all: readonly string[]): str
|
|
|
98
98
|
return all.filter(name => selected.has(name));
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function formatAdvisorTools(tools: readonly string[] | undefined, emptyLabel: string): string {
|
|
102
|
+
if (tools === undefined) return "read, grep, glob (default)";
|
|
103
|
+
return tools.length > 0 ? tools.join(", ") : emptyLabel;
|
|
104
|
+
}
|
|
105
|
+
|
|
101
106
|
/** Soft-wrap plain text to `width`, returning at least one (possibly empty) line. */
|
|
102
107
|
function wrap(text: string, width: number): string[] {
|
|
103
108
|
if (!text) return [""];
|
|
@@ -266,7 +271,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
266
271
|
|
|
267
272
|
#advisorPreview(advisor: AdvisorConfig, bodyWidth: number): string[] {
|
|
268
273
|
const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
269
|
-
const tools = advisor.tools
|
|
274
|
+
const tools = formatAdvisorTools(advisor.tools, "no tools");
|
|
270
275
|
const lines = [
|
|
271
276
|
theme.bold(advisor.name || "(unnamed)"),
|
|
272
277
|
"",
|
|
@@ -303,13 +308,16 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
303
308
|
const advisor = doc.advisors[0];
|
|
304
309
|
if (!advisor) return false;
|
|
305
310
|
return (
|
|
306
|
-
advisor.name === "default" &&
|
|
311
|
+
advisor.name === "default" &&
|
|
312
|
+
!advisor.model?.trim() &&
|
|
313
|
+
advisor.tools === undefined &&
|
|
314
|
+
!advisor.instructions?.trim()
|
|
307
315
|
);
|
|
308
316
|
}
|
|
309
317
|
|
|
310
318
|
#advisorSummary(advisor: AdvisorConfig): string {
|
|
311
319
|
const model = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
312
|
-
const tools = advisor.tools
|
|
320
|
+
const tools = formatAdvisorTools(advisor.tools, "no tools");
|
|
313
321
|
return `${model} · ${tools}`;
|
|
314
322
|
}
|
|
315
323
|
|
|
@@ -384,7 +392,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
384
392
|
return;
|
|
385
393
|
}
|
|
386
394
|
const modelDescription = advisor.model?.trim() || this.#defaultModelLabel || "advisor role default";
|
|
387
|
-
const toolsDescription = advisor.tools
|
|
395
|
+
const toolsDescription = formatAdvisorTools(advisor.tools, "no tools");
|
|
388
396
|
const items: SelectItem[] = [
|
|
389
397
|
{ value: "name", label: "Name", description: advisor.name },
|
|
390
398
|
{ value: "model", label: "Model", description: modelDescription },
|
|
@@ -524,7 +532,7 @@ export class AdvisorConfigOverlayComponent implements Component {
|
|
|
524
532
|
this.#setScreen(
|
|
525
533
|
"tools",
|
|
526
534
|
list,
|
|
527
|
-
"Enter / click toggle · select Done or Esc to apply (empty
|
|
535
|
+
"Enter / click toggle · select Done or Esc to apply (empty = no tools; read/grep/glob = default)",
|
|
528
536
|
);
|
|
529
537
|
}
|
|
530
538
|
|
|
@@ -25,7 +25,6 @@ interface DirEntry {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const MAX_RESULTS = 15;
|
|
28
|
-
const OVERLAY_WIDTH = 68;
|
|
29
28
|
|
|
30
29
|
/** TTL for the directory listing cache (ms). */
|
|
31
30
|
const DIR_CACHE_TTL = 500;
|
|
@@ -230,8 +229,8 @@ export class MoveOverlay implements Component, Focusable {
|
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
231
|
|
|
233
|
-
render(
|
|
234
|
-
const w =
|
|
232
|
+
render(width: number): readonly string[] {
|
|
233
|
+
const w = width;
|
|
235
234
|
const lines: string[] = [];
|
|
236
235
|
|
|
237
236
|
lines.push(topBorder(w, "Move to directory"));
|
|
@@ -275,6 +275,7 @@ export class StatusLineComponent implements Component {
|
|
|
275
275
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
276
276
|
#loopModeStatus: { enabled: boolean } | null = null;
|
|
277
277
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
278
|
+
#vibeModeStatus: { enabled: boolean } | null = null;
|
|
278
279
|
#collabStatus: CollabStatus | null = null;
|
|
279
280
|
#focusedAgentId: string | undefined;
|
|
280
281
|
#activeRepoCache: ActiveRepoCache | undefined;
|
|
@@ -503,6 +504,10 @@ export class StatusLineComponent implements Component {
|
|
|
503
504
|
this.#goalModeStatus = status ?? null;
|
|
504
505
|
}
|
|
505
506
|
|
|
507
|
+
setVibeModeStatus(status: { enabled: boolean } | undefined): void {
|
|
508
|
+
this.#vibeModeStatus = status ?? null;
|
|
509
|
+
}
|
|
510
|
+
|
|
506
511
|
setCollabStatus(status: CollabStatus | null): void {
|
|
507
512
|
this.#collabStatus = status;
|
|
508
513
|
}
|
|
@@ -1047,6 +1052,7 @@ export class StatusLineComponent implements Component {
|
|
|
1047
1052
|
planMode: this.#planModeStatus,
|
|
1048
1053
|
loopMode: this.#loopModeStatus,
|
|
1049
1054
|
goalMode: this.#goalModeStatus,
|
|
1055
|
+
vibeMode: this.#vibeModeStatus,
|
|
1050
1056
|
collab: this.#collabStatus,
|
|
1051
1057
|
usageStats,
|
|
1052
1058
|
contextPercent,
|
|
@@ -213,6 +213,12 @@ const modeSegment: StatusLineSegment = {
|
|
|
213
213
|
return renderGoalMode(ctx, goal);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
const vibe = ctx.vibeMode;
|
|
217
|
+
if (vibe?.enabled) {
|
|
218
|
+
const content = withIcon(theme.icon.agents, "Vibe");
|
|
219
|
+
return { content: theme.fg("accent", content), visible: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
216
222
|
const loop = ctx.loopMode;
|
|
217
223
|
if (loop?.enabled) {
|
|
218
224
|
const content = withIcon(theme.icon.loop, "Loop");
|
|
@@ -47,7 +47,12 @@ import { limitMatchesActiveAccount } from "../../slash-commands/helpers/active-o
|
|
|
47
47
|
import { outputMeta } from "../../tools/output-meta";
|
|
48
48
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
49
49
|
import { replaceTabs, truncateToWidth } from "../../tools/render-utils";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
getChangelogPath,
|
|
52
|
+
parseChangelog,
|
|
53
|
+
RECENT_CHANGELOG_ENTRY_LIMIT,
|
|
54
|
+
renderChangelogEntries,
|
|
55
|
+
} from "../../utils/changelog";
|
|
51
56
|
import { copyToClipboard } from "../../utils/clipboard";
|
|
52
57
|
import { openPath } from "../../utils/open";
|
|
53
58
|
import { setSessionTerminalTitle } from "../../utils/title-generator";
|
|
@@ -487,16 +492,9 @@ export class CommandController {
|
|
|
487
492
|
async handleChangelogCommand(showFull = false): Promise<void> {
|
|
488
493
|
const changelogPath = getChangelogPath();
|
|
489
494
|
const allEntries = await parseChangelog(changelogPath);
|
|
490
|
-
|
|
491
|
-
// allEntries comes from parseChangelog with newest first, reverse to show oldest->newest
|
|
492
|
-
const entriesToShow = showFull ? allEntries : allEntries.slice(0, 3);
|
|
495
|
+
const entriesToShow = showFull ? allEntries : allEntries.slice(0, RECENT_CHANGELOG_ENTRY_LIMIT);
|
|
493
496
|
const changelogMarkdown =
|
|
494
|
-
entriesToShow.length > 0
|
|
495
|
-
? [...entriesToShow]
|
|
496
|
-
.reverse()
|
|
497
|
-
.map(e => e.content)
|
|
498
|
-
.join("\n\n")
|
|
499
|
-
: "No changelog entries found.";
|
|
497
|
+
entriesToShow.length > 0 ? renderChangelogEntries(entriesToShow).markdown : "No changelog entries found.";
|
|
500
498
|
const title = showFull ? "Full Changelog" : "Recent Changes";
|
|
501
499
|
const hint = showFull
|
|
502
500
|
? ""
|