@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.6

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.
Files changed (135) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/cli.js +692 -607
  3. package/dist/types/cli/usage-cli.d.ts +10 -1
  4. package/dist/types/commands/usage.d.ts +9 -0
  5. package/dist/types/config/api-key-resolver.d.ts +9 -3
  6. package/dist/types/config/keybindings.d.ts +1 -1
  7. package/dist/types/config/model-discovery.d.ts +6 -4
  8. package/dist/types/config/model-registry.d.ts +7 -4
  9. package/dist/types/config/settings-schema.d.ts +508 -155
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/mnemopi/config.d.ts +3 -1
  12. package/dist/types/modes/components/reset-usage-selector.d.ts +12 -0
  13. package/dist/types/modes/components/session-selector.d.ts +1 -1
  14. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  15. package/dist/types/modes/components/settings-selector.d.ts +9 -4
  16. package/dist/types/modes/components/tool-execution.d.ts +26 -1
  17. package/dist/types/modes/components/transcript-container.d.ts +12 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +9 -1
  19. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  20. package/dist/types/modes/interactive-mode.d.ts +10 -0
  21. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  22. package/dist/types/modes/theme/theme.d.ts +23 -3
  23. package/dist/types/modes/types.d.ts +2 -0
  24. package/dist/types/modes/utils/context-usage.d.ts +6 -1
  25. package/dist/types/session/agent-session.d.ts +28 -8
  26. package/dist/types/session/auth-storage.d.ts +1 -1
  27. package/dist/types/session/codex-auto-reset.d.ts +107 -0
  28. package/dist/types/session/snapcompact-inline.d.ts +129 -0
  29. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  30. package/dist/types/slash-commands/helpers/reset-usage.d.ts +27 -0
  31. package/dist/types/system-prompt.d.ts +3 -1
  32. package/dist/types/task/render.d.ts +17 -6
  33. package/dist/types/tools/gh.d.ts +3 -0
  34. package/dist/types/tools/render-utils.d.ts +8 -16
  35. package/dist/types/tools/todo.d.ts +0 -11
  36. package/dist/types/utils/session-color.d.ts +15 -3
  37. package/dist/types/web/kagi.d.ts +1 -2
  38. package/dist/types/web/search/providers/codex.d.ts +1 -1
  39. package/dist/types/web/search/providers/gemini.d.ts +9 -6
  40. package/package.json +11 -11
  41. package/src/auto-thinking/classifier.ts +1 -5
  42. package/src/cli/usage-cli.ts +187 -16
  43. package/src/commands/usage.ts +8 -0
  44. package/src/commit/model-selection.ts +3 -6
  45. package/src/config/api-key-resolver.ts +10 -3
  46. package/src/config/keybindings.ts +1 -1
  47. package/src/config/model-discovery.ts +60 -46
  48. package/src/config/model-registry.ts +21 -8
  49. package/src/config/model-resolver.ts +57 -3
  50. package/src/config/settings-schema.ts +654 -153
  51. package/src/config/settings.ts +9 -0
  52. package/src/eval/completion-bridge.ts +1 -5
  53. package/src/export/html/template.generated.ts +1 -1
  54. package/src/export/html/template.js +13 -6
  55. package/src/internal-urls/docs-index.generated.ts +6 -6
  56. package/src/internal-urls/issue-pr-protocol.ts +10 -4
  57. package/src/memories/index.ts +2 -10
  58. package/src/mnemopi/backend.ts +30 -8
  59. package/src/mnemopi/config.ts +6 -1
  60. package/src/mnemopi/state.ts +6 -0
  61. package/src/modes/components/extensions/inspector-panel.ts +6 -2
  62. package/src/modes/components/plan-review-overlay.ts +15 -17
  63. package/src/modes/components/plugin-settings.ts +22 -5
  64. package/src/modes/components/reset-usage-selector.ts +161 -0
  65. package/src/modes/components/session-selector.ts +8 -2
  66. package/src/modes/components/settings-defs.ts +19 -4
  67. package/src/modes/components/settings-selector.ts +510 -95
  68. package/src/modes/components/status-line/component.ts +3 -1
  69. package/src/modes/components/status-line/segments.ts +3 -1
  70. package/src/modes/components/tool-execution.ts +87 -12
  71. package/src/modes/components/transcript-container.ts +49 -1
  72. package/src/modes/components/tree-selector.ts +16 -6
  73. package/src/modes/controllers/command-controller.ts +61 -8
  74. package/src/modes/controllers/event-controller.ts +1 -0
  75. package/src/modes/controllers/input-controller.ts +68 -6
  76. package/src/modes/controllers/selector-controller.ts +149 -61
  77. package/src/modes/interactive-mode.ts +63 -2
  78. package/src/modes/rpc/rpc-mode.ts +2 -1
  79. package/src/modes/session-observer-registry.ts +61 -3
  80. package/src/modes/shared.ts +2 -0
  81. package/src/modes/theme/theme.ts +102 -9
  82. package/src/modes/types.ts +2 -0
  83. package/src/modes/utils/context-usage.ts +78 -2
  84. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  85. package/src/modes/utils/ui-helpers.ts +9 -5
  86. package/src/prompts/system/personalities/default.md +26 -0
  87. package/src/prompts/system/personalities/friendly.md +17 -0
  88. package/src/prompts/system/personalities/pragmatic.md +15 -0
  89. package/src/prompts/system/snapcompact-context-frames-note.md +1 -0
  90. package/src/prompts/system/snapcompact-context-stub.md +1 -0
  91. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  92. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  93. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  94. package/src/prompts/system/system-prompt.md +5 -22
  95. package/src/prompts/tools/browser.md +33 -43
  96. package/src/prompts/tools/eval.md +27 -50
  97. package/src/prompts/tools/irc.md +29 -31
  98. package/src/prompts/tools/read.md +31 -37
  99. package/src/prompts/tools/task.md +3 -3
  100. package/src/prompts/tools/todo.md +1 -2
  101. package/src/sdk.ts +23 -1
  102. package/src/session/agent-session.ts +221 -29
  103. package/src/session/auth-storage.ts +4 -0
  104. package/src/session/codex-auto-reset.ts +190 -0
  105. package/src/session/session-dump-format.ts +8 -1
  106. package/src/session/session-manager.ts +5 -5
  107. package/src/session/snapcompact-inline.ts +524 -0
  108. package/src/slash-commands/builtin-registry.ts +145 -8
  109. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  110. package/src/slash-commands/helpers/context-report.ts +28 -1
  111. package/src/slash-commands/helpers/reset-usage.ts +66 -0
  112. package/src/slash-commands/helpers/usage-report.ts +36 -3
  113. package/src/system-prompt.ts +15 -1
  114. package/src/task/index.ts +30 -7
  115. package/src/task/render.ts +57 -32
  116. package/src/tool-discovery/tool-index.ts +2 -0
  117. package/src/tools/bash.ts +10 -3
  118. package/src/tools/eval-render.ts +13 -8
  119. package/src/tools/gh.ts +39 -1
  120. package/src/tools/image-gen.ts +114 -78
  121. package/src/tools/inspect-image.ts +1 -5
  122. package/src/tools/job.ts +25 -5
  123. package/src/tools/read.ts +1 -57
  124. package/src/tools/render-utils.ts +29 -31
  125. package/src/tools/ssh.ts +3 -3
  126. package/src/tools/todo.ts +8 -128
  127. package/src/tools/tts.ts +40 -20
  128. package/src/utils/clipboard.ts +56 -4
  129. package/src/utils/commit-message-generator.ts +1 -5
  130. package/src/utils/session-color.ts +83 -9
  131. package/src/utils/title-generator.ts +1 -1
  132. package/src/web/kagi.ts +26 -27
  133. package/src/web/search/providers/codex.ts +42 -40
  134. package/src/web/search/providers/gemini.ts +42 -22
  135. package/src/web/search/providers/perplexity.ts +22 -10
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
5
+ import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
5
6
  import { Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
6
7
  import { $ } from "bun";
7
8
  import type { SettingPath, SettingValue } from "../config/settings";
@@ -21,7 +22,7 @@ import {
21
22
  } from "../extensibility/plugins/marketplace";
22
23
  import { resolveMemoryBackend } from "../memory-backend";
23
24
  import type { InteractiveModeContext } from "../modes/types";
24
- import type { FreshSessionResult } from "../session/agent-session";
25
+ import type { AgentSession, FreshSessionResult } from "../session/agent-session";
25
26
  import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
26
27
  import { getChangelogPath, parseChangelog } from "../utils/changelog";
27
28
  import { buildContextReportText } from "./helpers/context-report";
@@ -29,6 +30,7 @@ import { formatDuration } from "./helpers/format";
29
30
  import { createMarketplaceManager } from "./helpers/marketplace-manager";
30
31
  import { handleMcpAcp } from "./helpers/mcp";
31
32
  import { commandConsumed, errorMessage, parseSlashCommand, parseSubcommand, usage } from "./helpers/parse";
33
+ import { describeRedeemOutcome, type ResetUsageAccount, toResetUsageAccounts } from "./helpers/reset-usage";
32
34
  import { handleSshAcp } from "./helpers/ssh";
33
35
  import { launchStatsDashboard, parseStatsDashboardArgs } from "./helpers/stats-dashboard";
34
36
  import { handleTodoAcp } from "./helpers/todo";
@@ -65,6 +67,95 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
65
67
  return commandConsumed();
66
68
  };
67
69
 
70
+ async function handleUsageResetCommand(
71
+ arg: string,
72
+ session: AgentSession,
73
+ output: SlashCommandRuntime["output"],
74
+ ): Promise<void> {
75
+ let accounts: ResetUsageAccount[];
76
+ try {
77
+ accounts = toResetUsageAccounts(await session.listResetCredits());
78
+ } catch (error) {
79
+ await output(`Could not load saved resets: ${errorMessage(error)}`);
80
+ return;
81
+ }
82
+ if (accounts.length === 0) {
83
+ await output("No Codex accounts found. Use /login to add one.");
84
+ return;
85
+ }
86
+ const targetArg = arg.trim();
87
+ if (!targetArg) {
88
+ const lines = ["Saved Codex rate-limit resets:"];
89
+ for (const account of accounts) {
90
+ const detail = account.error ? `unavailable (${account.error})` : `${account.availableCount} available`;
91
+ lines.push(`- ${account.label}: ${detail}${account.active ? " (active)" : ""}`);
92
+ }
93
+ lines.push("", "Spend one with `/usage reset <account email>` or `/usage reset active`.");
94
+ await output(lines.join("\n"));
95
+ return;
96
+ }
97
+ const wanted = targetArg.toLowerCase();
98
+ const target =
99
+ wanted === "active"
100
+ ? accounts.find(account => account.active)
101
+ : accounts.find(
102
+ account =>
103
+ account.label.toLowerCase() === wanted ||
104
+ account.target.email?.toLowerCase() === wanted ||
105
+ account.target.accountId?.toLowerCase() === wanted,
106
+ );
107
+ if (!target) {
108
+ await output(`No Codex account matches "${targetArg}".`);
109
+ return;
110
+ }
111
+ if (target.availableCount <= 0) {
112
+ await output(`${target.label}: no saved resets to spend.`);
113
+ return;
114
+ }
115
+ const outcome = await session.redeemResetCredit(target.target);
116
+ await output(describeRedeemOutcome(outcome, target.label));
117
+ }
118
+
119
+ const DEBUG_DUMP_NEXT_REQUEST_USAGE = "Usage: /debug dump-next-request <path>";
120
+
121
+ function resolveDebugRequestDumpPath(target: string, cwd: string): string {
122
+ const expanded =
123
+ target === "~"
124
+ ? os.homedir()
125
+ : target.startsWith("~/") || target.startsWith("~\\")
126
+ ? path.join(os.homedir(), target.slice(2))
127
+ : target;
128
+ return path.resolve(cwd, expanded);
129
+ }
130
+
131
+ async function handleDebugSubcommand(
132
+ args: string,
133
+ cwd: string,
134
+ output: (text: string) => Promise<void> | void,
135
+ ): Promise<SlashCommandResult> {
136
+ const { verb, rest } = parseSubcommand(args);
137
+ switch (verb) {
138
+ case "":
139
+ await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
140
+ return commandConsumed();
141
+ case "dump-next-request":
142
+ case "dump-request":
143
+ case "next-request": {
144
+ if (!rest) {
145
+ await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
146
+ return commandConsumed();
147
+ }
148
+ const requestPath = resolveDebugRequestDumpPath(rest, cwd);
149
+ setNextRequestDebugPath(requestPath);
150
+ await output(`Next AI provider request will be dumped to ${requestPath}`);
151
+ return commandConsumed();
152
+ }
153
+ default:
154
+ await output(`Unknown /debug subcommand "${verb}". ${DEBUG_DUMP_NEXT_REQUEST_USAGE}`);
155
+ return commandConsumed();
156
+ }
157
+ }
158
+
68
159
  /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
69
160
  function parseShakeMode(args: string): ShakeMode | { error: string } {
70
161
  const verb = args.trim().toLowerCase();
@@ -551,12 +642,41 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
551
642
  name: "usage",
552
643
  description: "Show provider usage and limits",
553
644
  acpDescription: "Show token usage",
554
- handle: async (_command, runtime) => {
555
- await runtime.output(await buildUsageReportText(runtime));
556
- return commandConsumed();
645
+ acpInputHint: "[show|reset [account|active]]",
646
+ subcommands: [
647
+ { name: "show", description: "Show provider usage and limits" },
648
+ { name: "reset", description: "Spend a saved Codex rate-limit reset", usage: "[account|active]" },
649
+ ],
650
+ allowArgs: true,
651
+ handle: async (command, runtime) => {
652
+ const { verb, rest } = parseSubcommand(command.args);
653
+ if (!verb || (verb === "show" && !rest)) {
654
+ await runtime.output(await buildUsageReportText(runtime));
655
+ return commandConsumed();
656
+ }
657
+ if (verb === "reset") {
658
+ await handleUsageResetCommand(rest, runtime.session, runtime.output);
659
+ return commandConsumed();
660
+ }
661
+ return usage("Usage: /usage [show|reset [account|active]]", runtime);
557
662
  },
558
- handleTui: async (_command, runtime) => {
559
- await runtime.ctx.handleUsageCommand();
663
+ handleTui: async (command, runtime) => {
664
+ const { verb, rest } = parseSubcommand(command.args);
665
+ if (!verb || (verb === "show" && !rest)) {
666
+ await runtime.ctx.handleUsageCommand();
667
+ runtime.ctx.editor.setText("");
668
+ return;
669
+ }
670
+ if (verb === "reset") {
671
+ if (rest) {
672
+ await handleUsageResetCommand(rest, runtime.ctx.session, text => runtime.ctx.showStatus(text));
673
+ } else {
674
+ await runtime.ctx.showResetUsageSelector();
675
+ }
676
+ runtime.ctx.editor.setText("");
677
+ return;
678
+ }
679
+ runtime.ctx.showStatus("Usage: /usage [show|reset [account|active]]");
560
680
  runtime.ctx.editor.setText("");
561
681
  },
562
682
  },
@@ -974,8 +1094,25 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
974
1094
  {
975
1095
  name: "debug",
976
1096
  description: "Open debug tools selector",
977
- handleTui: (_command, runtime) => {
978
- runtime.ctx.showDebugSelector();
1097
+ allowArgs: true,
1098
+ subcommands: [
1099
+ {
1100
+ name: "dump-next-request",
1101
+ description: "Dump the next AI provider HTTP request as JSON",
1102
+ usage: "<path>",
1103
+ },
1104
+ ],
1105
+ handle: async (command, runtime) =>
1106
+ handleDebugSubcommand(command.args, runtime.cwd, text => runtime.output(text)),
1107
+ handleTui: async (command, runtime) => {
1108
+ const args = command.args.trim();
1109
+ if (args.length === 0) {
1110
+ runtime.ctx.showDebugSelector();
1111
+ } else {
1112
+ await handleDebugSubcommand(args, runtime.ctx.sessionManager.getCwd(), text =>
1113
+ runtime.ctx.showStatus(text),
1114
+ );
1115
+ }
979
1116
  runtime.ctx.editor.setText("");
980
1117
  },
981
1118
  },
@@ -0,0 +1,44 @@
1
+ import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
2
+ import type { OAuthAccountIdentity } from "../../session/auth-storage";
3
+
4
+ function normalizeIdentityValue(value: unknown): string | undefined {
5
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
6
+ }
7
+
8
+ /**
9
+ * True when a single usage-limit column belongs to the given OAuth identity.
10
+ *
11
+ * Single definition of the matching rules for both `/usage` renderers:
12
+ * - `accountId` ↔ report metadata `accountId`/`account_id` or `limit.scope.accountId`
13
+ * - `email` ↔ report metadata `email`
14
+ * - `projectId` ↔ report metadata `projectId` or `limit.scope.projectId`
15
+ * (Google-style providers key usage on the GCP project, not an account id)
16
+ */
17
+ export function limitMatchesActiveAccount(
18
+ report: UsageReport,
19
+ limit: UsageLimit,
20
+ identity: OAuthAccountIdentity | undefined,
21
+ ): boolean {
22
+ if (!identity) return false;
23
+ const metadata = report.metadata ?? {};
24
+ const activeAccountId = normalizeIdentityValue(identity.accountId);
25
+ if (activeAccountId) {
26
+ const reportAccountId = normalizeIdentityValue(metadata.accountId) ?? normalizeIdentityValue(metadata.account_id);
27
+ if (reportAccountId === activeAccountId) return true;
28
+ if (normalizeIdentityValue(limit.scope.accountId) === activeAccountId) return true;
29
+ }
30
+ const activeEmail = normalizeIdentityValue(identity.email);
31
+ if (activeEmail && normalizeIdentityValue(metadata.email) === activeEmail) return true;
32
+ const activeProjectId = normalizeIdentityValue(identity.projectId);
33
+ if (activeProjectId) {
34
+ if (normalizeIdentityValue(metadata.projectId) === activeProjectId) return true;
35
+ if (normalizeIdentityValue(limit.scope.projectId) === activeProjectId) return true;
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /** True when any limit column in `report` belongs to the given OAuth identity. */
41
+ export function reportMatchesActiveAccount(report: UsageReport, identity: OAuthAccountIdentity | undefined): boolean {
42
+ if (!identity) return false;
43
+ return report.limits.some(limit => limitMatchesActiveAccount(report, limit, identity));
44
+ }
@@ -9,7 +9,7 @@ import { renderAsciiBar } from "./format";
9
9
  */
10
10
  export function buildContextReportText(runtime: SlashCommandRuntime): string {
11
11
  try {
12
- const breakdown = computeContextBreakdown(runtime.session);
12
+ const breakdown = computeContextBreakdown(runtime.session, { snapcompactSavings: true });
13
13
  if (breakdown.contextWindow <= 0) {
14
14
  return "Context usage is unavailable: no model is selected for this session.";
15
15
  }
@@ -30,6 +30,33 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
30
30
  const fraction = breakdown.freeTokens / breakdown.contextWindow;
31
31
  lines.push(` ${"Free".padEnd(16)} ${renderAsciiBar(fraction)} ${breakdown.freeTokens} tokens`);
32
32
  }
33
+ const snap = breakdown.snapcompact;
34
+ if (snap) {
35
+ if (!snap.visionCapable) {
36
+ lines.push("Snapcompact: inactive (model has no image input)");
37
+ } else {
38
+ lines.push("Snapcompact (estimated wire savings):");
39
+ if (snap.systemPrompt) {
40
+ const sp = snap.systemPrompt;
41
+ lines.push(
42
+ sp.applied
43
+ ? ` System prompt: ${sp.textTokens} text tokens → ${sp.frames} frame${sp.frames === 1 ? "" : "s"} ≈ ${sp.imageTokens} tokens (saves ~${sp.savedTokens})`
44
+ : " System prompt: stays text (no net savings)",
45
+ );
46
+ }
47
+ if (snap.toolResults) {
48
+ const tr = snap.toolResults;
49
+ lines.push(
50
+ tr.swapped > 0
51
+ ? ` Tool results: ${tr.swapped} of ${tr.total} imaged, ${tr.textTokens} text tokens → ${tr.frames} frames ≈ ${tr.imageTokens} tokens (saves ~${tr.savedTokens})`
52
+ : ` Tool results: none imaged (${tr.total} in history)`,
53
+ );
54
+ }
55
+ if (snap.savedTokens > 0) {
56
+ lines.push(` Estimated next request: ~${breakdown.usedTokens - snap.savedTokens} tokens on the wire`);
57
+ }
58
+ }
59
+ }
33
60
  return lines.join("\n");
34
61
  } catch {
35
62
  const fallback = runtime.session.getContextUsage();
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared helpers for the `/usage reset` command (TUI selector + ACP): turn the
3
+ * live per-account reset-credit status into selector rows, and map a redeem
4
+ * outcome code to a human message.
5
+ */
6
+ import type { ResetCreditAccountStatus, ResetCreditRedeemOutcome, ResetCreditTarget } from "../../session/auth-storage";
7
+
8
+ export const CODEX_PROVIDER_ID = "openai-codex";
9
+
10
+ /** One Codex account row for the reset-usage selector. */
11
+ export interface ResetUsageAccount {
12
+ /** Display label (email, else account id). */
13
+ label: string;
14
+ /** Saved resets redeemable for this account right now. */
15
+ availableCount: number;
16
+ /** Identifies the account when redeeming. */
17
+ target: ResetCreditTarget;
18
+ /** Whether this is the session's active Codex account. */
19
+ active: boolean;
20
+ /** Set when this account could not be reached (token/list failure). */
21
+ error?: string;
22
+ }
23
+
24
+ /**
25
+ * Map live per-account reset status to selector rows. Sorted with the active
26
+ * account first, then most-credits, then label.
27
+ */
28
+ export function toResetUsageAccounts(statuses: ResetCreditAccountStatus[]): ResetUsageAccount[] {
29
+ return statuses
30
+ .map(status => ({
31
+ label: status.email ?? status.accountId ?? "account",
32
+ availableCount: status.availableCount,
33
+ target: {
34
+ credentialId: status.credentialId,
35
+ accountId: status.accountId,
36
+ email: status.email,
37
+ } satisfies ResetCreditTarget,
38
+ active: status.active,
39
+ error: status.error,
40
+ }))
41
+ .sort((a, b) => {
42
+ if (a.active !== b.active) return a.active ? -1 : 1;
43
+ if (a.availableCount !== b.availableCount) return b.availableCount - a.availableCount;
44
+ return a.label.localeCompare(b.label);
45
+ });
46
+ }
47
+
48
+ /** Human-facing summary of a redeem outcome for status lines and ACP output. */
49
+ export function describeRedeemOutcome(outcome: ResetCreditRedeemOutcome, label: string): string {
50
+ switch (outcome.code) {
51
+ case "reset":
52
+ return `Reset applied for ${label} — your rate-limit window has been refreshed.`;
53
+ case "already_redeemed":
54
+ return `${label}: that reset was already redeemed.`;
55
+ case "no_credit":
56
+ return `${label}: no saved resets available to spend.`;
57
+ case "nothing_to_reset":
58
+ return `${label}: nothing to reset right now — your limits aren't constrained, so no credit was spent.`;
59
+ case "no_account":
60
+ return `Could not find a stored Codex account matching "${label}".`;
61
+ case "account_unavailable":
62
+ return `${label}: could not authenticate this account — try /login.`;
63
+ default:
64
+ return `${label}: reset did not apply (${outcome.code}).`;
65
+ }
66
+ }
@@ -1,5 +1,7 @@
1
1
  import type { UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
2
+ import type { OAuthAccountIdentity } from "../../session/auth-storage";
2
3
  import type { SlashCommandRuntime } from "../types";
4
+ import { reportMatchesActiveAccount } from "./active-oauth-account";
3
5
  import { formatDuration, renderAsciiBar } from "./format";
4
6
 
5
7
  function formatProviderName(provider: string): string {
@@ -31,7 +33,11 @@ function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index:
31
33
  return `account ${index + 1}`;
32
34
  }
33
35
 
34
- function renderUsageReports(reports: UsageReport[], nowMs: number): string {
36
+ function renderUsageReports(
37
+ reports: UsageReport[],
38
+ nowMs: number,
39
+ resolveActiveAccount?: (provider: string) => OAuthAccountIdentity | undefined,
40
+ ): string {
35
41
  const latestFetchedAt = Math.max(...reports.map(report => report.fetchedAt ?? 0));
36
42
  const lines = [`Usage${latestFetchedAt ? ` (${formatDuration(nowMs - latestFetchedAt)} ago)` : ""}`];
37
43
  const grouped = new Map<string, UsageReport[]>();
@@ -45,7 +51,21 @@ function renderUsageReports(reports: UsageReport[], nowMs: number): string {
45
51
  left.localeCompare(right),
46
52
  )) {
47
53
  lines.push("", formatProviderName(provider));
54
+ const activeAccount = resolveActiveAccount?.(provider);
48
55
  for (const report of providerReports) {
56
+ const inUse = reportMatchesActiveAccount(report, activeAccount);
57
+ const savedResets = report.resetCredits?.availableCount ?? 0;
58
+ if (savedResets > 0) {
59
+ const resetLabel =
60
+ typeof report.metadata?.email === "string"
61
+ ? report.metadata.email
62
+ : typeof report.metadata?.accountId === "string"
63
+ ? report.metadata.accountId
64
+ : "account";
65
+ lines.push(
66
+ `- ${resetLabel}: ${savedResets} saved rate-limit reset${savedResets === 1 ? "" : "s"} available — /usage reset to spend`,
67
+ );
68
+ }
49
69
  if (report.limits.length === 0) {
50
70
  const email = typeof report.metadata?.email === "string" ? report.metadata.email : "account";
51
71
  lines.push(`- ${email}: no limits reported`);
@@ -56,7 +76,9 @@ function renderUsageReports(reports: UsageReport[], nowMs: number): string {
56
76
  const window = limit.window?.label ?? limit.scope.windowId;
57
77
  const tier = limit.scope.tier ? ` (${limit.scope.tier})` : "";
58
78
  lines.push(`- ${limit.label}${tier}${window ? ` — ${window}` : ""}`);
59
- lines.push(` ${formatUsageReportAccount(report, limit, index)}: ${formatUsageAmount(limit)}`);
79
+ lines.push(
80
+ ` ${formatUsageReportAccount(report, limit, index)}: ${formatUsageAmount(limit)}${inUse ? " ← in use by this session" : ""}`,
81
+ );
60
82
  lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
61
83
  if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
62
84
  lines.push(` resets in ${formatDuration(limit.window.resetsAt - nowMs)}`);
@@ -79,7 +101,18 @@ export async function buildUsageReportText(runtime: SlashCommandRuntime): Promis
79
101
  };
80
102
  if (provider.fetchUsageReports) {
81
103
  const reports = await provider.fetchUsageReports();
82
- if (reports && reports.length > 0) return renderUsageReports(reports, Date.now());
104
+ if (reports && reports.length > 0) {
105
+ const currentProvider = runtime.session.model?.provider;
106
+ const activeAccount = currentProvider
107
+ ? runtime.session.modelRegistry.authStorage.getOAuthAccountIdentity(
108
+ currentProvider,
109
+ runtime.session.sessionId,
110
+ )
111
+ : undefined;
112
+ return renderUsageReports(reports, Date.now(), providerId =>
113
+ providerId === currentProvider ? activeAccount : undefined,
114
+ );
115
+ }
83
116
  }
84
117
 
85
118
  const stats = runtime.session.sessionManager.getUsageStatistics();
@@ -9,17 +9,27 @@ import { $ } from "bun";
9
9
  import { contextFileCapability } from "./capability/context-file";
10
10
  import { systemPromptCapability } from "./capability/system-prompt";
11
11
  import { findConfigFile } from "./config";
12
- import type { SkillsSettings } from "./config/settings";
12
+ import type { Personality, SkillsSettings } from "./config/settings";
13
13
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
14
14
  import { expandAtImports } from "./discovery/at-imports";
15
15
  import { loadSkills, type Skill } from "./extensibility/skills";
16
16
  import { hasObsidian } from "./internal-urls/vault-protocol";
17
17
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
18
+ import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
19
+ import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
20
+ import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" with { type: "text" };
18
21
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
19
22
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
20
23
  import { shortenPath } from "./tools/render-utils";
21
24
  import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
22
25
 
26
+ /** Bundled personality specs, keyed by the `personality` setting value. */
27
+ const PERSONALITY_SPECS: Record<Exclude<Personality, "none">, string> = {
28
+ default: defaultPersonality,
29
+ friendly: friendlyPersonality,
30
+ pragmatic: pragmaticPersonality,
31
+ };
32
+
23
33
  interface AlwaysApplyRule {
24
34
  name: string;
25
35
  content: string;
@@ -385,6 +395,8 @@ export interface BuildSystemPromptOptions {
385
395
  memoryRootEnabled?: boolean;
386
396
  /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
387
397
  model?: string;
398
+ /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
399
+ personality?: Personality;
388
400
  }
389
401
 
390
402
  /** Result of building provider-facing system prompt messages. */
@@ -419,6 +431,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
419
431
  workspaceTree: providedWorkspaceTree,
420
432
  memoryRootEnabled = false,
421
433
  model,
434
+ personality = "default",
422
435
  } = options;
423
436
  const resolvedCwd = cwd ?? getProjectDir();
424
437
 
@@ -590,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
590
603
  dateTime,
591
604
  cwd: promptCwd,
592
605
  model: model ?? "",
606
+ personality: personality === "none" ? "" : PERSONALITY_SPECS[personality].trim(),
593
607
  intentTracing: !!intentField,
594
608
  intentField: intentField ?? "",
595
609
  mcpDiscoveryMode,
package/src/task/index.ts CHANGED
@@ -698,7 +698,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
698
698
  buildDetails("running", ownJobId) as unknown as Record<string, unknown>,
699
699
  );
700
700
  try {
701
- const result = await this.#executeSync(toolCallId, spawnParams, runSignal, undefined, agentId);
701
+ const result = await this.#executeSync(
702
+ toolCallId,
703
+ spawnParams,
704
+ runSignal,
705
+ undefined,
706
+ agentId,
707
+ progress.index,
708
+ );
702
709
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
703
710
  const singleResult = result.details?.results[0];
704
711
  // A missing result means the sync path failed at the tool level
@@ -781,7 +788,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
781
788
  if (spawnItems.length === 1) {
782
789
  await semaphore.acquire();
783
790
  try {
784
- return await this.#executeSync(toolCallId, spawnParamsFor(params, spawnItems[0]), signal, onUpdate);
791
+ return await this.#executeSync(
792
+ toolCallId,
793
+ spawnParamsFor(params, spawnItems[0]),
794
+ signal,
795
+ onUpdate,
796
+ undefined,
797
+ 0,
798
+ );
785
799
  } finally {
786
800
  semaphore.release();
787
801
  }
@@ -818,7 +832,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
818
832
  }
819
833
  }
820
834
  : undefined;
821
- return await this.#executeSync(toolCallId, spawnParamsFor(params, item), workerSignal, itemOnUpdate);
835
+ return await this.#executeSync(
836
+ toolCallId,
837
+ spawnParamsFor(params, item),
838
+ workerSignal,
839
+ itemOnUpdate,
840
+ undefined,
841
+ index,
842
+ );
822
843
  } finally {
823
844
  semaphore.release();
824
845
  }
@@ -875,8 +896,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
875
896
  signal?: AbortSignal,
876
897
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
877
898
  preAllocatedId?: string,
899
+ spawnIndex = 0,
878
900
  ): Promise<AgentToolResult<TaskToolDetails>> {
879
- return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId);
901
+ return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex);
880
902
  }
881
903
 
882
904
  /** Spawn a fresh subagent and run it to completion. */
@@ -886,6 +908,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
886
908
  signal?: AbortSignal,
887
909
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
888
910
  preAllocatedId?: string,
911
+ spawnIndex = 0,
889
912
  ): Promise<AgentToolResult<TaskToolDetails>> {
890
913
  const startTime = Date.now();
891
914
  const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
@@ -1070,7 +1093,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1070
1093
 
1071
1094
  // Progress tracking for the single agent
1072
1095
  let latestProgress: AgentProgress = {
1073
- index: 0,
1096
+ index: spawnIndex,
1074
1097
  id: agentId,
1075
1098
  agent: agentName,
1076
1099
  agentSource: agent.source,
@@ -1120,7 +1143,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1120
1143
  context: sharedContext,
1121
1144
  planReference,
1122
1145
  description: params.description,
1123
- index: 0,
1146
+ index: spawnIndex,
1124
1147
  parentToolCallId: toolCallId,
1125
1148
  id: agentId,
1126
1149
  taskDepth,
@@ -1226,7 +1249,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1226
1249
  } catch (err) {
1227
1250
  const message = err instanceof Error ? err.message : String(err);
1228
1251
  return {
1229
- index: 0,
1252
+ index: spawnIndex,
1230
1253
  id: agentId,
1231
1254
  agent: agent.name,
1232
1255
  agentSource: agent.source,