@oh-my-pi/pi-coding-agent 13.14.2 → 13.15.2
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 +140 -0
- package/package.json +10 -8
- package/src/autoresearch/command-initialize.md +34 -0
- package/src/autoresearch/command-resume.md +17 -0
- package/src/autoresearch/contract.ts +332 -0
- package/src/autoresearch/dashboard.ts +447 -0
- package/src/autoresearch/git.ts +243 -0
- package/src/autoresearch/helpers.ts +458 -0
- package/src/autoresearch/index.ts +693 -0
- package/src/autoresearch/prompt.md +227 -0
- package/src/autoresearch/resume-message.md +16 -0
- package/src/autoresearch/state.ts +386 -0
- package/src/autoresearch/tools/init-experiment.ts +310 -0
- package/src/autoresearch/tools/log-experiment.ts +833 -0
- package/src/autoresearch/tools/run-experiment.ts +640 -0
- package/src/autoresearch/types.ts +218 -0
- package/src/cli/args.ts +8 -2
- package/src/cli/initial-message.ts +58 -0
- package/src/config/keybindings.ts +417 -212
- package/src/config/model-registry.ts +1 -0
- package/src/config/model-resolver.ts +57 -9
- package/src/config/settings-schema.ts +38 -10
- package/src/config/settings.ts +1 -4
- package/src/export/html/template.css +43 -13
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.html +1 -0
- package/src/export/html/template.js +107 -0
- package/src/extensibility/extensions/types.ts +31 -8
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +44 -44
- package/src/mcp/oauth-discovery.ts +1 -1
- package/src/modes/acp/acp-agent.ts +957 -0
- package/src/modes/acp/acp-event-mapper.ts +531 -0
- package/src/modes/acp/acp-mode.ts +13 -0
- package/src/modes/acp/index.ts +2 -0
- package/src/modes/components/agent-dashboard.ts +5 -4
- package/src/modes/components/custom-editor.ts +47 -47
- package/src/modes/components/extensions/extension-dashboard.ts +2 -1
- package/src/modes/components/history-search.ts +2 -1
- package/src/modes/components/hook-editor.ts +2 -1
- package/src/modes/components/hook-input.ts +8 -7
- package/src/modes/components/hook-selector.ts +15 -10
- package/src/modes/components/keybinding-hints.ts +9 -9
- package/src/modes/components/login-dialog.ts +3 -3
- package/src/modes/components/mcp-add-wizard.ts +2 -1
- package/src/modes/components/model-selector.ts +14 -3
- package/src/modes/components/oauth-selector.ts +2 -1
- package/src/modes/components/session-selector.ts +2 -1
- package/src/modes/components/settings-selector.ts +2 -1
- package/src/modes/components/status-line-segment-editor.ts +2 -1
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/components/user-message-selector.ts +3 -8
- package/src/modes/components/user-message.ts +16 -0
- package/src/modes/controllers/extension-ui-controller.ts +89 -4
- package/src/modes/controllers/input-controller.ts +29 -23
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +17 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/prompt-action-autocomplete.ts +7 -7
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/rpc/rpc-types.ts +1 -0
- package/src/modes/theme/theme.ts +53 -44
- package/src/modes/types.ts +9 -2
- package/src/modes/utils/hotkeys-markdown.ts +19 -19
- package/src/modes/utils/keybinding-matchers.ts +21 -0
- package/src/modes/utils/ui-helpers.ts +1 -1
- package/src/patch/hashline.ts +139 -127
- package/src/patch/index.ts +77 -59
- package/src/patch/shared.ts +19 -11
- package/src/prompts/tools/hashline.md +43 -116
- package/src/sdk.ts +34 -17
- package/src/session/agent-session.ts +123 -30
- package/src/session/session-manager.ts +32 -31
- package/src/tools/ask.ts +56 -30
- package/src/tools/bash-interceptor.ts +1 -39
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/gemini-image.ts +1 -1
- package/src/tools/resolve.ts +1 -1
- package/src/utils/child-process.ts +88 -0
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
calculateRateLimitBackoffMs,
|
|
47
47
|
getSupportedEfforts,
|
|
48
48
|
isContextOverflow,
|
|
49
|
+
isUsageLimitError,
|
|
49
50
|
modelsAreEqual,
|
|
50
51
|
parseRateLimitReason,
|
|
51
52
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -224,6 +225,8 @@ export interface AgentSessionConfig {
|
|
|
224
225
|
mcpDiscoveryEnabled?: boolean;
|
|
225
226
|
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
226
227
|
initialSelectedMCPToolNames?: string[];
|
|
228
|
+
/** Whether constructor-provided MCP defaults should be persisted immediately. */
|
|
229
|
+
persistInitialMCPToolSelection?: boolean;
|
|
227
230
|
/** MCP server names whose tools should seed discovery-mode sessions whenever those servers are connected. */
|
|
228
231
|
defaultSelectedMCPServerNames?: string[];
|
|
229
232
|
/** MCP tool names that should seed brand-new sessions created from this AgentSession. */
|
|
@@ -363,6 +366,7 @@ export class AgentSession {
|
|
|
363
366
|
#followUpMessages: string[] = [];
|
|
364
367
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
365
368
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
369
|
+
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
366
370
|
#planModeState: PlanModeState | undefined;
|
|
367
371
|
#planReferenceSent = false;
|
|
368
372
|
#planReferencePath = "local://PLAN.md";
|
|
@@ -483,8 +487,11 @@ export class AgentSession {
|
|
|
483
487
|
this.#pruneSelectedMCPToolNames();
|
|
484
488
|
const persistedSelectedMCPToolNames = this.sessionManager.buildSessionContext().selectedMCPToolNames;
|
|
485
489
|
const currentSelectedMCPToolNames = this.getSelectedMCPToolNames();
|
|
490
|
+
const persistInitialMCPToolSelection =
|
|
491
|
+
config.persistInitialMCPToolSelection ?? this.sessionManager.getBranch().length === 0;
|
|
486
492
|
if (
|
|
487
493
|
this.#mcpDiscoveryEnabled &&
|
|
494
|
+
persistInitialMCPToolSelection &&
|
|
488
495
|
!this.#selectedMCPToolNamesMatch(persistedSelectedMCPToolNames, currentSelectedMCPToolNames)
|
|
489
496
|
) {
|
|
490
497
|
this.sessionManager.appendMCPToolSelection(currentSelectedMCPToolNames);
|
|
@@ -781,7 +788,6 @@ export class AgentSession {
|
|
|
781
788
|
attempt: this.#retryAttempt,
|
|
782
789
|
});
|
|
783
790
|
this.#retryAttempt = 0;
|
|
784
|
-
this.#resolveRetry();
|
|
785
791
|
}
|
|
786
792
|
}
|
|
787
793
|
|
|
@@ -857,6 +863,7 @@ export class AgentSession {
|
|
|
857
863
|
const didRetry = await this.#handleRetryableError(msg);
|
|
858
864
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
859
865
|
}
|
|
866
|
+
this.#resolveRetry();
|
|
860
867
|
|
|
861
868
|
if (msg.stopReason === "aborted" && this.#checkpointState) {
|
|
862
869
|
this.#checkpointState = undefined;
|
|
@@ -2566,6 +2573,74 @@ export class AgentSession {
|
|
|
2566
2573
|
});
|
|
2567
2574
|
}
|
|
2568
2575
|
|
|
2576
|
+
#queueHiddenNextTurnMessage(message: CustomMessage, triggerTurn: boolean): void {
|
|
2577
|
+
this.#pendingNextTurnMessages.push(message);
|
|
2578
|
+
if (!triggerTurn) return;
|
|
2579
|
+
const generation = this.#promptGeneration;
|
|
2580
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
this.#scheduledHiddenNextTurnGeneration = generation;
|
|
2584
|
+
this.#schedulePostPromptTask(
|
|
2585
|
+
async () => {
|
|
2586
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2587
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2588
|
+
}
|
|
2589
|
+
if (this.#pendingNextTurnMessages.length === 0) {
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
try {
|
|
2593
|
+
await this.#promptQueuedHiddenNextTurnMessages();
|
|
2594
|
+
} catch {
|
|
2595
|
+
// Leave the hidden next-turn messages queued for the next explicit prompt.
|
|
2596
|
+
}
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
generation,
|
|
2600
|
+
onSkip: () => {
|
|
2601
|
+
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
2602
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2603
|
+
}
|
|
2604
|
+
},
|
|
2605
|
+
},
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
|
|
2610
|
+
if (this.#pendingNextTurnMessages.length === 0) {
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const queuedMessages = [...this.#pendingNextTurnMessages];
|
|
2615
|
+
this.#pendingNextTurnMessages = [];
|
|
2616
|
+
const message = queuedMessages[queuedMessages.length - 1];
|
|
2617
|
+
if (!message) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
const prependMessages = queuedMessages.slice(0, -1);
|
|
2622
|
+
const textContent = this.#getCustomMessageTextContent(message);
|
|
2623
|
+
try {
|
|
2624
|
+
await this.#promptWithMessage(message, textContent, {
|
|
2625
|
+
prependMessages,
|
|
2626
|
+
skipPostPromptRecoveryWait: true,
|
|
2627
|
+
});
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
this.#pendingNextTurnMessages = [...queuedMessages, ...this.#pendingNextTurnMessages];
|
|
2630
|
+
throw error;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
2635
|
+
if (typeof message.content === "string") {
|
|
2636
|
+
return message.content;
|
|
2637
|
+
}
|
|
2638
|
+
return message.content
|
|
2639
|
+
.filter((content): content is TextContent => content.type === "text")
|
|
2640
|
+
.map(content => content.text)
|
|
2641
|
+
.join("");
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2569
2644
|
/**
|
|
2570
2645
|
* Throw an error if the text is an extension command.
|
|
2571
2646
|
*/
|
|
@@ -2606,7 +2681,7 @@ export class AgentSession {
|
|
|
2606
2681
|
};
|
|
2607
2682
|
if (this.isStreaming) {
|
|
2608
2683
|
if (options?.deliverAs === "nextTurn") {
|
|
2609
|
-
this.#
|
|
2684
|
+
this.#queueHiddenNextTurnMessage(appMessage, options?.triggerTurn ?? false);
|
|
2610
2685
|
return;
|
|
2611
2686
|
}
|
|
2612
2687
|
|
|
@@ -2618,6 +2693,22 @@ export class AgentSession {
|
|
|
2618
2693
|
return;
|
|
2619
2694
|
}
|
|
2620
2695
|
|
|
2696
|
+
if (options?.deliverAs === "nextTurn") {
|
|
2697
|
+
if (options?.triggerTurn) {
|
|
2698
|
+
await this.agent.prompt(appMessage);
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
this.agent.appendMessage(appMessage);
|
|
2702
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
2703
|
+
message.customType,
|
|
2704
|
+
message.content,
|
|
2705
|
+
message.display,
|
|
2706
|
+
message.details,
|
|
2707
|
+
message.attribution ?? "agent",
|
|
2708
|
+
);
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2621
2712
|
if (options?.triggerTurn) {
|
|
2622
2713
|
await this.agent.prompt(appMessage);
|
|
2623
2714
|
return;
|
|
@@ -2685,9 +2776,9 @@ export class AgentSession {
|
|
|
2685
2776
|
return { steering, followUp };
|
|
2686
2777
|
}
|
|
2687
2778
|
|
|
2688
|
-
/** Number of pending messages (includes
|
|
2779
|
+
/** Number of pending messages (includes steering, follow-up, and next-turn messages) */
|
|
2689
2780
|
get queuedMessageCount(): number {
|
|
2690
|
-
return this.#steeringMessages.length + this.#followUpMessages.length;
|
|
2781
|
+
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
2691
2782
|
}
|
|
2692
2783
|
|
|
2693
2784
|
/** Get pending messages (read-only) */
|
|
@@ -2829,6 +2920,7 @@ export class AgentSession {
|
|
|
2829
2920
|
async abort(): Promise<void> {
|
|
2830
2921
|
this.abortRetry();
|
|
2831
2922
|
this.#promptGeneration++;
|
|
2923
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2832
2924
|
this.#resolveTtsrResume();
|
|
2833
2925
|
this.#cancelPostPromptTasks();
|
|
2834
2926
|
this.agent.abort();
|
|
@@ -2878,6 +2970,7 @@ export class AgentSession {
|
|
|
2878
2970
|
this.#steeringMessages = [];
|
|
2879
2971
|
this.#followUpMessages = [];
|
|
2880
2972
|
this.#pendingNextTurnMessages = [];
|
|
2973
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
2881
2974
|
|
|
2882
2975
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
2883
2976
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
@@ -3611,6 +3704,7 @@ export class AgentSession {
|
|
|
3611
3704
|
this.#steeringMessages = [];
|
|
3612
3705
|
this.#followUpMessages = [];
|
|
3613
3706
|
this.#pendingNextTurnMessages = [];
|
|
3707
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
3614
3708
|
this.#todoReminderCount = 0;
|
|
3615
3709
|
|
|
3616
3710
|
// Inject the handoff document as a custom message
|
|
@@ -4278,7 +4372,9 @@ export class AgentSession {
|
|
|
4278
4372
|
const shouldRetry =
|
|
4279
4373
|
retrySettings.enabled &&
|
|
4280
4374
|
attempt < retrySettings.maxRetries &&
|
|
4281
|
-
(retryAfterMs !== undefined ||
|
|
4375
|
+
(retryAfterMs !== undefined ||
|
|
4376
|
+
this.#isTransientErrorMessage(message) ||
|
|
4377
|
+
isUsageLimitError(message));
|
|
4282
4378
|
if (!shouldRetry) {
|
|
4283
4379
|
lastError = error;
|
|
4284
4380
|
break;
|
|
@@ -4476,8 +4572,9 @@ export class AgentSession {
|
|
|
4476
4572
|
// =========================================================================
|
|
4477
4573
|
|
|
4478
4574
|
/**
|
|
4479
|
-
* Check if an error is retryable (
|
|
4575
|
+
* Check if an error is retryable (transient errors or usage limits).
|
|
4480
4576
|
* Context overflow errors are NOT retryable (handled by compaction instead).
|
|
4577
|
+
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
4481
4578
|
*/
|
|
4482
4579
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
4483
4580
|
if (message.stopReason !== "error" || !message.errorMessage) return false;
|
|
@@ -4487,20 +4584,17 @@ export class AgentSession {
|
|
|
4487
4584
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
4488
4585
|
|
|
4489
4586
|
const err = message.errorMessage;
|
|
4490
|
-
return this.#
|
|
4587
|
+
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
4491
4588
|
}
|
|
4492
4589
|
|
|
4493
|
-
#
|
|
4494
|
-
// Match: overloaded_error,
|
|
4495
|
-
|
|
4590
|
+
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
4591
|
+
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504,
|
|
4592
|
+
// service unavailable, network/connection errors, fetch failed, terminated, retry delay exceeded
|
|
4593
|
+
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall/i.test(
|
|
4496
4594
|
errorMessage,
|
|
4497
4595
|
);
|
|
4498
4596
|
}
|
|
4499
4597
|
|
|
4500
|
-
#isUsageLimitErrorMessage(errorMessage: string): boolean {
|
|
4501
|
-
return /usage.?limit|usage_limit_reached|limit_reached|quota.?exceeded|resource.?exhausted/i.test(errorMessage);
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
4598
|
#parseRetryAfterMsFromError(errorMessage: string): number | undefined {
|
|
4505
4599
|
const now = Date.now();
|
|
4506
4600
|
const retryAfterMsMatch = /retry-after-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
|
|
@@ -4582,7 +4676,7 @@ export class AgentSession {
|
|
|
4582
4676
|
const errorMessage = message.errorMessage || "Unknown error";
|
|
4583
4677
|
let delayMs = retrySettings.baseDelayMs * 2 ** (this.#retryAttempt - 1);
|
|
4584
4678
|
|
|
4585
|
-
if (this.model &&
|
|
4679
|
+
if (this.model && isUsageLimitError(errorMessage)) {
|
|
4586
4680
|
const retryAfterMs =
|
|
4587
4681
|
this.#parseRetryAfterMsFromError(errorMessage) ??
|
|
4588
4682
|
calculateRateLimitBackoffMs(parseRateLimitReason(errorMessage));
|
|
@@ -4960,6 +5054,7 @@ export class AgentSession {
|
|
|
4960
5054
|
this.#steeringMessages = [];
|
|
4961
5055
|
this.#followUpMessages = [];
|
|
4962
5056
|
this.#pendingNextTurnMessages = [];
|
|
5057
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
4963
5058
|
|
|
4964
5059
|
// Flush pending writes before switching
|
|
4965
5060
|
await this.sessionManager.flush();
|
|
@@ -5003,21 +5098,18 @@ export class AgentSession {
|
|
|
5003
5098
|
const hasThinkingEntry = this.sessionManager.getBranch().some(entry => entry.type === "thinking_level_change");
|
|
5004
5099
|
const hasServiceTierEntry = this.sessionManager.getBranch().some(entry => entry.type === "service_tier_change");
|
|
5005
5100
|
const defaultThinkingLevel = this.settings.get("defaultThinkingLevel");
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
this.
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
} else {
|
|
5019
|
-
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
5020
|
-
}
|
|
5101
|
+
const configuredServiceTier = this.settings.get("serviceTier");
|
|
5102
|
+
const nextThinkingLevel = resolveThinkingLevelForModel(
|
|
5103
|
+
this.model,
|
|
5104
|
+
hasThinkingEntry ? (sessionContext.thinkingLevel as ThinkingLevel | undefined) : defaultThinkingLevel,
|
|
5105
|
+
);
|
|
5106
|
+
this.#thinkingLevel = nextThinkingLevel;
|
|
5107
|
+
this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
5108
|
+
this.agent.serviceTier = hasServiceTierEntry
|
|
5109
|
+
? sessionContext.serviceTier
|
|
5110
|
+
: configuredServiceTier === "none"
|
|
5111
|
+
? undefined
|
|
5112
|
+
: configuredServiceTier;
|
|
5021
5113
|
|
|
5022
5114
|
this.#reconnectToAgent();
|
|
5023
5115
|
return true;
|
|
@@ -5059,6 +5151,7 @@ export class AgentSession {
|
|
|
5059
5151
|
|
|
5060
5152
|
// Clear pending messages (bound to old session state)
|
|
5061
5153
|
this.#pendingNextTurnMessages = [];
|
|
5154
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5062
5155
|
|
|
5063
5156
|
// Flush pending writes before branching
|
|
5064
5157
|
await this.sessionManager.flush();
|
|
@@ -1302,21 +1302,19 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
|
|
|
1302
1302
|
}
|
|
1303
1303
|
}
|
|
1304
1304
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1305
|
+
const stats = storage.statSync(file);
|
|
1306
|
+
sessions.push({
|
|
1307
|
+
path: file,
|
|
1308
|
+
id: header.id,
|
|
1309
|
+
cwd: typeof header.cwd === "string" ? header.cwd : "",
|
|
1310
|
+
title: header.title ?? shortSummary,
|
|
1311
|
+
parentSessionPath: (header as SessionHeader).parentSession,
|
|
1312
|
+
created: new Date(header.timestamp),
|
|
1313
|
+
modified: stats.mtime,
|
|
1314
|
+
messageCount,
|
|
1315
|
+
firstMessage: firstMessage || "(no messages)",
|
|
1316
|
+
allMessagesText: allMessages.join(" "),
|
|
1317
|
+
});
|
|
1320
1318
|
} catch {}
|
|
1321
1319
|
}),
|
|
1322
1320
|
);
|
|
@@ -1381,6 +1379,7 @@ export class SessionManager {
|
|
|
1381
1379
|
#sessionName: string | undefined;
|
|
1382
1380
|
#sessionFile: string | undefined;
|
|
1383
1381
|
#flushed: boolean = false;
|
|
1382
|
+
#needsFullRewriteOnNextPersist: boolean = false;
|
|
1384
1383
|
#fileEntries: FileEntry[] = [];
|
|
1385
1384
|
#byId: Map<string, SessionEntry> = new Map();
|
|
1386
1385
|
#labelsById: Map<string, string> = new Map();
|
|
@@ -1443,9 +1442,7 @@ export class SessionManager {
|
|
|
1443
1442
|
this.#sessionId = header?.id ?? Snowflake.next();
|
|
1444
1443
|
this.#sessionName = header?.title;
|
|
1445
1444
|
|
|
1446
|
-
|
|
1447
|
-
await this.#rewriteFile();
|
|
1448
|
-
}
|
|
1445
|
+
this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
|
|
1449
1446
|
|
|
1450
1447
|
await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
|
|
1451
1448
|
|
|
@@ -1632,6 +1629,7 @@ export class SessionManager {
|
|
|
1632
1629
|
this.#labelsById.clear();
|
|
1633
1630
|
this.#leafId = null;
|
|
1634
1631
|
this.#flushed = false;
|
|
1632
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
1635
1633
|
this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, premiumRequests: 0, cost: 0 };
|
|
1636
1634
|
|
|
1637
1635
|
if (this.persist) {
|
|
@@ -1774,6 +1772,7 @@ export class SessionManager {
|
|
|
1774
1772
|
this.#fileEntries.map(entry => prepareEntryForPersistence(entry, this.#blobStore)),
|
|
1775
1773
|
);
|
|
1776
1774
|
await this.#writeEntriesAtomically(entries);
|
|
1775
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
1777
1776
|
this.#flushed = true;
|
|
1778
1777
|
});
|
|
1779
1778
|
}
|
|
@@ -1782,6 +1781,16 @@ export class SessionManager {
|
|
|
1782
1781
|
return this.persist;
|
|
1783
1782
|
}
|
|
1784
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* Force-persist all current entries to disk, even when no assistant message exists yet.
|
|
1786
|
+
* Used by ACP mode where session/new must create a discoverable session immediately.
|
|
1787
|
+
*/
|
|
1788
|
+
async ensureOnDisk(): Promise<void> {
|
|
1789
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
1790
|
+
if (this.#flushed && !this.#needsFullRewriteOnNextPersist) return;
|
|
1791
|
+
await this.#rewriteFile();
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1785
1794
|
/** Flush pending writes to disk. Call before switching sessions or on shutdown. */
|
|
1786
1795
|
async flush(): Promise<void> {
|
|
1787
1796
|
await this.#queuePersistTask(async () => {
|
|
@@ -1911,23 +1920,15 @@ export class SessionManager {
|
|
|
1911
1920
|
|
|
1912
1921
|
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1913
1922
|
if (!hasAssistant) {
|
|
1914
|
-
// Mark as not flushed so when assistant arrives, all entries get written
|
|
1923
|
+
// Mark as not flushed so when assistant arrives, all entries get written.
|
|
1915
1924
|
this.#flushed = false;
|
|
1916
1925
|
return;
|
|
1917
1926
|
}
|
|
1918
1927
|
|
|
1919
|
-
if (!this.#flushed) {
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
if (!writer) return;
|
|
1924
|
-
const entries = await Promise.all(
|
|
1925
|
-
this.#fileEntries.map(e => prepareEntryForPersistence(e, this.#blobStore)),
|
|
1926
|
-
);
|
|
1927
|
-
for (const persistedEntry of entries) {
|
|
1928
|
-
await writer.write(persistedEntry);
|
|
1929
|
-
}
|
|
1930
|
-
});
|
|
1928
|
+
if (this.#needsFullRewriteOnNextPersist || !this.#flushed) {
|
|
1929
|
+
// Full flush: rewrite the entire file atomically to avoid
|
|
1930
|
+
// duplicating entries if the file already exists (e.g. from ensureOnDisk).
|
|
1931
|
+
void this.#rewriteFile();
|
|
1931
1932
|
} else {
|
|
1932
1933
|
void this.#queuePersistTask(async () => {
|
|
1933
1934
|
const writer = this.#ensurePersistWriter();
|
package/src/tools/ask.ts
CHANGED
|
@@ -16,13 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
|
-
import type
|
|
20
|
-
import { TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
19
|
+
import { type Component, Container, Markdown, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
21
20
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
22
21
|
import { type Static, Type } from "@sinclair/typebox";
|
|
23
22
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
24
23
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
25
|
-
import { type Theme, theme } from "../modes/theme/theme";
|
|
24
|
+
import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
26
25
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
27
26
|
import { renderStatusLine } from "../tui";
|
|
28
27
|
import type { ToolSession } from ".";
|
|
@@ -574,10 +573,13 @@ interface AskRenderArgs {
|
|
|
574
573
|
export const askToolRenderer = {
|
|
575
574
|
renderCall(args: AskRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
576
575
|
const label = formatTitle("Ask", uiTheme);
|
|
576
|
+
const mdTheme = getMarkdownTheme();
|
|
577
|
+
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
|
577
578
|
|
|
578
579
|
// Multi-part questions
|
|
579
580
|
if (args.questions && args.questions.length > 0) {
|
|
580
|
-
|
|
581
|
+
const container = new Container();
|
|
582
|
+
container.addChild(new Text(`${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`, 0, 0));
|
|
581
583
|
|
|
582
584
|
for (let i = 0; i < args.questions.length; i++) {
|
|
583
585
|
const q = args.questions[i];
|
|
@@ -585,25 +587,29 @@ export const askToolRenderer = {
|
|
|
585
587
|
const qBranch = isLastQ ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
586
588
|
const continuation = isLastQ ? " " : uiTheme.tree.vertical;
|
|
587
589
|
|
|
588
|
-
// Question line with metadata
|
|
589
590
|
const meta: string[] = [];
|
|
590
591
|
if (q.multi) meta.push("multi");
|
|
591
592
|
if (q.options?.length) meta.push(`options:${q.options.length}`);
|
|
592
593
|
const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
|
|
593
594
|
|
|
594
|
-
|
|
595
|
+
container.addChild(
|
|
596
|
+
new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
|
|
597
|
+
);
|
|
598
|
+
container.addChild(new Markdown(q.question, 3, 0, mdTheme, accentStyle));
|
|
595
599
|
|
|
596
|
-
// Options under question
|
|
597
600
|
if (q.options?.length) {
|
|
601
|
+
let optText = "";
|
|
598
602
|
for (let j = 0; j < q.options.length; j++) {
|
|
599
603
|
const opt = q.options[j];
|
|
600
604
|
const isLastOpt = j === q.options.length - 1;
|
|
601
605
|
const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
602
|
-
|
|
606
|
+
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
|
607
|
+
optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
|
|
603
608
|
}
|
|
609
|
+
container.addChild(new Text(optText, 0, 0));
|
|
604
610
|
}
|
|
605
611
|
}
|
|
606
|
-
return
|
|
612
|
+
return container;
|
|
607
613
|
}
|
|
608
614
|
|
|
609
615
|
// Single question
|
|
@@ -611,22 +617,26 @@ export const askToolRenderer = {
|
|
|
611
617
|
return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
|
|
612
618
|
}
|
|
613
619
|
|
|
614
|
-
|
|
620
|
+
const container = new Container();
|
|
615
621
|
const meta: string[] = [];
|
|
616
622
|
if (args.multi) meta.push("multi");
|
|
617
623
|
if (args.options?.length) meta.push(`options:${args.options.length}`);
|
|
618
|
-
|
|
624
|
+
container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
|
|
625
|
+
container.addChild(new Markdown(args.question, 1, 0, mdTheme, accentStyle));
|
|
619
626
|
|
|
620
627
|
if (args.options?.length) {
|
|
628
|
+
let optText = "";
|
|
621
629
|
for (let i = 0; i < args.options.length; i++) {
|
|
622
630
|
const opt = args.options[i];
|
|
623
631
|
const isLast = i === args.options.length - 1;
|
|
624
632
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
625
|
-
|
|
633
|
+
const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
|
|
634
|
+
optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", uiTheme.checkbox.unchecked)} ${optLabel}`;
|
|
626
635
|
}
|
|
636
|
+
container.addChild(new Text(optText, 0, 0));
|
|
627
637
|
}
|
|
628
638
|
|
|
629
|
-
return
|
|
639
|
+
return container;
|
|
630
640
|
},
|
|
631
641
|
|
|
632
642
|
renderResult(
|
|
@@ -635,6 +645,9 @@ export const askToolRenderer = {
|
|
|
635
645
|
uiTheme: Theme,
|
|
636
646
|
): Component {
|
|
637
647
|
const { details } = result;
|
|
648
|
+
const mdTheme = getMarkdownTheme();
|
|
649
|
+
const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
|
|
650
|
+
|
|
638
651
|
if (!details) {
|
|
639
652
|
const txt = result.content[0];
|
|
640
653
|
const fallback = txt?.type === "text" && txt.text ? txt.text : "";
|
|
@@ -655,7 +668,8 @@ export const askToolRenderer = {
|
|
|
655
668
|
},
|
|
656
669
|
uiTheme,
|
|
657
670
|
);
|
|
658
|
-
|
|
671
|
+
const container = new Container();
|
|
672
|
+
container.addChild(new Text(header, 0, 0));
|
|
659
673
|
|
|
660
674
|
for (let i = 0; i < details.results.length; i++) {
|
|
661
675
|
const r = details.results[i];
|
|
@@ -667,22 +681,31 @@ export const askToolRenderer = {
|
|
|
667
681
|
? uiTheme.styledSymbol("status.success", "success")
|
|
668
682
|
: uiTheme.styledSymbol("status.warning", "warning");
|
|
669
683
|
|
|
670
|
-
|
|
684
|
+
container.addChild(
|
|
685
|
+
new Text(` ${uiTheme.fg("dim", branch)} ${statusIcon} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0),
|
|
686
|
+
);
|
|
687
|
+
container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
|
|
671
688
|
|
|
689
|
+
let answerText = "";
|
|
672
690
|
if (r.customInput) {
|
|
673
|
-
|
|
691
|
+
answerText = `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", r.customInput)}`;
|
|
674
692
|
} else if (r.selectedOptions.length > 0) {
|
|
675
693
|
for (let j = 0; j < r.selectedOptions.length; j++) {
|
|
676
694
|
const isLast = j === r.selectedOptions.length - 1;
|
|
677
695
|
const optBranch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
678
|
-
|
|
696
|
+
const selectedLabel = renderInlineMarkdown(r.selectedOptions[j], mdTheme, t =>
|
|
697
|
+
uiTheme.fg("toolOutput", t),
|
|
698
|
+
);
|
|
699
|
+
answerText += `\n${continuation}${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`;
|
|
679
700
|
}
|
|
680
701
|
} else {
|
|
681
|
-
|
|
702
|
+
answerText = `${continuation}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
703
|
+
}
|
|
704
|
+
if (answerText) {
|
|
705
|
+
container.addChild(new Text(answerText, 0, 0));
|
|
682
706
|
}
|
|
683
707
|
}
|
|
684
|
-
|
|
685
|
-
return new Text(text, 0, 0);
|
|
708
|
+
return container;
|
|
686
709
|
}
|
|
687
710
|
|
|
688
711
|
// Single question result
|
|
@@ -693,25 +716,28 @@ export const askToolRenderer = {
|
|
|
693
716
|
}
|
|
694
717
|
|
|
695
718
|
const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
|
|
696
|
-
const header = renderStatusLine(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
let text = header;
|
|
719
|
+
const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
|
|
720
|
+
const container = new Container();
|
|
721
|
+
container.addChild(new Text(header, 0, 0));
|
|
722
|
+
container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
|
|
702
723
|
|
|
724
|
+
let answerText = "";
|
|
703
725
|
if (details.customInput) {
|
|
704
|
-
|
|
726
|
+
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", details.customInput)}`;
|
|
705
727
|
} else if (details.selectedOptions && details.selectedOptions.length > 0) {
|
|
706
728
|
for (let i = 0; i < details.selectedOptions.length; i++) {
|
|
707
729
|
const isLast = i === details.selectedOptions.length - 1;
|
|
708
730
|
const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
|
|
709
|
-
|
|
731
|
+
const selectedLabel = renderInlineMarkdown(details.selectedOptions[i], mdTheme, t =>
|
|
732
|
+
uiTheme.fg("toolOutput", t),
|
|
733
|
+
);
|
|
734
|
+
answerText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("success", uiTheme.checkbox.checked)} ${selectedLabel}`;
|
|
710
735
|
}
|
|
711
736
|
} else {
|
|
712
|
-
|
|
737
|
+
answerText = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
|
|
713
738
|
}
|
|
739
|
+
container.addChild(new Text(answerText, 0, 0));
|
|
714
740
|
|
|
715
|
-
return
|
|
741
|
+
return container;
|
|
716
742
|
},
|
|
717
743
|
};
|
|
@@ -5,45 +5,7 @@
|
|
|
5
5
|
* this interceptor provides helpful error messages directing them to use
|
|
6
6
|
* the specialized tools instead.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
9
|
-
|
|
10
|
-
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
11
|
-
{
|
|
12
|
-
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
|
|
13
|
-
tool: "read",
|
|
14
|
-
message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
|
|
18
|
-
tool: "grep",
|
|
19
|
-
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
|
|
23
|
-
tool: "find",
|
|
24
|
-
message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
pattern: "^\\s*sed\\s+(-i|--in-place)",
|
|
28
|
-
tool: "edit",
|
|
29
|
-
message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
pattern: "^\\s*perl\\s+.*-[pn]?i",
|
|
33
|
-
tool: "edit",
|
|
34
|
-
message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
pattern: "^\\s*awk\\s+.*-i\\s+inplace",
|
|
38
|
-
tool: "edit",
|
|
39
|
-
message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
|
|
43
|
-
tool: "write",
|
|
44
|
-
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
|
|
45
|
-
},
|
|
46
|
-
];
|
|
8
|
+
import { type BashInterceptorRule, DEFAULT_BASH_INTERCEPTOR_RULES } from "../config/settings-schema";
|
|
47
9
|
|
|
48
10
|
export interface InterceptionResult {
|
|
49
11
|
/** If true, the bash command should be blocked */
|
|
@@ -131,7 +131,7 @@ async function resolveInternalUrlToPath(
|
|
|
131
131
|
return resolvedLocalPath;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
if (!internalRouter
|
|
134
|
+
if (!internalRouter?.canHandle(url)) {
|
|
135
135
|
throw new ToolError(
|
|
136
136
|
`Cannot resolve ${scheme}:// URL in bash command: ${url}\n` +
|
|
137
137
|
"Internal URL router is unavailable for this protocol in the current session.",
|
package/src/tools/browser.ts
CHANGED
|
@@ -564,7 +564,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
564
564
|
if (this.#page && !this.#page.isClosed()) {
|
|
565
565
|
return this.#page;
|
|
566
566
|
}
|
|
567
|
-
if (!this.#browser
|
|
567
|
+
if (!this.#browser?.isConnected()) {
|
|
568
568
|
return this.#resetBrowser(params);
|
|
569
569
|
}
|
|
570
570
|
this.#page = await this.#browser.newPage();
|