@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.7
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 +88 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-1hmwwt45.md} +88 -0
- package/dist/cli.js +3156 -3163
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +24 -6
- package/dist/types/extensibility/extensions/types.d.ts +60 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/stats-cli.ts +6 -72
- package/src/commands/stats.ts +7 -4
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +20 -5
- package/src/extensibility/extensions/runner.ts +239 -24
- package/src/extensibility/extensions/types.ts +62 -0
- package/src/extensibility/extensions/wrapper.ts +26 -11
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
package/src/lsp/writethrough.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { isEnoent, logger, once, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { BunFile } from "bun";
|
|
4
|
+
import { isPermissionDeniedError, writeFileWithFallback } from "../tools/file-write-fallback";
|
|
4
5
|
import { FileChangeType, notifyWorkspaceWatchedFiles } from "./client";
|
|
5
6
|
import { getServersForFile } from "./config";
|
|
6
7
|
import {
|
|
@@ -66,11 +67,7 @@ export async function writethroughNoop(
|
|
|
66
67
|
_batch?: LspWritethroughBatchRequest,
|
|
67
68
|
_getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
68
69
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
69
|
-
|
|
70
|
-
await file.write(content);
|
|
71
|
-
} else {
|
|
72
|
-
await Bun.write(dst, content);
|
|
73
|
-
}
|
|
70
|
+
await writeFileWithFallback(dst, content, file);
|
|
74
71
|
return undefined;
|
|
75
72
|
}
|
|
76
73
|
|
|
@@ -78,6 +75,12 @@ interface PendingWritethrough {
|
|
|
78
75
|
dst: string;
|
|
79
76
|
file?: BunFile;
|
|
80
77
|
changeType: FileChangeType;
|
|
78
|
+
/**
|
|
79
|
+
* The bytes this entry committed. The flush prefers a fresh read of `dst` so
|
|
80
|
+
* post-processing sees whatever else in the batch touched the file, and falls
|
|
81
|
+
* back to these when that read is denied.
|
|
82
|
+
*/
|
|
83
|
+
content: string;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
interface RunLspWritethroughOptions {
|
|
@@ -288,7 +291,7 @@ async function runLspWritethrough(
|
|
|
288
291
|
const contentAlreadyWritten = runOptions?.contentAlreadyWritten ?? false;
|
|
289
292
|
|
|
290
293
|
let finalContent = content;
|
|
291
|
-
const writeContent = async (value: string) => (
|
|
294
|
+
const writeContent = async (value: string) => writeFileWithFallback(dst, value, file);
|
|
292
295
|
const getWritePromise = once(() =>
|
|
293
296
|
contentAlreadyWritten && finalContent === content ? Promise.resolve() : writeContent(finalContent),
|
|
294
297
|
);
|
|
@@ -458,9 +461,16 @@ async function flushWritethroughBatch(
|
|
|
458
461
|
try {
|
|
459
462
|
content = await fs.promises.readFile(entry.dst, "utf8");
|
|
460
463
|
} catch (error) {
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
+
if (isEnoent(error)) {
|
|
465
|
+
bundle?.finalize(undefined);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
// A brokered write lands bytes this process may not be able to read
|
|
469
|
+
// back: a sandbox that denies the write commonly denies the read too.
|
|
470
|
+
// Failing here would fail a flush whose every write succeeded, so the
|
|
471
|
+
// content this entry committed stands in for the unreadable file.
|
|
472
|
+
if (!isPermissionDeniedError(error)) throw error;
|
|
473
|
+
content = entry.content;
|
|
464
474
|
}
|
|
465
475
|
const deferredInner =
|
|
466
476
|
bundle &&
|
|
@@ -552,7 +562,7 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
552
562
|
}
|
|
553
563
|
|
|
554
564
|
const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
|
|
555
|
-
state.entries.set(dst, { dst, file, changeType });
|
|
565
|
+
state.entries.set(dst, { dst, file, changeType, content });
|
|
556
566
|
if (!batch.flush) return undefined;
|
|
557
567
|
|
|
558
568
|
writethroughBatches.delete(batch.id);
|
package/src/mcp/config.ts
CHANGED
|
@@ -224,6 +224,43 @@ export function extractExaApiKey(config: MCPServerConfig): string | undefined {
|
|
|
224
224
|
return undefined;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/** Exa MCP tools already covered by the native Exa integration. */
|
|
228
|
+
const NATIVE_EXA_MCP_TOOLS: Record<string, true> = { web_search_exa: true };
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse the comma-separated `tools` restriction from an Exa MCP config.
|
|
232
|
+
* Returns `null` when the config does not restrict its tool set.
|
|
233
|
+
*/
|
|
234
|
+
function getRequestedExaMcpTools(config: MCPServerConfig): string[] | null {
|
|
235
|
+
const raw = (() => {
|
|
236
|
+
if (config.type === "http" || config.type === "sse") {
|
|
237
|
+
const httpConfig = config as { url?: string };
|
|
238
|
+
if (!httpConfig.url) return undefined;
|
|
239
|
+
try {
|
|
240
|
+
return new URL(httpConfig.url).searchParams.get("tools") ?? undefined;
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!config.type || config.type === "stdio") {
|
|
246
|
+
const stdioConfig = config as { args?: string[] };
|
|
247
|
+
const args = stdioConfig.args ?? [];
|
|
248
|
+
for (let i = 0; i < args.length; i++) {
|
|
249
|
+
if (/^--?tools$/i.test(args[i])) return args[i + 1];
|
|
250
|
+
const match = args[i].match(/(?:^|[\s?&])tools=([^&\s]+)/i) ?? args[i].match(/--?tools[=\s]([^\s]+)/i);
|
|
251
|
+
if (match) return match[1];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
})();
|
|
256
|
+
if (!raw) return null;
|
|
257
|
+
const tools = raw
|
|
258
|
+
.split(",")
|
|
259
|
+
.map(tool => tool.trim())
|
|
260
|
+
.filter(tool => tool.length > 0);
|
|
261
|
+
return tools.length > 0 ? tools : null;
|
|
262
|
+
}
|
|
263
|
+
|
|
227
264
|
/** Result of filtering Exa MCP servers */
|
|
228
265
|
export interface ExaFilterResult {
|
|
229
266
|
/** Configs with Exa servers removed */
|
|
@@ -236,7 +273,9 @@ export interface ExaFilterResult {
|
|
|
236
273
|
|
|
237
274
|
/**
|
|
238
275
|
* Filter out Exa MCP servers and extract their API keys.
|
|
239
|
-
* Since we have native Exa integration, we don't need the MCP server
|
|
276
|
+
* Since we have native Exa integration, we don't need the MCP server —
|
|
277
|
+
* unless the config explicitly requests Exa tools the native integration
|
|
278
|
+
* does not provide (e.g. `web_fetch_exa`, `web_search_advanced_exa`).
|
|
240
279
|
*/
|
|
241
280
|
export function filterExaMCPServers(
|
|
242
281
|
configs: Record<string, MCPServerConfig>,
|
|
@@ -248,17 +287,22 @@ export function filterExaMCPServers(
|
|
|
248
287
|
|
|
249
288
|
for (const [name, config] of Object.entries(configs)) {
|
|
250
289
|
if (isExaMCPServer(name, config)) {
|
|
251
|
-
// Extract API key
|
|
290
|
+
// Extract API key for the native Exa integration even when the MCP
|
|
291
|
+
// server is kept below for its extra tools.
|
|
252
292
|
const apiKey = extractExaApiKey(config);
|
|
253
293
|
if (apiKey) {
|
|
254
294
|
exaApiKeys.push(apiKey);
|
|
255
295
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
296
|
+
const requested = getRequestedExaMcpTools(config);
|
|
297
|
+
const hasExtraTools = requested?.some(tool => !NATIVE_EXA_MCP_TOOLS[tool.toLowerCase()]) ?? false;
|
|
298
|
+
if (!hasExtraTools) {
|
|
299
|
+
continue;
|
|
260
300
|
}
|
|
261
301
|
}
|
|
302
|
+
filtered[name] = config;
|
|
303
|
+
if (sources[name]) {
|
|
304
|
+
filteredSources[name] = sources[name];
|
|
305
|
+
}
|
|
262
306
|
}
|
|
263
307
|
|
|
264
308
|
return { configs: filtered, exaApiKeys, sources: filteredSources };
|
package/src/memories/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type * as fsNode from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
6
|
-
import { type ApiKey, completeSimple, Effort, type Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { type ApiKey, completeSimple, Effort, type Model, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { clampThinkingLevelForModel } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
8
8
|
import { getAgentDbPath, getMemoriesDir, isEnoent, logger, parseJsonlLenient, prompt } from "@oh-my-pi/pi-utils";
|
|
9
9
|
|
|
@@ -750,18 +750,20 @@ async function runStage1Job(options: {
|
|
|
750
750
|
response_items_json: truncatedItems,
|
|
751
751
|
});
|
|
752
752
|
|
|
753
|
-
const response = await
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
753
|
+
const response = await retryTransientCompletion(() =>
|
|
754
|
+
completeSimple(
|
|
755
|
+
model,
|
|
756
|
+
{
|
|
757
|
+
systemPrompt: [stageOneSystemTemplate],
|
|
758
|
+
messages: [{ role: "user", content: [{ type: "text", text: inputPrompt }], timestamp: Date.now() }],
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
apiKey,
|
|
762
|
+
metadata: options.metadata,
|
|
763
|
+
maxTokens: Math.max(1024, Math.min(4096, Math.floor(modelMaxTokens * 0.2))),
|
|
764
|
+
reasoning: clampThinkingLevelForModel(model, Effort.Low),
|
|
765
|
+
},
|
|
766
|
+
),
|
|
765
767
|
);
|
|
766
768
|
|
|
767
769
|
if (response.stopReason === "error") {
|
|
@@ -887,18 +889,20 @@ async function runConsolidationModel(options: {
|
|
|
887
889
|
rollout_summaries: truncateByApproxTokens(rolloutSummaries, 12_000),
|
|
888
890
|
});
|
|
889
891
|
|
|
890
|
-
const response = await
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
892
|
+
const response = await retryTransientCompletion(() =>
|
|
893
|
+
completeSimple(
|
|
894
|
+
model,
|
|
895
|
+
{
|
|
896
|
+
systemPrompt: [consolidationSystemTemplate],
|
|
897
|
+
messages: [{ role: "user", content: [{ type: "text", text: input }], timestamp: Date.now() }],
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
apiKey,
|
|
901
|
+
metadata: options.metadata,
|
|
902
|
+
maxTokens: 8192,
|
|
903
|
+
reasoning: clampThinkingLevelForModel(model, Effort.Medium),
|
|
904
|
+
},
|
|
905
|
+
),
|
|
902
906
|
);
|
|
903
907
|
if (response.stopReason === "error") {
|
|
904
908
|
throw new Error(response.errorMessage || "phase2 model error");
|
package/src/mnemopi/backend.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { type ApiKeyResolver, completeSimple } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { type ApiKeyResolver, completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
|
|
5
5
|
import type { Mnemopi } from "@oh-my-pi/pi-mnemopi";
|
|
6
6
|
import type * as MnemopiDiagnoseNs from "@oh-my-pi/pi-mnemopi/diagnose";
|
|
@@ -545,16 +545,18 @@ async function resolveMnemopiProviderOptions(
|
|
|
545
545
|
});
|
|
546
546
|
return null;
|
|
547
547
|
}
|
|
548
|
-
const message = await
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
548
|
+
const message = await retryTransientCompletion(() =>
|
|
549
|
+
completeSimple(
|
|
550
|
+
model,
|
|
551
|
+
{
|
|
552
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
apiKey: modelRegistry.resolver(model, sessionId),
|
|
556
|
+
maxTokens: opts?.maxTokens,
|
|
557
|
+
temperature: opts?.temperature,
|
|
558
|
+
},
|
|
559
|
+
),
|
|
558
560
|
);
|
|
559
561
|
return message.content
|
|
560
562
|
.filter(
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Markdown,
|
|
5
5
|
type MarkdownTheme,
|
|
6
6
|
matchesKey,
|
|
7
|
+
padding,
|
|
7
8
|
renderInlineMarkdown,
|
|
8
9
|
replaceTabs,
|
|
9
10
|
ScrollView,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
Text,
|
|
13
14
|
type TUI,
|
|
14
15
|
truncateToWidth,
|
|
16
|
+
visibleWidth,
|
|
15
17
|
wrapTextWithAnsi,
|
|
16
18
|
} from "@oh-my-pi/pi-tui";
|
|
17
19
|
import type {
|
|
@@ -315,8 +317,18 @@ function renderRowLabel(
|
|
|
315
317
|
const cursor = selected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
316
318
|
const label = renderInlineMarkdown(rowItem.label, mdTheme, t => theme.fg(color, t));
|
|
317
319
|
const noteMarker = state.note && state.noteRowKey === rowItem.key ? theme.fg("success", " ✎ note") : "";
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
// `width` is already the inner content width consumed by row(); when a
|
|
321
|
+
// scrollbar is needed, renderRows() calls this again with one less column.
|
|
322
|
+
// Keep the cursor, option marker, first wrapped label line, and optional
|
|
323
|
+
// note marker within that budget so the outer fit() never truncates them.
|
|
324
|
+
const noteWidth = noteMarker ? visibleWidth(noteMarker) : 0;
|
|
325
|
+
const labelWidth = Math.max(1, width - visibleWidth(cursor) - visibleWidth(marker) - noteWidth);
|
|
326
|
+
const wrappedLabel = wrapTextWithAnsi(label, labelWidth);
|
|
327
|
+
const indent = padding(visibleWidth(cursor) + visibleWidth(marker));
|
|
328
|
+
const lines = [`${cursor}${marker}${wrappedLabel[0] ?? ""}${noteMarker}`];
|
|
329
|
+
for (let i = 1; i < wrappedLabel.length; i++) {
|
|
330
|
+
lines.push(`${indent}${wrappedLabel[i] ?? ""}`);
|
|
331
|
+
}
|
|
320
332
|
if (rowItem.kind === "option") {
|
|
321
333
|
const option = question.options[rowItem.optionIndex ?? -1];
|
|
322
334
|
if (option?.description?.trim()) {
|
|
@@ -602,7 +614,9 @@ export class AskDialogComponent implements Component {
|
|
|
602
614
|
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
603
615
|
}
|
|
604
616
|
const question = this.#questions[this.#currentQuestionIndex()];
|
|
605
|
-
|
|
617
|
+
// Enter advances in multi-question dialogs and submits single-question ones.
|
|
618
|
+
const enterAction = this.#questions.length > 1 ? "next" : "submit";
|
|
619
|
+
const action = question?.multi ? `Space toggle · Enter ${enterAction}` : "Enter select · n note";
|
|
606
620
|
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
607
621
|
if (this.#questionCanPage && indicator) {
|
|
608
622
|
return `${action} · ↑/↓${tabs} · ${cancel} · ${pageKeysLabel()} ${indicator}`;
|
|
@@ -680,8 +694,14 @@ export class AskDialogComponent implements Component {
|
|
|
680
694
|
const option = question.options[rowItem.optionIndex ?? -1];
|
|
681
695
|
if (!option) return;
|
|
682
696
|
if (question.multi) {
|
|
683
|
-
|
|
684
|
-
|
|
697
|
+
if (isEnter) {
|
|
698
|
+
// Enter confirms the current selection without toggling the
|
|
699
|
+
// focused option; Space toggles. Advances to the next question
|
|
700
|
+
// (submitting only for a single-question dialog), matching
|
|
701
|
+
// single-select Enter (#8252).
|
|
702
|
+
this.#advanceAfterQuestion();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
685
705
|
if (state.selectedOptions.has(option.label)) {
|
|
686
706
|
state.selectedOptions.delete(option.label);
|
|
687
707
|
clearNoteIfRow(state, rowItem.key);
|
|
@@ -734,10 +734,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
734
734
|
* region (a later block streams below it), or — while it is still the
|
|
735
735
|
* live tail — its head rows were committed because the frame outgrew the
|
|
736
736
|
* viewport. Committed rows are immutable, so from that point every further
|
|
737
|
-
* partial snapshot is dropped.
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
737
|
+
* partial snapshot is dropped. A hidden, wholly uncommitted block keeps
|
|
738
|
+
* accepting snapshots so revealing it starts from current progress. Rows
|
|
739
|
+
* restyle static gray only when nothing is committed yet; otherwise the
|
|
740
|
+
* bytes stay exactly as painted. One-way — blocks never re-enter the live
|
|
741
|
+
* region. Returns whether the block is frozen.
|
|
741
742
|
*/
|
|
742
743
|
#maybeFreezeBackgroundTask(): boolean {
|
|
743
744
|
if (this.#backgroundTaskFrozen) return true;
|
|
@@ -745,7 +746,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
745
746
|
const asyncState = (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state;
|
|
746
747
|
if (asyncState !== "running") return false;
|
|
747
748
|
const uncommitted = this.#liveRegion.isBlockUncommitted?.(this) ?? true;
|
|
748
|
-
if (uncommitted && this.#liveRegion.isBlockInLiveRegion(this)) return false;
|
|
749
|
+
if (uncommitted && (!this.#toolActivityVisible || this.#liveRegion.isBlockInLiveRegion(this))) return false;
|
|
749
750
|
this.#backgroundTaskFrozen = true;
|
|
750
751
|
this.#updateSpinnerAnimation();
|
|
751
752
|
if (uncommitted) {
|
|
@@ -825,10 +826,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
825
826
|
* Reports `false` while it can still visually change so the
|
|
826
827
|
* {@link TranscriptContainer} keeps it inside the repaintable live region:
|
|
827
828
|
* a foreground tool awaiting its result, or one streaming partial output.
|
|
829
|
+
* Hidden blocks render no rows, so they cannot gate later streaming content.
|
|
830
|
+
* Visibility toggles reset and replay the transcript before revealing them.
|
|
828
831
|
* A final (non-partial) result, a background-async tool the agent has moved
|
|
829
|
-
* past, or an explicit {@link seal} flips it to `true`.
|
|
832
|
+
* past, or an explicit {@link seal} also flips it to `true`.
|
|
830
833
|
*/
|
|
831
834
|
isTranscriptBlockFinalized(): boolean {
|
|
835
|
+
if (!this.#toolActivityVisible) return true;
|
|
832
836
|
if (this.#sealed) return true;
|
|
833
837
|
if (this.#result === undefined) return false;
|
|
834
838
|
// A displaceable snapshot stays live: its rows are kept out of native
|
|
@@ -248,7 +248,10 @@ export class WelcomeComponent implements Component {
|
|
|
248
248
|
visibleWidth(this.modelName),
|
|
249
249
|
visibleWidth(this.providerName),
|
|
250
250
|
);
|
|
251
|
-
const desiredLeftCol = Math.
|
|
251
|
+
const desiredLeftCol = Math.max(
|
|
252
|
+
Math.min(preferredLeftCol, Math.max(minLeftCol, Math.floor(dualContentWidth * 0.35))),
|
|
253
|
+
leftMinContentWidth,
|
|
254
|
+
);
|
|
252
255
|
const dualLeftCol =
|
|
253
256
|
dualContentWidth >= minRightCol + 1
|
|
254
257
|
? Math.min(desiredLeftCol, dualContentWidth - minRightCol)
|
|
@@ -1053,53 +1053,73 @@ export class ExtensionUiController {
|
|
|
1053
1053
|
const savedText = this.ctx.editor.getText();
|
|
1054
1054
|
const keybindings = KeybindingsManager.inMemory();
|
|
1055
1055
|
|
|
1056
|
-
const { promise, resolve } = Promise.withResolvers<T>();
|
|
1056
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
1057
1057
|
let component: (Component & { dispose?(): void }) | undefined;
|
|
1058
1058
|
let overlayHandle: OverlayHandle | undefined;
|
|
1059
1059
|
let closed = false;
|
|
1060
|
+
let editorReplaced = false;
|
|
1060
1061
|
|
|
1061
|
-
const
|
|
1062
|
-
if (closed) return;
|
|
1063
|
-
closed = true;
|
|
1062
|
+
const cleanup = () => {
|
|
1064
1063
|
component?.dispose?.();
|
|
1065
1064
|
overlayHandle?.hide();
|
|
1066
1065
|
overlayHandle = undefined;
|
|
1067
|
-
if (
|
|
1066
|
+
if (editorReplaced) {
|
|
1068
1067
|
this.ctx.editorContainer.clear();
|
|
1069
1068
|
this.ctx.editorContainer.addChild(this.ctx.editor);
|
|
1070
1069
|
this.ctx.editor.setText(savedText);
|
|
1071
1070
|
}
|
|
1072
1071
|
this.ctx.ui.setFocus(this.ctx.editor);
|
|
1073
1072
|
this.ctx.ui.requestRender();
|
|
1074
|
-
resolve(result);
|
|
1075
1073
|
};
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
const overlayOptions =
|
|
1085
|
-
typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
|
|
1086
|
-
overlayHandle = this.ctx.ui.showOverlay(
|
|
1087
|
-
component,
|
|
1088
|
-
overlayOptions ?? {
|
|
1089
|
-
anchor: "bottom-center",
|
|
1090
|
-
width: "100%",
|
|
1091
|
-
maxHeight: "100%",
|
|
1092
|
-
margin: 0,
|
|
1093
|
-
},
|
|
1094
|
-
);
|
|
1095
|
-
options.onHandle?.(overlayHandle);
|
|
1096
|
-
return;
|
|
1074
|
+
const finish = (settle: () => void) => {
|
|
1075
|
+
if (closed) return;
|
|
1076
|
+
closed = true;
|
|
1077
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1078
|
+
try {
|
|
1079
|
+
cleanup();
|
|
1080
|
+
} finally {
|
|
1081
|
+
settle();
|
|
1097
1082
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1083
|
+
};
|
|
1084
|
+
const fail = (error: unknown) => finish(() => reject(error));
|
|
1085
|
+
const onAbort = () => fail(options?.signal?.reason ?? new DOMException("Dialog aborted", "AbortError"));
|
|
1086
|
+
const close = (result: T) => finish(() => resolve(result));
|
|
1087
|
+
|
|
1088
|
+
if (options?.signal?.aborted) {
|
|
1089
|
+
fail(options.signal.reason ?? new DOMException("Dialog aborted", "AbortError"));
|
|
1090
|
+
return promise;
|
|
1091
|
+
}
|
|
1092
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1093
|
+
|
|
1094
|
+
Promise.try(() => factory(this.ctx.ui, theme, keybindings, close))
|
|
1095
|
+
.then(c => {
|
|
1096
|
+
if (closed) {
|
|
1097
|
+
c.dispose?.();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
component = c;
|
|
1101
|
+
if (options?.overlay) {
|
|
1102
|
+
const overlayOptions =
|
|
1103
|
+
typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions;
|
|
1104
|
+
overlayHandle = this.ctx.ui.showOverlay(
|
|
1105
|
+
component,
|
|
1106
|
+
overlayOptions ?? {
|
|
1107
|
+
anchor: "bottom-center",
|
|
1108
|
+
width: "100%",
|
|
1109
|
+
maxHeight: "100%",
|
|
1110
|
+
margin: 0,
|
|
1111
|
+
},
|
|
1112
|
+
);
|
|
1113
|
+
options.onHandle?.(overlayHandle);
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
editorReplaced = true;
|
|
1117
|
+
this.ctx.editorContainer.clear();
|
|
1118
|
+
this.ctx.editorContainer.addChild(component);
|
|
1119
|
+
this.ctx.ui.setFocus(component);
|
|
1120
|
+
this.ctx.ui.requestRender();
|
|
1121
|
+
})
|
|
1122
|
+
.catch(fail);
|
|
1103
1123
|
return promise;
|
|
1104
1124
|
}
|
|
1105
1125
|
|
|
@@ -562,6 +562,12 @@ export class SelectorController {
|
|
|
562
562
|
this.ctx.rebuildChatFromMessages();
|
|
563
563
|
this.ctx.ui.resetDisplay();
|
|
564
564
|
break;
|
|
565
|
+
case "display.showTokenUsage":
|
|
566
|
+
// Rebuild reruns usage-row detection under the new setting; resetDisplay
|
|
567
|
+
// retires rows already committed to native scrollback.
|
|
568
|
+
this.ctx.rebuildChatFromMessages();
|
|
569
|
+
this.ctx.ui.resetDisplay();
|
|
570
|
+
break;
|
|
565
571
|
case "tui.tight":
|
|
566
572
|
setTuiTight(value as boolean);
|
|
567
573
|
this.ctx.ui.invalidate();
|
|
@@ -111,6 +111,7 @@ export class TanCommandController {
|
|
|
111
111
|
let jobId = "";
|
|
112
112
|
try {
|
|
113
113
|
const cloneManager = await SessionManager.forkFrom(parentFile, cwd, sessionDir, undefined, {
|
|
114
|
+
copyArtifacts: false,
|
|
114
115
|
suppressBreadcrumb: true,
|
|
115
116
|
sessionFile: cloneFile,
|
|
116
117
|
});
|
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
logger,
|
|
51
51
|
postmortem,
|
|
52
52
|
prompt,
|
|
53
|
+
sanitizeText,
|
|
53
54
|
setProjectDir,
|
|
54
55
|
} from "@oh-my-pi/pi-utils";
|
|
55
56
|
import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
@@ -390,16 +391,20 @@ export interface InteractiveModeOptions {
|
|
|
390
391
|
/**
|
|
391
392
|
* Anchored live-region container for the HUD/status rows between the transcript
|
|
392
393
|
* and the editor (working loader, todo + subagent HUDs, transient notification
|
|
393
|
-
* panels). While it has content every row is live: it reports a seam at 0
|
|
394
|
-
* engine never commits these anchored,
|
|
395
|
-
* scrollback — otherwise stale duplicates pile
|
|
396
|
-
* terminals once the loader sits below a tall HUD. The transcript's own seam,
|
|
394
|
+
* panels). While it has content every row is live: it reports a seam at 0 and
|
|
395
|
+
* pins that live region so the engine never commits these anchored,
|
|
396
|
+
* rebuilt-in-place rows to native scrollback — otherwise stale duplicates pile
|
|
397
|
+
* up above the live copy on short terminals once the loader sits below a tall HUD. The transcript's own seam,
|
|
397
398
|
* when present, sits higher and wins (topmost-seam merge in TUI.render).
|
|
398
399
|
*/
|
|
399
400
|
class AnchoredLiveContainer extends Container implements NativeScrollbackLiveRegion {
|
|
400
401
|
getNativeScrollbackLiveRegionStart(): number | undefined {
|
|
401
402
|
return this.children.length > 0 ? 0 : undefined;
|
|
402
403
|
}
|
|
404
|
+
|
|
405
|
+
isNativeScrollbackLiveRegionPinned(): boolean {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
403
408
|
}
|
|
404
409
|
|
|
405
410
|
/**
|
|
@@ -814,7 +819,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
814
819
|
this.editor.onAutocompleteUpdate = () => {
|
|
815
820
|
this.ui.requestRender();
|
|
816
821
|
};
|
|
817
|
-
this.editor.setShimmerRepaintHandler(() => this.ui.
|
|
822
|
+
this.editor.setShimmerRepaintHandler(() => this.ui.requestDirectWrite(this.editor));
|
|
818
823
|
this.#syncEditorMaxHeight();
|
|
819
824
|
this.#resizeHandler = () => {
|
|
820
825
|
this.#syncEditorMaxHeight();
|
|
@@ -1108,6 +1113,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1108
1113
|
// before initHooksAndCustomTools/#reconcileModeFromSession/#enterPlanMode —
|
|
1109
1114
|
// all of which can reach setSessionName during init.
|
|
1110
1115
|
this.#eventBusUnsubscribers.push(
|
|
1116
|
+
this.sessionManager.onPersistenceError(error => {
|
|
1117
|
+
const detail = truncateToWidth(
|
|
1118
|
+
replaceTabs(sanitizeText(error.message)).replace(/[\r\n]+/g, " "),
|
|
1119
|
+
TRUNCATE_LENGTHS.LINE,
|
|
1120
|
+
);
|
|
1121
|
+
this.showWarning(
|
|
1122
|
+
`Session persistence failed: ${detail}. Unsaved entries remain in memory; persistence will retry on the next entry.`,
|
|
1123
|
+
);
|
|
1124
|
+
}),
|
|
1111
1125
|
this.sessionManager.onSessionNameChanged(() => {
|
|
1112
1126
|
setSessionTerminalTitle(this.sessionManager.getSessionName(), this.sessionManager.getCwd());
|
|
1113
1127
|
this.#handleSessionAccentInputsChanged();
|
|
@@ -1177,6 +1191,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1177
1191
|
|
|
1178
1192
|
this.#eventBusUnsubscribers.push(
|
|
1179
1193
|
this.session.subscribe(event => {
|
|
1194
|
+
if (event.type === "model_changed") {
|
|
1195
|
+
this.#updateWelcomeModel();
|
|
1196
|
+
}
|
|
1180
1197
|
void this.#handleGoalSessionEvent(event);
|
|
1181
1198
|
}),
|
|
1182
1199
|
onStatusLineSessionAccentChanged(() => {
|
|
@@ -1184,6 +1201,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1184
1201
|
this.#handleSessionAccentInputsChanged();
|
|
1185
1202
|
}),
|
|
1186
1203
|
);
|
|
1204
|
+
// Resync the welcome banner to the live model: init-time reconciliations
|
|
1205
|
+
// (#reconcileModeFromSession, #enterPlanMode for plan.defaultOnStartup)
|
|
1206
|
+
// can change the model before this subscription exists, so the
|
|
1207
|
+
// model_changed events they emit are never observed by the handler above.
|
|
1208
|
+
this.#updateWelcomeModel();
|
|
1187
1209
|
this.#eventBusUnsubscribers.push(
|
|
1188
1210
|
onModelRolesChanged(() => {
|
|
1189
1211
|
void this.#reapplyPlanModeModelOnRoleChange();
|
|
@@ -3509,10 +3531,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3509
3531
|
if (!this.vibeModeEnabled) {
|
|
3510
3532
|
return;
|
|
3511
3533
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3534
|
+
// Tear down with the queued-message drain suppressed: aborting the active
|
|
3535
|
+
// turn would otherwise let a queued user steer/follow-up restart on the
|
|
3536
|
+
// still-live Vibe tools before this teardown removes them (issue #8326).
|
|
3537
|
+
let killed = 0;
|
|
3538
|
+
await this.session.runModeExitTeardown(async () => {
|
|
3539
|
+
if (this.session.isStreaming) {
|
|
3540
|
+
await this.session.abort();
|
|
3541
|
+
}
|
|
3542
|
+
killed = await VibeSessionRegistry.global().killAll(this.#vibeParentSession(), this.#vibeModeOwnerScope);
|
|
3543
|
+
await this.session.deactivateVibeTools(this.#vibeModePreviousTools ?? []);
|
|
3544
|
+
this.session.setVibeModeState(undefined);
|
|
3545
|
+
});
|
|
3516
3546
|
this.vibeModeEnabled = false;
|
|
3517
3547
|
this.#vibeModePreviousTools = undefined;
|
|
3518
3548
|
this.#vibeModeOwnerScope = undefined;
|
|
@@ -4252,7 +4282,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4252
4282
|
nextEditor.onAutocompleteUpdate = () => {
|
|
4253
4283
|
this.ui.requestRender();
|
|
4254
4284
|
};
|
|
4255
|
-
nextEditor.setShimmerRepaintHandler(() => this.ui.
|
|
4285
|
+
nextEditor.setShimmerRepaintHandler(() => this.ui.requestDirectWrite(nextEditor));
|
|
4256
4286
|
nextEditor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth));
|
|
4257
4287
|
nextEditor.setMaxHeight(this.#computeEditorMaxHeight());
|
|
4258
4288
|
if (this.historyStorage) {
|
|
@@ -4430,6 +4460,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4430
4460
|
);
|
|
4431
4461
|
}
|
|
4432
4462
|
|
|
4463
|
+
#updateWelcomeModel(): void {
|
|
4464
|
+
if (!this.#welcomeComponent) {
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
|
|
4468
|
+
this.#welcomeComponent.setModel(this.session.model?.name ?? "Unknown", this.session.model?.provider ?? "Unknown");
|
|
4469
|
+
this.ui.requestRender();
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4433
4472
|
#updateWelcomeLspServers(): void {
|
|
4434
4473
|
if (!this.#welcomeComponent) {
|
|
4435
4474
|
return;
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"thinkingText": "warmGray",
|
|
34
34
|
"selectedBg": "selectedBg",
|
|
35
35
|
"userMessageBg": "userMsgBg",
|
|
36
|
-
"userMessageText": "",
|
|
36
|
+
"userMessageText": "forestFloor",
|
|
37
37
|
"customMessageBg": "customMsgBg",
|
|
38
|
-
"customMessageText": "",
|
|
38
|
+
"customMessageText": "forestFloor",
|
|
39
39
|
"customMessageLabel": "#5a7a52",
|
|
40
40
|
"toolPendingBg": "toolPendingBg",
|
|
41
41
|
"toolSuccessBg": "toolSuccessBg",
|