@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.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 +26 -0
- package/dist/cli.js +3336 -3318
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +830 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +288 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/chat-transcript-builder.ts +22 -1
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/selector-controller.ts +15 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/sdk.ts +33 -3
- package/src/session/agent-session.ts +239 -17
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
|
@@ -6,7 +6,7 @@ import { getMnemopiSessionState, type MnemopiScopedMemoryHit, type MnemopiSessio
|
|
|
6
6
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
7
7
|
import { buildDirectoryResource } from "./filesystem-resource";
|
|
8
8
|
import { validateRelativePath } from "./skill-protocol";
|
|
9
|
-
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
9
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
10
10
|
|
|
11
11
|
const DEFAULT_MEMORY_FILE = "memory_summary.md";
|
|
12
12
|
const MEMORY_NAMESPACE = "root";
|
|
@@ -28,6 +28,11 @@ export function memoryRootsFromRegistry(): string[] {
|
|
|
28
28
|
return roots;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function memoryRootsForContext(context?: ResolveContext): string[] {
|
|
32
|
+
if (context?.cwd) return [getMemoryRoot(getAgentDir(), context.cwd)];
|
|
33
|
+
return memoryRootsFromRegistry();
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
function ensureWithinRoot(targetPath: string, rootPath: string): void {
|
|
32
37
|
if (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {
|
|
33
38
|
throw new Error("memory:// URL escapes memory root");
|
|
@@ -196,16 +201,15 @@ function renderMnemopiMemory(url: InternalUrl, hit: MnemopiScopedMemoryHit): Int
|
|
|
196
201
|
|
|
197
202
|
/**
|
|
198
203
|
* Protocol handler for memory:// URLs.
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* sharing a cwd see the same file regardless of order.
|
|
204
|
+
* Resolves file-backed roots against the calling session cwd when provided.
|
|
205
|
+
* Contextless callers fall back to the live-session registry for legacy
|
|
206
|
+
* cross-session lookups.
|
|
203
207
|
*/
|
|
204
208
|
export class MemoryProtocolHandler implements ProtocolHandler {
|
|
205
209
|
readonly scheme = "memory";
|
|
206
210
|
readonly immutable = true;
|
|
207
211
|
|
|
208
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
212
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
209
213
|
const namespace = url.rawHost || url.hostname;
|
|
210
214
|
if (!namespace) {
|
|
211
215
|
throw new Error("memory:// URL requires a namespace: memory://root or memory://<memory-id>");
|
|
@@ -230,7 +234,7 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
230
234
|
);
|
|
231
235
|
}
|
|
232
236
|
|
|
233
|
-
const roots =
|
|
237
|
+
const roots = memoryRootsForContext(context);
|
|
234
238
|
if (roots.length === 0) {
|
|
235
239
|
throw new Error(
|
|
236
240
|
"Memory artifacts are not available for this project yet. Run a session with memories enabled first.",
|
|
@@ -259,9 +263,9 @@ export class MemoryProtocolHandler implements ProtocolHandler {
|
|
|
259
263
|
throw new Error(`Memory file not found: ${url.href}`);
|
|
260
264
|
}
|
|
261
265
|
|
|
262
|
-
async complete(): Promise<UrlCompletion[]> {
|
|
266
|
+
async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
|
|
263
267
|
const completions: UrlCompletion[] = [];
|
|
264
|
-
if (
|
|
268
|
+
if (memoryRootsForContext(context).length > 0) {
|
|
265
269
|
completions.push({ value: MEMORY_NAMESPACE, description: "Project memory summary" });
|
|
266
270
|
}
|
|
267
271
|
if (mnemopiSessionStatesFromRegistry().length > 0) {
|
package/src/lsp/client.ts
CHANGED
|
@@ -750,8 +750,14 @@ export async function getOrCreateClient(
|
|
|
750
750
|
|
|
751
751
|
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
752
752
|
|
|
753
|
-
//
|
|
753
|
+
// Finish the initialize handshake before publishing the client as ready.
|
|
754
754
|
await sendNotification(client, "initialized", {}, signal);
|
|
755
|
+
await sendNotification(
|
|
756
|
+
client,
|
|
757
|
+
"workspace/didChangeConfiguration",
|
|
758
|
+
{ settings: config.settings ?? {} },
|
|
759
|
+
signal,
|
|
760
|
+
);
|
|
755
761
|
|
|
756
762
|
client.status = "ready";
|
|
757
763
|
// Publish only after init succeeds: pre-init clients are reachable
|
package/src/main.ts
CHANGED
|
@@ -636,6 +636,27 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
|
|
636
636
|
return undefined;
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
+
const SESSION_ID_ARG_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
640
|
+
|
|
641
|
+
export function normalizeContinueSessionArgs(parsed: Args, rawArgs?: readonly string[]): void {
|
|
642
|
+
if (!parsed.continue || parsed.resume || parsed.fork) return;
|
|
643
|
+
|
|
644
|
+
let message: string | undefined;
|
|
645
|
+
if (parsed.unrecognizedFlags.length === 0 && parsed.messages.length === 1) {
|
|
646
|
+
message = parsed.messages[0]?.trim();
|
|
647
|
+
} else if (rawArgs) {
|
|
648
|
+
const continueIndex = rawArgs.findIndex(arg => arg === "--continue" || arg === "-c");
|
|
649
|
+
message = rawArgs[continueIndex + 1]?.trim();
|
|
650
|
+
}
|
|
651
|
+
if (!message || !SESSION_ID_ARG_RE.test(message)) return;
|
|
652
|
+
|
|
653
|
+
const messageIndex = parsed.messages.indexOf(message);
|
|
654
|
+
if (messageIndex === -1) return;
|
|
655
|
+
parsed.resume = message;
|
|
656
|
+
parsed.continue = false;
|
|
657
|
+
parsed.messages.splice(messageIndex, 1);
|
|
658
|
+
}
|
|
659
|
+
|
|
639
660
|
/** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */
|
|
640
661
|
export async function createSessionManager(
|
|
641
662
|
parsed: Args,
|
|
@@ -665,6 +686,8 @@ export async function createSessionManager(
|
|
|
665
686
|
if (parsed.noSession) {
|
|
666
687
|
return SessionManager.inMemory();
|
|
667
688
|
}
|
|
689
|
+
normalizeContinueSessionArgs(parsed);
|
|
690
|
+
|
|
668
691
|
if (typeof parsed.resume === "string") {
|
|
669
692
|
const sessionArg = parsed.resume;
|
|
670
693
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
@@ -1188,6 +1211,11 @@ export async function runRootCommand(
|
|
|
1188
1211
|
);
|
|
1189
1212
|
}
|
|
1190
1213
|
|
|
1214
|
+
// Resolve an explicit `--continue <id>` before extension flags are loaded.
|
|
1215
|
+
// Reading the token immediately after `--continue` distinguishes the session
|
|
1216
|
+
// id from UUID-shaped values owned by later extension flags.
|
|
1217
|
+
normalizeContinueSessionArgs(parsedArgs, rawArgs);
|
|
1218
|
+
|
|
1191
1219
|
// Create session manager based on CLI flags. SessionResolutionError signals a
|
|
1192
1220
|
// user-facing failure (unknown --resume/--fork id, non-interactive fork
|
|
1193
1221
|
// prompt, --fork with --no-session): print + exit cleanly instead of letting
|
|
@@ -1386,6 +1414,7 @@ export async function runRootCommand(
|
|
|
1386
1414
|
},
|
|
1387
1415
|
};
|
|
1388
1416
|
const initialArgs = applyExtensionFlags(extensionFlagSink, rawArgs) ?? parsedArgs;
|
|
1417
|
+
normalizeContinueSessionArgs(initialArgs, rawArgs);
|
|
1389
1418
|
// Fail fast on stale/typo flags (e.g. `omp --list-models`) now that we
|
|
1390
1419
|
// know the real extension flag set. Without this check the unrecognized
|
|
1391
1420
|
// token gets silently consumed and any following positional leaks as the
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -15,8 +15,10 @@ import type {
|
|
|
15
15
|
CustomToolResult,
|
|
16
16
|
RenderResultOptions,
|
|
17
17
|
} from "../extensibility/custom-tools/types";
|
|
18
|
+
import { resolveLocalUrlToFile } from "../internal-urls/local-protocol";
|
|
18
19
|
import type { Theme } from "../modes/theme/theme";
|
|
19
20
|
import type { OutputMeta } from "../tools/output-meta";
|
|
21
|
+
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
20
22
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
21
23
|
import { callTool } from "./client";
|
|
22
24
|
import { renderMCPCall, renderMCPResult } from "./render";
|
|
@@ -104,13 +106,62 @@ function stripHarnessIntent(args: MCPToolArgs, inputSchema: MCPToolDefinition["i
|
|
|
104
106
|
return rest;
|
|
105
107
|
}
|
|
106
108
|
|
|
109
|
+
async function resolveOutboundLocalUrlArgs(
|
|
110
|
+
value: unknown,
|
|
111
|
+
context: CustomToolContext,
|
|
112
|
+
seen: WeakSet<object> = new WeakSet(),
|
|
113
|
+
): Promise<unknown> {
|
|
114
|
+
if (typeof value === "string") {
|
|
115
|
+
const normalized = normalizeLocalScheme(value);
|
|
116
|
+
if (!normalized.startsWith("local://")) return value;
|
|
117
|
+
const localFile = await resolveLocalUrlToFile(normalized, {
|
|
118
|
+
cwd: context.sessionManager?.getCwd?.(),
|
|
119
|
+
settings: context.settings,
|
|
120
|
+
localProtocolOptions: context.localProtocolOptions,
|
|
121
|
+
});
|
|
122
|
+
return localFile?.path ?? value;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value !== "object" || value === null) return value;
|
|
125
|
+
if (seen.has(value)) return value;
|
|
126
|
+
seen.add(value);
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
let resolved: unknown[] | undefined;
|
|
130
|
+
for (let index = 0; index < value.length; index++) {
|
|
131
|
+
const item = value[index];
|
|
132
|
+
const next = await resolveOutboundLocalUrlArgs(item, context, seen);
|
|
133
|
+
if (next === item && !resolved) continue;
|
|
134
|
+
resolved ??= value.slice();
|
|
135
|
+
resolved[index] = next;
|
|
136
|
+
}
|
|
137
|
+
return resolved ?? value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const input = value as Record<string, unknown>;
|
|
141
|
+
let resolved: Record<string, unknown> | undefined;
|
|
142
|
+
for (const key in input) {
|
|
143
|
+
const item = input[key];
|
|
144
|
+
const next = await resolveOutboundLocalUrlArgs(item, context, seen);
|
|
145
|
+
if (next === item && !resolved) continue;
|
|
146
|
+
resolved ??= { ...input };
|
|
147
|
+
resolved[key] = next;
|
|
148
|
+
}
|
|
149
|
+
return resolved ?? value;
|
|
150
|
+
}
|
|
151
|
+
|
|
107
152
|
/**
|
|
108
153
|
* Normalize raw tool params into the outbound `tools/call` arguments: strip
|
|
109
|
-
* the harness intent field,
|
|
110
|
-
* declares but doesn't require
|
|
154
|
+
* the harness intent field, drop optional empty placeholders the server
|
|
155
|
+
* declares but doesn't require, then translate session-local files to paths
|
|
156
|
+
* external MCP servers can read.
|
|
111
157
|
*/
|
|
112
|
-
function prepareOutboundArgs(
|
|
113
|
-
|
|
158
|
+
async function prepareOutboundArgs(
|
|
159
|
+
params: unknown,
|
|
160
|
+
inputSchema: MCPToolDefinition["inputSchema"],
|
|
161
|
+
context: CustomToolContext,
|
|
162
|
+
): Promise<MCPToolArgs> {
|
|
163
|
+
const args = omitUnusedOptionalArgs(stripHarnessIntent(normalizeToolArgs(params), inputSchema), inputSchema);
|
|
164
|
+
return (await resolveOutboundLocalUrlArgs(args, context)) as MCPToolArgs;
|
|
114
165
|
}
|
|
115
166
|
|
|
116
167
|
/** Details included in MCP tool results for rendering */
|
|
@@ -316,7 +367,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
316
367
|
signal?: AbortSignal,
|
|
317
368
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
318
369
|
throwIfAborted(signal);
|
|
319
|
-
const args = prepareOutboundArgs(params, this.tool.inputSchema);
|
|
370
|
+
const args = await prepareOutboundArgs(params, this.tool.inputSchema, _ctx);
|
|
320
371
|
const provider = this.connection._source?.provider;
|
|
321
372
|
const providerName = this.connection._source?.providerName;
|
|
322
373
|
|
|
@@ -415,7 +466,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
415
466
|
signal?: AbortSignal,
|
|
416
467
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
417
468
|
throwIfAborted(signal);
|
|
418
|
-
const args = prepareOutboundArgs(params, this.tool.inputSchema);
|
|
469
|
+
const args = await prepareOutboundArgs(params, this.tool.inputSchema, _ctx);
|
|
419
470
|
const provider = this.#fallbackProvider;
|
|
420
471
|
const providerName = this.#fallbackProviderName;
|
|
421
472
|
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
buildIrcMessageCard,
|
|
36
36
|
normalizeToolArgs,
|
|
37
37
|
resolveAssistantErrorPresentation,
|
|
38
|
+
splitAssistantMessageToolTimeline,
|
|
38
39
|
} from "../utils/transcript-render-helpers";
|
|
39
40
|
import { createAdvisorMessageCard } from "./advisor-message";
|
|
40
41
|
import { AssistantMessageComponent } from "./assistant-message";
|
|
@@ -273,8 +274,9 @@ export class ChatTranscriptBuilder {
|
|
|
273
274
|
#appendAssistantMessage(message: Extract<AgentMessage, { role: "assistant" }>): void {
|
|
274
275
|
const hideThinkingBlock = this.deps.hideThinkingBlock?.() ?? false;
|
|
275
276
|
const proseOnlyThinking = this.deps.proseOnlyThinking ? this.deps.proseOnlyThinking() : true;
|
|
277
|
+
const timeline = splitAssistantMessageToolTimeline(message);
|
|
276
278
|
const assistantComponent = new AssistantMessageComponent(
|
|
277
|
-
|
|
279
|
+
timeline.beforeTools,
|
|
278
280
|
hideThinkingBlock,
|
|
279
281
|
() => this.deps.requestRender(),
|
|
280
282
|
this.deps.getMessageRenderer ? undefined : [], // placeholder for thinkingRenderers
|
|
@@ -301,11 +303,24 @@ export class ChatTranscriptBuilder {
|
|
|
301
303
|
const errorPresentation = resolveAssistantErrorPresentation(message);
|
|
302
304
|
const hasErrorStop = errorPresentation.kind === "full";
|
|
303
305
|
const errorMessage = hasErrorStop ? errorPresentation.text : null;
|
|
306
|
+
const appendAssistantSegment = (segment: Extract<AgentMessage, { role: "assistant" }> | undefined) => {
|
|
307
|
+
if (!segment || !assistantHasVisibleContent(segment)) return;
|
|
308
|
+
const component = new AssistantMessageComponent(
|
|
309
|
+
segment,
|
|
310
|
+
hideThinkingBlock,
|
|
311
|
+
() => this.deps.requestRender(),
|
|
312
|
+
this.deps.getMessageRenderer ? undefined : [],
|
|
313
|
+
undefined,
|
|
314
|
+
proseOnlyThinking,
|
|
315
|
+
);
|
|
316
|
+
this.container.addChild(component);
|
|
317
|
+
};
|
|
304
318
|
|
|
305
319
|
for (const content of message.content) {
|
|
306
320
|
if (content.type !== "toolCall") continue;
|
|
307
321
|
this.#resolveWaitingPoll(content.name);
|
|
308
322
|
|
|
323
|
+
const afterToolSegment = timeline.afterToolCalls.get(content.id);
|
|
309
324
|
if (
|
|
310
325
|
content.name === "read" &&
|
|
311
326
|
readArgsHaveTarget(content.arguments) &&
|
|
@@ -319,10 +334,15 @@ export class ChatTranscriptBuilder {
|
|
|
319
334
|
false,
|
|
320
335
|
content.id,
|
|
321
336
|
);
|
|
337
|
+
} else if (afterToolSegment) {
|
|
338
|
+
const group = this.#ensureReadGroup();
|
|
339
|
+
group.updateArgs(content.arguments, content.id);
|
|
340
|
+
this.#pendingTools.set(content.id, group);
|
|
322
341
|
} else {
|
|
323
342
|
const normalizedArgs = normalizeToolArgs(content.arguments);
|
|
324
343
|
this.#readArgs.set(content.id, normalizedArgs);
|
|
325
344
|
}
|
|
345
|
+
appendAssistantSegment(afterToolSegment);
|
|
326
346
|
continue;
|
|
327
347
|
}
|
|
328
348
|
|
|
@@ -355,6 +375,7 @@ export class ChatTranscriptBuilder {
|
|
|
355
375
|
} else {
|
|
356
376
|
this.#pendingTools.set(content.id, component);
|
|
357
377
|
}
|
|
378
|
+
appendAssistantSegment(afterToolSegment);
|
|
358
379
|
}
|
|
359
380
|
|
|
360
381
|
this.#pendingUsage =
|
|
@@ -780,7 +780,16 @@ export class StatusLineComponent implements Component {
|
|
|
780
780
|
const activeProvider = session.state.model?.provider ?? session.model?.provider ?? "";
|
|
781
781
|
if (!activeProvider) return "";
|
|
782
782
|
const identity = session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId);
|
|
783
|
-
|
|
783
|
+
// orgId is part of the key: rotating between two same-email Anthropic
|
|
784
|
+
// subscriptions must invalidate the cached usage immediately instead of
|
|
785
|
+
// showing the previous org's quota for the rest of the cache TTL.
|
|
786
|
+
return [
|
|
787
|
+
activeProvider,
|
|
788
|
+
identity?.accountId ?? "",
|
|
789
|
+
identity?.email ?? "",
|
|
790
|
+
identity?.projectId ?? "",
|
|
791
|
+
identity?.orgId ?? "",
|
|
792
|
+
].join("\0");
|
|
784
793
|
}
|
|
785
794
|
|
|
786
795
|
/**
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Container,
|
|
4
4
|
type NativeScrollbackCommittedRows,
|
|
5
5
|
type NativeScrollbackLiveRegion,
|
|
6
|
+
type NativeScrollbackReplay,
|
|
6
7
|
type RenderStablePrefix,
|
|
7
8
|
type ViewportTailProvider,
|
|
8
9
|
} from "@oh-my-pi/pi-tui";
|
|
@@ -119,6 +120,8 @@ interface BlockSegment {
|
|
|
119
120
|
sep: number;
|
|
120
121
|
/** Whether the block reported finalized when this segment was rendered. */
|
|
121
122
|
finalized: boolean;
|
|
123
|
+
/** Safe to drop after commit: produced while finalized, without post-finalize version tracking. */
|
|
124
|
+
compactable: boolean;
|
|
122
125
|
/** Block version observed when this segment was rendered (see {@link FinalizableBlock}). */
|
|
123
126
|
version: number | undefined;
|
|
124
127
|
}
|
|
@@ -154,7 +157,12 @@ const EMPTY_TAIL: readonly string[] = [];
|
|
|
154
157
|
*/
|
|
155
158
|
export class TranscriptContainer
|
|
156
159
|
extends Container
|
|
157
|
-
implements
|
|
160
|
+
implements
|
|
161
|
+
NativeScrollbackLiveRegion,
|
|
162
|
+
NativeScrollbackCommittedRows,
|
|
163
|
+
NativeScrollbackReplay,
|
|
164
|
+
RenderStablePrefix,
|
|
165
|
+
ViewportTailProvider
|
|
158
166
|
{
|
|
159
167
|
// Bumped to retire every block segment at once (theme change / clear); a
|
|
160
168
|
// segment is only reused when its stored generation matches.
|
|
@@ -172,6 +180,14 @@ export class TranscriptContainer
|
|
|
172
180
|
// Finalized blocks wholly before this boundary are immutable on-screen history;
|
|
173
181
|
// their previous contribution can be replayed without calling render().
|
|
174
182
|
#committedRows = 0;
|
|
183
|
+
// Leading children whose rows were handed to native scrollback and dropped
|
|
184
|
+
// from the local frame. Children remain owned by the session and can be
|
|
185
|
+
// re-rendered when the TUI prepares a destructive full replay.
|
|
186
|
+
#compactedChildStart = 0;
|
|
187
|
+
// Suppresses re-compaction for the rehydrating render. The TUI feeds its old
|
|
188
|
+
// committed-row count immediately before render, so resetting that count
|
|
189
|
+
// alone cannot distinguish a replay from an ordinary update.
|
|
190
|
+
#replayPending = false;
|
|
175
191
|
// Stable-prefix floor accumulated across renders since the last
|
|
176
192
|
// getRenderStablePrefixRows() read (see RenderStablePrefix: reading
|
|
177
193
|
// consumes the report and re-bases the baseline). Out-of-band renders
|
|
@@ -187,12 +203,24 @@ export class TranscriptContainer
|
|
|
187
203
|
override clear(): void {
|
|
188
204
|
this.#generation++;
|
|
189
205
|
super.clear();
|
|
206
|
+
this.#compactedChildStart = 0;
|
|
207
|
+
this.#committedRows = 0;
|
|
208
|
+
this.#replayPending = false;
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
setNativeScrollbackCommittedRows(rows: number): void {
|
|
193
212
|
this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
|
|
194
213
|
}
|
|
195
214
|
|
|
215
|
+
prepareNativeScrollbackReplay(): void {
|
|
216
|
+
if (this.#compactedChildStart === 0) return;
|
|
217
|
+
this.#compactedChildStart = 0;
|
|
218
|
+
this.#replayPending = true;
|
|
219
|
+
this.#generation++;
|
|
220
|
+
this.#lines.length = 0;
|
|
221
|
+
this.#stableRowsFloor = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
getRenderStablePrefixRows(): number {
|
|
197
225
|
const value = Math.min(this.#stableRowsFloor, this.#lines.length);
|
|
198
226
|
this.#stableRowsFloor = this.#lines.length;
|
|
@@ -213,8 +241,14 @@ export class TranscriptContainer
|
|
|
213
241
|
* committed rows and is safely removable.
|
|
214
242
|
*/
|
|
215
243
|
isBlockUncommitted(component: Component): boolean {
|
|
244
|
+
const index = this.children.indexOf(component);
|
|
245
|
+
// Compacted prefix is already committed native history and must not be
|
|
246
|
+
// retracted. Compacted slots may be sparse holes after a later re-render
|
|
247
|
+
// (render only fills from #compactedChildStart), so the loop below must
|
|
248
|
+
// skip undefined entries.
|
|
249
|
+
if (index >= 0 && index < this.#compactedChildStart) return false;
|
|
216
250
|
for (const segment of this.#segments) {
|
|
217
|
-
if (segment.component !== component) continue;
|
|
251
|
+
if (segment === undefined || segment.component !== component) continue;
|
|
218
252
|
return segment.rowCount === 0 || segment.startRow >= this.#committedRows;
|
|
219
253
|
}
|
|
220
254
|
return true;
|
|
@@ -268,7 +302,7 @@ export class TranscriptContainer
|
|
|
268
302
|
if (maxRows <= 0) return EMPTY_TAIL;
|
|
269
303
|
const collected: (readonly string[])[] = [];
|
|
270
304
|
let total = 0;
|
|
271
|
-
for (let i = this.children.length - 1; i >=
|
|
305
|
+
for (let i = this.children.length - 1; i >= this.#compactedChildStart && total < maxRows; i--) {
|
|
272
306
|
const contribution = stripPlainBlankEdges(this.children[i]!.render(width));
|
|
273
307
|
if (contribution.length === 0) continue;
|
|
274
308
|
// One blank separator sits between this block and the (already
|
|
@@ -292,6 +326,7 @@ export class TranscriptContainer
|
|
|
292
326
|
this.#nativeScrollbackLiveRegionStart = undefined;
|
|
293
327
|
|
|
294
328
|
const count = this.children.length;
|
|
329
|
+
if (this.#compactedChildStart > count) this.#compactedChildStart = count;
|
|
295
330
|
|
|
296
331
|
// Seal displaceable snapshots whose rows are already on the tape (per the
|
|
297
332
|
// previous frame's segments — the geometry the committed count was
|
|
@@ -301,8 +336,9 @@ export class TranscriptContainer
|
|
|
301
336
|
// frame, and every frame so a block that BECAME displaceable after its
|
|
302
337
|
// pending-preview rows committed (late result on a scrolled-off call) is
|
|
303
338
|
// caught too.
|
|
304
|
-
for (let i =
|
|
305
|
-
const previous = this.#segments[i]
|
|
339
|
+
for (let i = this.#compactedChildStart; i < count && i < this.#segments.length; i++) {
|
|
340
|
+
const previous = this.#segments[i];
|
|
341
|
+
if (previous === undefined) continue;
|
|
306
342
|
if (previous.startRow >= this.#committedRows) break;
|
|
307
343
|
if (previous.rowCount === 0 || previous.component !== this.children[i]) continue;
|
|
308
344
|
sealCommittedSnapshot(previous.component);
|
|
@@ -316,7 +352,7 @@ export class TranscriptContainer
|
|
|
316
352
|
// reaches.
|
|
317
353
|
let liveStartIndex = -1;
|
|
318
354
|
let hasLiveBlock = false;
|
|
319
|
-
for (let i =
|
|
355
|
+
for (let i = this.#compactedChildStart; i < count; i++) {
|
|
320
356
|
if (!isBlockFinalized(this.children[i]!)) {
|
|
321
357
|
liveStartIndex = i;
|
|
322
358
|
hasLiveBlock = true;
|
|
@@ -348,7 +384,7 @@ export class TranscriptContainer
|
|
|
348
384
|
// Frame row cursor: rows emitted (reused or pushed) so far.
|
|
349
385
|
let row = 0;
|
|
350
386
|
let stableRows = 0;
|
|
351
|
-
for (let i =
|
|
387
|
+
for (let i = this.#compactedChildStart; i < count; i++) {
|
|
352
388
|
const child = this.children[i]!;
|
|
353
389
|
|
|
354
390
|
// This child's contribution: its current render with plain-blank
|
|
@@ -389,6 +425,7 @@ export class TranscriptContainer
|
|
|
389
425
|
previous.width === width &&
|
|
390
426
|
previous.generation === this.#generation);
|
|
391
427
|
const contribution = reusable ? previous.contribution : stripPlainBlankEdges(raw);
|
|
428
|
+
const compactable = finalized && version === undefined && previous?.finalized !== false;
|
|
392
429
|
|
|
393
430
|
// Empty (or stripped-to-nothing) children contribute nothing and never
|
|
394
431
|
// affect spacing. An empty still-live child still gates the commit
|
|
@@ -413,6 +450,7 @@ export class TranscriptContainer
|
|
|
413
450
|
rowCount: 0,
|
|
414
451
|
sep: 0,
|
|
415
452
|
finalized,
|
|
453
|
+
compactable,
|
|
416
454
|
version,
|
|
417
455
|
};
|
|
418
456
|
continue;
|
|
@@ -464,6 +502,7 @@ export class TranscriptContainer
|
|
|
464
502
|
rowCount,
|
|
465
503
|
sep,
|
|
466
504
|
finalized,
|
|
505
|
+
compactable,
|
|
467
506
|
version,
|
|
468
507
|
};
|
|
469
508
|
row += rowCount;
|
|
@@ -473,8 +512,72 @@ export class TranscriptContainer
|
|
|
473
512
|
if (lines.length !== row) lines.length = row;
|
|
474
513
|
this.#segments = segments;
|
|
475
514
|
this.#stableRowsFloor = Math.min(stableFloorBefore, stableRows, row);
|
|
515
|
+
if (this.#replayPending) {
|
|
516
|
+
this.#replayPending = false;
|
|
517
|
+
} else {
|
|
518
|
+
this.#compactCommittedPrefix();
|
|
519
|
+
}
|
|
476
520
|
return lines;
|
|
477
521
|
}
|
|
522
|
+
|
|
523
|
+
#compactCommittedPrefix(): void {
|
|
524
|
+
if (this.#committedRows <= 0 || this.#compactedChildStart >= this.children.length) return;
|
|
525
|
+
const lines = this.#lines;
|
|
526
|
+
const segments = this.#segments;
|
|
527
|
+
let dropRows = 0;
|
|
528
|
+
let dropUntil = this.#compactedChildStart;
|
|
529
|
+
for (let i = this.#compactedChildStart; i < segments.length; i++) {
|
|
530
|
+
const segment = segments[i];
|
|
531
|
+
if (segment === undefined || !segment.compactable) break;
|
|
532
|
+
const segmentEnd = segment.startRow + segment.rowCount;
|
|
533
|
+
if (segmentEnd > this.#committedRows) break;
|
|
534
|
+
dropRows = segmentEnd;
|
|
535
|
+
dropUntil = i + 1;
|
|
536
|
+
}
|
|
537
|
+
const retained = segments[dropUntil];
|
|
538
|
+
if (retained !== undefined && retained.sep > 0) {
|
|
539
|
+
const committedSeparatorRows = this.#committedRows - retained.startRow;
|
|
540
|
+
if (committedSeparatorRows <= 0) {
|
|
541
|
+
dropRows = 0;
|
|
542
|
+
dropUntil = this.#compactedChildStart;
|
|
543
|
+
} else {
|
|
544
|
+
const trim = Math.min(retained.sep, committedSeparatorRows);
|
|
545
|
+
dropRows += trim;
|
|
546
|
+
retained.sep -= trim;
|
|
547
|
+
retained.rowCount -= trim;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (dropRows === 0) return;
|
|
551
|
+
|
|
552
|
+
lines.splice(0, dropRows);
|
|
553
|
+
for (let i = this.#compactedChildStart; i < dropUntil; i++) {
|
|
554
|
+
const segment = segments[i];
|
|
555
|
+
if (segment === undefined) continue;
|
|
556
|
+
segments[i] = {
|
|
557
|
+
component: segment.component,
|
|
558
|
+
rawRef: EMPTY_TAIL,
|
|
559
|
+
contribution: EMPTY_TAIL,
|
|
560
|
+
width: segment.width,
|
|
561
|
+
generation: segment.generation,
|
|
562
|
+
startRow: 0,
|
|
563
|
+
rowCount: 0,
|
|
564
|
+
sep: 0,
|
|
565
|
+
finalized: true,
|
|
566
|
+
compactable: false,
|
|
567
|
+
version: segment.version,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
for (let i = dropUntil; i < segments.length; i++) {
|
|
571
|
+
const segment = segments[i];
|
|
572
|
+
if (segment !== undefined) segment.startRow -= dropRows;
|
|
573
|
+
}
|
|
574
|
+
this.#compactedChildStart = dropUntil;
|
|
575
|
+
this.#committedRows = Math.max(0, this.#committedRows - dropRows);
|
|
576
|
+
this.#stableRowsFloor = 0;
|
|
577
|
+
if (this.#nativeScrollbackLiveRegionStart !== undefined) {
|
|
578
|
+
this.#nativeScrollbackLiveRegionStart = Math.max(0, this.#nativeScrollbackLiveRegionStart - dropRows);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
478
581
|
}
|
|
479
582
|
|
|
480
583
|
/**
|
|
@@ -1388,14 +1388,22 @@ function formatWindowSuffix(label: string, windowLabel: string, uiTheme: typeof
|
|
|
1388
1388
|
return uiTheme.fg("dim", `(${windowLabel})`);
|
|
1389
1389
|
}
|
|
1390
1390
|
|
|
1391
|
+
/** ` (org)` suffix when the report is org-attributed — two subscriptions can share one email. */
|
|
1392
|
+
function orgSuffix(report: UsageReport): string {
|
|
1393
|
+
const orgName = report.metadata?.orgName;
|
|
1394
|
+
const orgId = report.metadata?.orgId;
|
|
1395
|
+
const org = typeof orgName === "string" && orgName ? orgName : typeof orgId === "string" ? orgId : undefined;
|
|
1396
|
+
return org ? ` (${org})` : "";
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1391
1399
|
function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: number): string {
|
|
1392
1400
|
const email = report.metadata?.email;
|
|
1393
|
-
if (typeof email === "string" && email) return email
|
|
1401
|
+
if (typeof email === "string" && email) return `${email}${orgSuffix(report)}`;
|
|
1394
1402
|
const accountId =
|
|
1395
1403
|
typeof report.metadata?.accountId === "string" && report.metadata.accountId
|
|
1396
1404
|
? report.metadata.accountId
|
|
1397
1405
|
: limit.scope.accountId || undefined;
|
|
1398
|
-
if (accountId) return accountId
|
|
1406
|
+
if (accountId) return `${accountId}${orgSuffix(report)}`;
|
|
1399
1407
|
const projectId =
|
|
1400
1408
|
typeof report.metadata?.projectId === "string" && report.metadata.projectId
|
|
1401
1409
|
? report.metadata.projectId
|
|
@@ -1406,9 +1414,9 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
|
|
|
1406
1414
|
|
|
1407
1415
|
function formatUnlimitedReportLabel(report: UsageReport, index: number): string {
|
|
1408
1416
|
const email = report.metadata?.email;
|
|
1409
|
-
if (typeof email === "string" && email) return email
|
|
1417
|
+
if (typeof email === "string" && email) return `${email}${orgSuffix(report)}`;
|
|
1410
1418
|
const accountId = report.metadata?.accountId;
|
|
1411
|
-
if (typeof accountId === "string" && accountId) return accountId
|
|
1419
|
+
if (typeof accountId === "string" && accountId) return `${accountId}${orgSuffix(report)}`;
|
|
1412
1420
|
const projectId = report.metadata?.projectId;
|
|
1413
1421
|
if (typeof projectId === "string" && projectId) return projectId;
|
|
1414
1422
|
return `account ${index + 1}`;
|