@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.8
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 +59 -0
- package/dist/cli.js +4967 -4948
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +16 -0
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/mcp/tool-bridge.d.ts +9 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +78 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/package.json +12 -13
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/config/model-registry.ts +47 -24
- package/src/config/model-resolver.ts +56 -31
- package/src/config/settings-schema.ts +5 -0
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +15 -5
- package/src/mcp/tool-bridge.ts +9 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +22 -5
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/sdk.ts +134 -48
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +300 -26
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +41 -27
- package/src/task/index.ts +11 -0
- package/src/task/prewalk.ts +6 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
package/src/main.ts
CHANGED
|
@@ -882,6 +882,7 @@ export async function buildSessionOptions(
|
|
|
882
882
|
cliProvider: parsed.provider,
|
|
883
883
|
cliModel: parsed.model,
|
|
884
884
|
modelRegistry,
|
|
885
|
+
availableModels: modelRegistry.getAvailable(),
|
|
885
886
|
settings: activeSettings,
|
|
886
887
|
preferences: modelMatchPreferences,
|
|
887
888
|
});
|
|
@@ -954,13 +955,22 @@ export async function buildSessionOptions(
|
|
|
954
955
|
if (resolved.warning) {
|
|
955
956
|
process.stderr.write(`${chalk.yellow(`Warning: ${resolved.warning}`)}\n`);
|
|
956
957
|
}
|
|
958
|
+
// Prewalk is an optional optimization (off by default): switch to a fast
|
|
959
|
+
// model at the first edit. If its hand-off target can't be resolved or has
|
|
960
|
+
// no configured auth, warn and leave prewalk unarmed rather than aborting
|
|
961
|
+
// startup and locking the user out of the app (issue #6064).
|
|
957
962
|
if (resolved.error || !resolved.model) {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
963
|
+
const target = parsed.prewalkInto ?? DEFAULT_PREWALK_TARGET;
|
|
964
|
+
process.stderr.write(
|
|
965
|
+
`${chalk.yellow(`Warning: prewalk disabled — ${resolved.error ?? `model "${target}" not found`}`)}\n`,
|
|
966
|
+
);
|
|
967
|
+
} else if (!modelRegistry.hasConfiguredAuth(resolved.model)) {
|
|
968
|
+
process.stderr.write(
|
|
969
|
+
`${chalk.yellow(`Warning: prewalk disabled — no API key for ${resolved.model.provider}/${resolved.model.id}`)}\n`,
|
|
970
|
+
);
|
|
971
|
+
} else {
|
|
972
|
+
options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
962
973
|
}
|
|
963
|
-
options.prewalk = { target: resolved.model, thinkingLevel: resolved.thinkingLevel };
|
|
964
974
|
}
|
|
965
975
|
|
|
966
976
|
if (parsed.planYoloInto !== undefined && !parsed.planYolo) {
|
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -332,6 +332,13 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
332
332
|
readonly approval = "write" as const;
|
|
333
333
|
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
334
334
|
readonly mergeCallAndResult = true;
|
|
335
|
+
/**
|
|
336
|
+
* MCP-backed tools opt out of strict structured-output grammar. The server
|
|
337
|
+
* owns validation, and strict mode makes OpenAI-family models over-fill
|
|
338
|
+
* mutually exclusive optional fields (#4336/#4340). Serializers preserve an
|
|
339
|
+
* explicit `false`; an omitted flag would leave nothing to preserve.
|
|
340
|
+
*/
|
|
341
|
+
readonly strict = false as const;
|
|
335
342
|
|
|
336
343
|
/** Create MCPTool instances for all tools from an MCP server connection */
|
|
337
344
|
static fromTools(connection: MCPServerConnection, tools: MCPToolDefinition[], reconnect?: MCPReconnect): MCPTool[] {
|
|
@@ -418,6 +425,8 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
418
425
|
readonly approval = "write" as const;
|
|
419
426
|
/** Render completed MCP calls with the result header replacing the pending call header. */
|
|
420
427
|
readonly mergeCallAndResult = true;
|
|
428
|
+
/** See {@link MCPTool.strict}: MCP servers own validation, so stay non-strict. */
|
|
429
|
+
readonly strict = false as const;
|
|
421
430
|
|
|
422
431
|
readonly #fallbackProvider: string | undefined;
|
|
423
432
|
readonly #fallbackProviderName: string | undefined;
|
package/src/mnemopi/state.ts
CHANGED
|
@@ -158,6 +158,39 @@ export interface MnemopiScopedMemoryHit {
|
|
|
158
158
|
|
|
159
159
|
type MnemopiRetentionMessage = { role: string; content: string };
|
|
160
160
|
|
|
161
|
+
interface MnemopiRetentionCursorRow {
|
|
162
|
+
content: string;
|
|
163
|
+
sourceId: string | null;
|
|
164
|
+
retainedThroughUserTurn: number | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function countRetainedUserTurns(transcript: string): number {
|
|
168
|
+
let turns = 0;
|
|
169
|
+
for (const line of transcript.split(/\r?\n/)) {
|
|
170
|
+
if (line === "[role: user]") turns++;
|
|
171
|
+
}
|
|
172
|
+
return turns;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function deriveRetainedTurnCursor(rows: readonly MnemopiRetentionCursorRow[], sessionId: string): number {
|
|
176
|
+
let cursor = 0;
|
|
177
|
+
for (const row of rows) {
|
|
178
|
+
if (Number.isInteger(row.retainedThroughUserTurn) && row.retainedThroughUserTurn !== null) {
|
|
179
|
+
cursor = Math.max(cursor, row.retainedThroughUserTurn);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (row.sourceId !== sessionId && !row.sourceId?.startsWith(`${sessionId}-`)) continue;
|
|
183
|
+
// Legacy rows carry no explicit cursor. Summing incremental rows looks
|
|
184
|
+
// right, but pre-fix resumed sessions also wrote cumulative rows under the
|
|
185
|
+
// incremental `${sessionId}-<ts>` id shape, so a sum can overshoot the real
|
|
186
|
+
// retained prefix and permanently skip unseen turns. Per-row max can only
|
|
187
|
+
// under-count, which at worst re-stores one suffix before an explicit
|
|
188
|
+
// cursor row takes over.
|
|
189
|
+
cursor = Math.max(cursor, countRetainedUserTurns(row.content));
|
|
190
|
+
}
|
|
191
|
+
return cursor;
|
|
192
|
+
}
|
|
193
|
+
|
|
161
194
|
function sliceUnretainedMessages(
|
|
162
195
|
messages: MnemopiRetentionMessage[],
|
|
163
196
|
lastRetainedTurn: number,
|
|
@@ -208,6 +241,7 @@ export class MnemopiSessionState {
|
|
|
208
241
|
hasRecalledForFirstTurn: boolean;
|
|
209
242
|
lastRecallSnippet?: string;
|
|
210
243
|
unsubscribe?: () => void;
|
|
244
|
+
#retentionCursorLoaded = false;
|
|
211
245
|
|
|
212
246
|
constructor(options: MnemopiSessionStateOptions) {
|
|
213
247
|
this.sessionId = options.sessionId;
|
|
@@ -222,11 +256,15 @@ export class MnemopiSessionState {
|
|
|
222
256
|
}
|
|
223
257
|
|
|
224
258
|
setSessionId(sessionId: string): void {
|
|
259
|
+
if (this.sessionId === sessionId) return;
|
|
225
260
|
this.sessionId = sessionId;
|
|
261
|
+
this.lastRetainedTurn = 0;
|
|
262
|
+
this.#retentionCursorLoaded = false;
|
|
226
263
|
}
|
|
227
264
|
|
|
228
265
|
resetConversationTracking(): void {
|
|
229
266
|
this.lastRetainedTurn = 0;
|
|
267
|
+
this.#retentionCursorLoaded = false;
|
|
230
268
|
this.hasRecalledForFirstTurn = false;
|
|
231
269
|
this.lastRecallSnippet = undefined;
|
|
232
270
|
}
|
|
@@ -445,11 +483,13 @@ export class MnemopiSessionState {
|
|
|
445
483
|
async maybeRetainOnAgentEnd(_messages: AgentMessage[]): Promise<void> {
|
|
446
484
|
if (!this.config.autoRetain || this.aliasOf) return;
|
|
447
485
|
const flat = extractMessages(this.session.sessionManager);
|
|
486
|
+
this.#restoreRetainedTurnCursor();
|
|
448
487
|
const userTurns = flat.filter(message => message.role === "user").length;
|
|
449
488
|
if (userTurns - this.lastRetainedTurn < this.config.retainEveryNTurns) return;
|
|
450
489
|
await this.retainMessages(
|
|
451
490
|
sliceUnretainedMessages(flat, this.lastRetainedTurn),
|
|
452
491
|
`${this.sessionId}-${Date.now()}`,
|
|
492
|
+
{ retainedThroughUserTurn: userTurns },
|
|
453
493
|
);
|
|
454
494
|
this.lastRetainedTurn = userTurns;
|
|
455
495
|
}
|
|
@@ -457,14 +497,19 @@ export class MnemopiSessionState {
|
|
|
457
497
|
async forceRetainCurrentSession(options: { extract?: boolean } = {}): Promise<void> {
|
|
458
498
|
if (this.aliasOf) return;
|
|
459
499
|
const flat = extractMessages(this.session.sessionManager);
|
|
460
|
-
|
|
461
|
-
|
|
500
|
+
this.#restoreRetainedTurnCursor();
|
|
501
|
+
const userTurns = flat.filter(message => message.role === "user").length;
|
|
502
|
+
await this.retainMessages(sliceUnretainedMessages(flat, this.lastRetainedTurn), this.sessionId, {
|
|
503
|
+
...options,
|
|
504
|
+
retainedThroughUserTurn: userTurns,
|
|
505
|
+
});
|
|
506
|
+
this.lastRetainedTurn = Math.max(this.lastRetainedTurn, userTurns);
|
|
462
507
|
}
|
|
463
508
|
|
|
464
509
|
async retainMessages(
|
|
465
510
|
messages: Array<{ role: string; content: string }>,
|
|
466
511
|
sourceId: string,
|
|
467
|
-
options: { extract?: boolean } = {},
|
|
512
|
+
options: { extract?: boolean; retainedThroughUserTurn?: number } = {},
|
|
468
513
|
): Promise<void> {
|
|
469
514
|
const { transcript, messageCount } = prepareRetentionTranscript(messages, true);
|
|
470
515
|
if (!transcript) return;
|
|
@@ -478,6 +523,9 @@ export class MnemopiSessionState {
|
|
|
478
523
|
session_id: this.sessionId,
|
|
479
524
|
source_id: sourceId,
|
|
480
525
|
message_count: messageCount,
|
|
526
|
+
...(options.retainedThroughUserTurn === undefined
|
|
527
|
+
? {}
|
|
528
|
+
: { retained_through_user_turn: options.retainedThroughUserTurn }),
|
|
481
529
|
cwd: this.session.sessionManager.getCwd(),
|
|
482
530
|
},
|
|
483
531
|
scope: "bank",
|
|
@@ -490,6 +538,25 @@ export class MnemopiSessionState {
|
|
|
490
538
|
});
|
|
491
539
|
}
|
|
492
540
|
|
|
541
|
+
#restoreRetainedTurnCursor(): void {
|
|
542
|
+
if (this.#retentionCursorLoaded) return;
|
|
543
|
+
this.#retentionCursorLoaded = true;
|
|
544
|
+
const rows = this.memory.beam.db
|
|
545
|
+
.prepare<MnemopiRetentionCursorRow, [string]>(`
|
|
546
|
+
SELECT
|
|
547
|
+
content,
|
|
548
|
+
json_extract(metadata_json, '$.source_id') AS sourceId,
|
|
549
|
+
CAST(json_extract(metadata_json, '$.retained_through_user_turn') AS INTEGER)
|
|
550
|
+
AS retainedThroughUserTurn
|
|
551
|
+
FROM working_memory
|
|
552
|
+
WHERE source = 'coding-agent-transcript'
|
|
553
|
+
AND json_extract(metadata_json, '$.session_id') = ?
|
|
554
|
+
ORDER BY rowid
|
|
555
|
+
`)
|
|
556
|
+
.all(this.sessionId);
|
|
557
|
+
this.lastRetainedTurn = Math.max(this.lastRetainedTurn, deriveRetainedTurnCursor(rows, this.sessionId));
|
|
558
|
+
}
|
|
559
|
+
|
|
493
560
|
attachSessionListeners(): void {
|
|
494
561
|
this.unsubscribe?.();
|
|
495
562
|
this.unsubscribe = this.session.subscribe((event: AgentSessionEvent) => {
|
|
@@ -49,6 +49,7 @@ import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-ar
|
|
|
49
49
|
import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
|
|
50
50
|
import { createAgentSession } from "../../sdk";
|
|
51
51
|
import { discoverAgents } from "../../task/discovery";
|
|
52
|
+
import { resolveAgentPrewalkDefault } from "../../task/prewalk";
|
|
52
53
|
import type { AgentDefinition, AgentSource } from "../../task/types";
|
|
53
54
|
import { shortenPath } from "../../tools/render-utils";
|
|
54
55
|
import { getEditorTheme, theme } from "../theme/theme";
|
|
@@ -602,7 +603,7 @@ export class AgentDashboard extends Container {
|
|
|
602
603
|
this.#persistPrewalkOverrides();
|
|
603
604
|
const pattern = resolveAgentPrewalkPattern({
|
|
604
605
|
settingsOverride: selected.prewalkOverride,
|
|
605
|
-
agentPrewalk: selected.prewalk,
|
|
606
|
+
agentPrewalk: resolveAgentPrewalkDefault(selected, this.#settingsManager?.get("task.prewalk") ?? false),
|
|
606
607
|
});
|
|
607
608
|
const state = selected.prewalkOverride ?? "agent default";
|
|
608
609
|
this.#notice = `Prewalk for ${selected.name}: ${state}${pattern ? ` (into ${pattern})` : ""}`;
|
|
@@ -1081,7 +1082,10 @@ export class AgentDashboard extends Container {
|
|
|
1081
1082
|
const prewalkPattern = selected
|
|
1082
1083
|
? resolveAgentPrewalkPattern({
|
|
1083
1084
|
settingsOverride: selected.prewalkOverride,
|
|
1084
|
-
agentPrewalk:
|
|
1085
|
+
agentPrewalk: resolveAgentPrewalkDefault(
|
|
1086
|
+
selected,
|
|
1087
|
+
this.#settingsManager?.get("task.prewalk") ?? false,
|
|
1088
|
+
),
|
|
1085
1089
|
})
|
|
1086
1090
|
: undefined;
|
|
1087
1091
|
const prewalkResolution = prewalkPattern ? this.#resolvePatterns([prewalkPattern]) : undefined;
|
|
@@ -56,7 +56,7 @@ import { SkillMessageComponent } from "./skill-message";
|
|
|
56
56
|
import { ToolExecutionComponent } from "./tool-execution";
|
|
57
57
|
import { TranscriptContainer } from "./transcript-container";
|
|
58
58
|
import { createUsageRowBlock } from "./usage-row";
|
|
59
|
-
import { UserMessageComponent } from "./user-message";
|
|
59
|
+
import { CollapsedSyntheticMessageComponent, UserMessageComponent } from "./user-message";
|
|
60
60
|
|
|
61
61
|
export interface ChatTranscriptBuilderDeps {
|
|
62
62
|
ui: TUI;
|
|
@@ -233,7 +233,18 @@ export class ChatTranscriptBuilder {
|
|
|
233
233
|
const textContent = message.role === "user" ? userMessageText(message) : "";
|
|
234
234
|
if (textContent) {
|
|
235
235
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
236
|
-
|
|
236
|
+
// Synthetic (agent-attributed) inputs — chiefly the advisor's `Session
|
|
237
|
+
// update` replay dumps — can be hundreds of KiB of Markdown each.
|
|
238
|
+
// Rendering their full body on cold open blocked the TUI (issue #6308);
|
|
239
|
+
// collapse them behind a compact summary that builds Markdown only on
|
|
240
|
+
// ctrl+o expand. Real user prompts stay fully rendered.
|
|
241
|
+
if (isSynthetic) {
|
|
242
|
+
const collapsed = new CollapsedSyntheticMessageComponent(textContent);
|
|
243
|
+
this.#trackExpandable(collapsed);
|
|
244
|
+
this.container.addChild(collapsed);
|
|
245
|
+
} else {
|
|
246
|
+
this.container.addChild(new UserMessageComponent(textContent, false));
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
break;
|
|
239
250
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { diffWords } from "@oh-my-pi/pi-natives";
|
|
1
2
|
import { DEFAULT_TAB_WIDTH, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
2
|
-
import * as Diff from "diff";
|
|
3
3
|
import { getLanguageFromPath, highlightCode, theme } from "../../modes/theme/theme";
|
|
4
4
|
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../../tools/render-utils";
|
|
5
5
|
|
|
@@ -53,7 +53,7 @@ function parseDiffLine(line: string): { prefix: CodeFrameMarker; lineNum: string
|
|
|
53
53
|
* Strips leading whitespace from inverse to avoid highlighting indentation.
|
|
54
54
|
*/
|
|
55
55
|
function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } {
|
|
56
|
-
const wordDiff =
|
|
56
|
+
const wordDiff = diffWords(oldContent, newContent);
|
|
57
57
|
|
|
58
58
|
let removedLine = "";
|
|
59
59
|
let addedLine = "";
|
|
@@ -152,6 +152,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
152
152
|
* motion mouse reports and cleared when the pointer leaves the option rows. */
|
|
153
153
|
#hoveredOption: number | undefined;
|
|
154
154
|
|
|
155
|
+
// Once a choice fires, the promise-based caller resolves but keeps this
|
|
156
|
+
// overlay mounted while it runs slow async approval work (e.g. context
|
|
157
|
+
// compaction). Lock input and switch to a "submitting" indicator so the
|
|
158
|
+
// overlay stops looking interactive and repeat Enter/Esc are not silently
|
|
159
|
+
// swallowed with zero feedback (#5926).
|
|
160
|
+
#committed = false;
|
|
161
|
+
/** Label of the committed choice, shown while the async approval settles. */
|
|
162
|
+
#committedLabel: string | undefined;
|
|
155
163
|
#annotating = false;
|
|
156
164
|
#input: Input;
|
|
157
165
|
|
|
@@ -283,11 +291,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
283
291
|
#confirmSelection(): void {
|
|
284
292
|
const index = this.#selectedIndex;
|
|
285
293
|
if (index >= 0 && index < this.#options.length && !this.#disabled.has(index)) {
|
|
294
|
+
this.#committed = true;
|
|
295
|
+
this.#committedLabel = this.#options[index]!;
|
|
286
296
|
this.callbacks.onPick(this.#options[index]!);
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
299
|
|
|
290
300
|
handleInput(keyData: string): void {
|
|
301
|
+
if (this.#committed) return;
|
|
291
302
|
if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) return;
|
|
292
303
|
if (this.#annotating) {
|
|
293
304
|
if (this.callbacks.onAnnotationExternalEditor && matchesAppExternalEditor(keyData)) {
|
|
@@ -300,6 +311,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
300
311
|
return;
|
|
301
312
|
}
|
|
302
313
|
if (matchesSelectCancel(keyData)) {
|
|
314
|
+
this.#committed = true;
|
|
303
315
|
this.callbacks.onCancel();
|
|
304
316
|
return;
|
|
305
317
|
}
|
|
@@ -838,10 +850,14 @@ export class PlanReviewOverlay implements Component {
|
|
|
838
850
|
const innerWidth = Math.max(1, width - 4);
|
|
839
851
|
const bodyContentWidth = sidebarShown ? splitBodyWidth(width, sidebarWidth) : innerWidth;
|
|
840
852
|
|
|
841
|
-
const
|
|
842
|
-
const
|
|
853
|
+
const committed = this.#committed;
|
|
854
|
+
const sliderLines = committed ? [] : this.#renderSliderLines();
|
|
855
|
+
const submittingLabel = this.#committedLabel ? `${this.#committedLabel} — submitting…` : "Submitting…";
|
|
856
|
+
const optionLines = committed ? [theme.bold(theme.fg("accent", submittingLabel))] : this.#renderOptionLines();
|
|
843
857
|
const promptLines = this.#promptTitle ? [theme.bold(theme.fg("accent", this.#promptTitle))] : [];
|
|
844
|
-
const footerLines =
|
|
858
|
+
const footerLines = committed
|
|
859
|
+
? [theme.fg("dim", "Applying your selection — this can take a moment while context is prepared.")]
|
|
860
|
+
: this.#renderFooterLines(innerWidth);
|
|
845
861
|
|
|
846
862
|
// Chrome rows: top border, two dividers, bottom border, plus the
|
|
847
863
|
// prompt/slider/option/footer rows between them.
|
|
@@ -884,7 +900,7 @@ export class PlanReviewOverlay implements Component {
|
|
|
884
900
|
for (const line of promptLines) out.push(row(line, width));
|
|
885
901
|
for (const line of sliderLines) out.push(row(line, width));
|
|
886
902
|
for (let i = 0; i < optionLines.length; i++) {
|
|
887
|
-
this.#optionClickRows.set(out.length, i);
|
|
903
|
+
if (!committed) this.#optionClickRows.set(out.length, i);
|
|
888
904
|
out.push(row(optionLines[i]!, width));
|
|
889
905
|
}
|
|
890
906
|
out.push(divider(width));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Container, Markdown } from "@oh-my-pi/pi-tui";
|
|
1
|
+
import { type Component, Container, Markdown } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { formatBytes } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
3
4
|
import { imageReferenceHyperlink, renderPlaceholders } from "../image-references";
|
|
4
5
|
import { highlightMagicKeywords } from "../magic-keywords";
|
|
@@ -66,3 +67,101 @@ export class UserMessageComponent extends Container {
|
|
|
66
67
|
return wrapped;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Collapsed placeholder for a synthetic (agent-attributed) user input in the
|
|
73
|
+
* file/remote-backed transcript viewer — chiefly the advisor's `Session update`
|
|
74
|
+
* replay dumps, which can each be hundreds of KiB of Markdown and, on cold open,
|
|
75
|
+
* blocked the TUI for tens of seconds while every historical body was laid out
|
|
76
|
+
* before the viewport clip (issue #6308).
|
|
77
|
+
*
|
|
78
|
+
* Collapsed by default: renders one dim summary row (label · size · line count ·
|
|
79
|
+
* expand hint) and builds NO Markdown. The heavy {@link UserMessageComponent} is
|
|
80
|
+
* constructed lazily only when expanded via `ctrl+o`, so blocks above the
|
|
81
|
+
* viewport never pay layout cost until the reader asks to see them. The raw
|
|
82
|
+
* observability data stays intact in `__advisor.jsonl`.
|
|
83
|
+
*/
|
|
84
|
+
export class CollapsedSyntheticMessageComponent implements Component {
|
|
85
|
+
#expanded = false;
|
|
86
|
+
#cache?: { width: number; lines: readonly string[] };
|
|
87
|
+
#body?: UserMessageComponent;
|
|
88
|
+
readonly #summary: string;
|
|
89
|
+
|
|
90
|
+
constructor(
|
|
91
|
+
private readonly text: string,
|
|
92
|
+
private readonly imageLinks?: readonly (string | undefined)[],
|
|
93
|
+
) {
|
|
94
|
+
this.#summary = summarizeSyntheticInput(text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** ctrl+o toggle: reveal/hide the full Markdown body. */
|
|
98
|
+
setExpanded(expanded: boolean): void {
|
|
99
|
+
if (this.#expanded === expanded) return;
|
|
100
|
+
this.#expanded = expanded;
|
|
101
|
+
this.#cache = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
invalidate(): void {
|
|
105
|
+
this.#cache = undefined;
|
|
106
|
+
this.#body?.invalidate?.();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
dispose(): void {
|
|
110
|
+
this.#body?.dispose?.();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
render(width: number): readonly string[] {
|
|
114
|
+
width = Math.max(1, width);
|
|
115
|
+
if (this.#cache?.width === width) return this.#cache.lines;
|
|
116
|
+
const lines = this.#expanded ? this.#renderExpanded(width) : [` ${this.#summaryRow(width)}`];
|
|
117
|
+
this.#cache = { width, lines };
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#renderExpanded(width: number): readonly string[] {
|
|
122
|
+
if (!this.#body) this.#body = new UserMessageComponent(this.text, true, this.imageLinks);
|
|
123
|
+
return [` ${this.#summaryRow(width)}`, ...this.#body.render(width)];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#summaryRow(width: number): string {
|
|
127
|
+
const hint = `${theme.sep.dot.trim()} ctrl+o`;
|
|
128
|
+
return theme.fg("dim", truncateSummary(`${this.#summary} ${hint}`, Math.max(10, width - 1)));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Truncate a plain summary label to `maxWidth` display columns, appending `…`. */
|
|
133
|
+
function truncateSummary(text: string, maxWidth: number): string {
|
|
134
|
+
if (Bun.stringWidth(text, { countAnsiEscapeCodes: false }) <= maxWidth) return text;
|
|
135
|
+
let out = "";
|
|
136
|
+
let w = 0;
|
|
137
|
+
for (const ch of text) {
|
|
138
|
+
const cw = Bun.stringWidth(ch, { countAnsiEscapeCodes: false });
|
|
139
|
+
if (w + cw > maxWidth - 1) break;
|
|
140
|
+
out += ch;
|
|
141
|
+
w += cw;
|
|
142
|
+
}
|
|
143
|
+
return `${out}…`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One-line summary for a collapsed synthetic input: `<label> · <size> · <n>
|
|
148
|
+
* lines`. The label is the first Markdown heading's text (e.g. `Session
|
|
149
|
+
* update`), falling back to `Synthetic input` when the body opens with none.
|
|
150
|
+
*/
|
|
151
|
+
function summarizeSyntheticInput(text: string): string {
|
|
152
|
+
const size = formatBytes(Buffer.byteLength(text, "utf-8"));
|
|
153
|
+
const lineCount = text === "" ? 0 : text.split("\n").length;
|
|
154
|
+
const dot = theme.sep.dot.trim();
|
|
155
|
+
return `${syntheticInputLabel(text)} ${dot} ${size} ${dot} ${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** First Markdown heading text in `text`, else `Synthetic input`. */
|
|
159
|
+
function syntheticInputLabel(text: string): string {
|
|
160
|
+
for (const raw of text.split("\n")) {
|
|
161
|
+
const line = raw.trim();
|
|
162
|
+
if (!line) continue;
|
|
163
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
164
|
+
return heading ? heading[1]!.trim() || "Synthetic input" : "Synthetic input";
|
|
165
|
+
}
|
|
166
|
+
return "Synthetic input";
|
|
167
|
+
}
|
|
@@ -1311,7 +1311,7 @@ export class CommandController {
|
|
|
1311
1311
|
|
|
1312
1312
|
compactingLoader.stop();
|
|
1313
1313
|
this.ctx.statusContainer.disposeChildren();
|
|
1314
|
-
this.ctx.rebuildChatFromMessages();
|
|
1314
|
+
this.ctx.rebuildChatFromMessages({ reuseSettledComponents: true });
|
|
1315
1315
|
|
|
1316
1316
|
this.ctx.statusLine.invalidate();
|
|
1317
1317
|
// Same as the auto-compaction rebuild: a collapsed transcript is an
|
|
@@ -1857,17 +1857,34 @@ export function renderUsageReports(
|
|
|
1857
1857
|
for (const line of resetAccountLines) lines.push(uiTheme.fg("dim", line));
|
|
1858
1858
|
}
|
|
1859
1859
|
|
|
1860
|
+
// Order account columns ONCE per provider (worst-first), then apply that
|
|
1861
|
+
// same order to every window group. Sorting each group independently by
|
|
1862
|
+
// its own used fraction (issue #6067) desynchronized the columns: an
|
|
1863
|
+
// account exhausted on its 5h window but light on the weekly window would
|
|
1864
|
+
// land in different column positions on each row, so the positional
|
|
1865
|
+
// `account N` labels denoted different credentials per row and an
|
|
1866
|
+
// exhausted limit appeared under a sibling that still had quota.
|
|
1867
|
+
const accountRank = new Map<UsageReport, number>();
|
|
1868
|
+
providerReports.forEach((report, position) => {
|
|
1869
|
+
const worst = report.limits.reduce((max, limit) => {
|
|
1870
|
+
const fraction = resolveUsedFraction(limit) ?? -1;
|
|
1871
|
+
return fraction > max ? fraction : max;
|
|
1872
|
+
}, -1);
|
|
1873
|
+
// Encode worst-first primary key with the stable position as tiebreak
|
|
1874
|
+
// so accounts tied on pressure keep their discovery order.
|
|
1875
|
+
accountRank.set(report, -worst * 1000 + position);
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1860
1878
|
const renderableGroups = Array.from(limitGroups.values()).map(group => {
|
|
1861
1879
|
const entries = group.limits.map((limit, index) => ({
|
|
1862
1880
|
limit,
|
|
1863
1881
|
report: group.reports[index],
|
|
1864
|
-
fraction: resolveUsedFraction(limit),
|
|
1865
1882
|
index,
|
|
1866
1883
|
}));
|
|
1867
1884
|
entries.sort((a, b) => {
|
|
1868
|
-
const
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1885
|
+
const aRank = accountRank.get(a.report) ?? a.index;
|
|
1886
|
+
const bRank = accountRank.get(b.report) ?? b.index;
|
|
1887
|
+
if (aRank !== bRank) return aRank - bRank;
|
|
1871
1888
|
return a.index - b.index;
|
|
1872
1889
|
});
|
|
1873
1890
|
const sortedLimits = entries.map(entry => entry.limit);
|
|
@@ -920,6 +920,9 @@ export class EventController {
|
|
|
920
920
|
),
|
|
921
921
|
);
|
|
922
922
|
}
|
|
923
|
+
if (displayMessage === event.message) {
|
|
924
|
+
this.ctx.transcriptMessageComponents.set(event.message, this.ctx.streamingComponent);
|
|
925
|
+
}
|
|
923
926
|
this.ctx.streamingComponent = undefined;
|
|
924
927
|
this.ctx.streamingMessage = undefined;
|
|
925
928
|
// Pin a turn-ending provider error (e.g. Anthropic content-filter block)
|
|
@@ -1316,7 +1319,7 @@ export class EventController {
|
|
|
1316
1319
|
}
|
|
1317
1320
|
} else if (event.result) {
|
|
1318
1321
|
this.ctx.lastAssistantUsage = undefined;
|
|
1319
|
-
this.ctx.rebuildChatFromMessages();
|
|
1322
|
+
this.ctx.rebuildChatFromMessages({ reuseSettledComponents: true });
|
|
1320
1323
|
this.ctx.statusLine.invalidate();
|
|
1321
1324
|
// When history collapses behind the summary divider, the frame
|
|
1322
1325
|
// shrinks far below the committed row count; without clearing, the
|
|
@@ -70,6 +70,12 @@ export class ExtensionUiController {
|
|
|
70
70
|
// the rest queue. See `#presentDialog`.
|
|
71
71
|
#dialogActive = false;
|
|
72
72
|
#dialogQueue: Array<() => void> = [];
|
|
73
|
+
/**
|
|
74
|
+
* Built once in `initHooksAndCustomTools()`. Reused directly by `/tree`
|
|
75
|
+
* `ask` re-answer (issue #5642) to drive a standalone `AskTool.execute()`
|
|
76
|
+
* call with the same picker/dialog primitives a live tool call would get.
|
|
77
|
+
*/
|
|
78
|
+
#toolUIContext: ExtensionUIContext | undefined;
|
|
73
79
|
constructor(private ctx: InteractiveModeContext) {}
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -121,6 +127,7 @@ export class ExtensionUiController {
|
|
|
121
127
|
setToolsExpanded: expanded => this.ctx.setToolsExpanded(expanded),
|
|
122
128
|
};
|
|
123
129
|
this.ctx.setToolUIContext(uiContext, true);
|
|
130
|
+
this.#toolUIContext = uiContext;
|
|
124
131
|
|
|
125
132
|
const extensionRunner = this.ctx.session.extensionRunner;
|
|
126
133
|
if (!extensionRunner) {
|
|
@@ -275,6 +282,17 @@ export class ExtensionUiController {
|
|
|
275
282
|
});
|
|
276
283
|
}
|
|
277
284
|
|
|
285
|
+
/**
|
|
286
|
+
* The `ExtensionUIContext` built in `initHooksAndCustomTools()` — the same
|
|
287
|
+
* picker/dialog primitives passed as `context.ui` for every live tool
|
|
288
|
+
* call. `/tree` `ask` re-answer (issue #5642) reuses this to drive a
|
|
289
|
+
* standalone `AskTool.execute()` call outside a normal agent turn.
|
|
290
|
+
* `undefined` before hooks have initialized.
|
|
291
|
+
*/
|
|
292
|
+
getToolUIContext(): ExtensionUIContext | undefined {
|
|
293
|
+
return this.#toolUIContext;
|
|
294
|
+
}
|
|
295
|
+
|
|
278
296
|
setHookWidget(key: string, content: ExtensionWidgetContent, options?: ExtensionWidgetOptions): void {
|
|
279
297
|
const placement = options?.placement ?? "aboveEditor";
|
|
280
298
|
this.#removeHookWidget(this.#hookWidgetsAbove, key);
|