@oh-my-pi/pi-coding-agent 13.9.2 → 13.9.4

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 (53) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/examples/sdk/02-custom-model.ts +2 -1
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +10 -6
  5. package/src/cli/list-models.ts +2 -2
  6. package/src/commands/launch.ts +3 -3
  7. package/src/config/model-registry.ts +136 -38
  8. package/src/config/model-resolver.ts +47 -21
  9. package/src/config/settings-schema.ts +56 -2
  10. package/src/discovery/helpers.ts +3 -3
  11. package/src/extensibility/custom-tools/types.ts +2 -0
  12. package/src/extensibility/extensions/loader.ts +3 -2
  13. package/src/extensibility/extensions/types.ts +10 -7
  14. package/src/extensibility/hooks/types.ts +2 -0
  15. package/src/main.ts +5 -22
  16. package/src/memories/index.ts +7 -3
  17. package/src/modes/components/footer.ts +10 -8
  18. package/src/modes/components/model-selector.ts +33 -38
  19. package/src/modes/components/settings-defs.ts +32 -3
  20. package/src/modes/components/settings-selector.ts +16 -5
  21. package/src/modes/components/status-line/context-thresholds.ts +68 -0
  22. package/src/modes/components/status-line/segments.ts +11 -12
  23. package/src/modes/components/status-line.ts +2 -6
  24. package/src/modes/components/thinking-selector.ts +7 -7
  25. package/src/modes/components/tree-selector.ts +3 -2
  26. package/src/modes/controllers/command-controller.ts +11 -26
  27. package/src/modes/controllers/event-controller.ts +16 -3
  28. package/src/modes/controllers/input-controller.ts +4 -2
  29. package/src/modes/controllers/selector-controller.ts +5 -4
  30. package/src/modes/interactive-mode.ts +2 -2
  31. package/src/modes/rpc/rpc-client.ts +5 -10
  32. package/src/modes/rpc/rpc-types.ts +5 -5
  33. package/src/modes/theme/theme.ts +8 -3
  34. package/src/priority.json +1 -0
  35. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  36. package/src/prompts/system/system-prompt.md +18 -2
  37. package/src/prompts/tools/hashline.md +139 -83
  38. package/src/sdk.ts +24 -16
  39. package/src/session/agent-session.ts +261 -118
  40. package/src/session/agent-storage.ts +14 -14
  41. package/src/session/compaction/compaction.ts +500 -13
  42. package/src/session/messages.ts +12 -1
  43. package/src/session/session-manager.ts +77 -19
  44. package/src/slash-commands/builtin-registry.ts +48 -0
  45. package/src/task/agents.ts +3 -2
  46. package/src/task/executor.ts +2 -2
  47. package/src/task/types.ts +2 -1
  48. package/src/thinking.ts +87 -0
  49. package/src/tools/browser.ts +15 -6
  50. package/src/tools/fetch.ts +118 -100
  51. package/src/tools/index.ts +2 -1
  52. package/src/web/kagi.ts +62 -7
  53. package/src/web/search/providers/exa.ts +74 -3
@@ -1,16 +1,11 @@
1
- import {
2
- getAvailableThinkingLevels,
3
- getThinkingMetadata,
4
- type Model,
5
- modelsAreEqual,
6
- supportsXhigh,
7
- type ThinkingMode,
8
- } from "@oh-my-pi/pi-ai";
1
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import { getSupportedEfforts, type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
9
3
  import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
10
4
  import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
11
5
  import { resolveModelRoleValue } from "../../config/model-resolver";
12
6
  import type { Settings } from "../../config/settings";
13
7
  import { type ThemeColor, theme } from "../../modes/theme/theme";
8
+ import { getThinkingLevelMetadata } from "../../thinking";
14
9
  import { fuzzyFilter } from "../../utils/fuzzy";
15
10
  import { getTabBarTheme } from "../shared";
16
11
  import { DynamicBorder } from "./dynamic-border";
@@ -29,15 +24,15 @@ interface ModelItem {
29
24
 
30
25
  interface ScopedModelItem {
31
26
  model: Model;
32
- thinkingLevel: string;
27
+ thinkingLevel?: string;
33
28
  }
34
29
 
35
30
  interface RoleAssignment {
36
31
  model: Model;
37
- thinkingMode: ThinkingMode;
32
+ thinkingLevel: ThinkingLevel;
38
33
  }
39
34
 
40
- type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void;
35
+ type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void;
41
36
  type CancelCallback = () => void;
42
37
  interface MenuRoleAction {
43
38
  label: string;
@@ -97,7 +92,7 @@ export class ModelSelectorComponent extends Container {
97
92
  settings: Settings,
98
93
  modelRegistry: ModelRegistry,
99
94
  scopedModels: ReadonlyArray<ScopedModelItem>,
100
- onSelect: (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void,
95
+ onSelect: (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void,
101
96
  onCancel: () => void,
102
97
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
103
98
  ) {
@@ -192,7 +187,8 @@ export class ModelSelectorComponent extends Container {
192
187
  if (model) {
193
188
  this.#roles[role] = {
194
189
  model,
195
- thinkingMode: explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : "inherit",
190
+ thinkingLevel:
191
+ explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : ThinkingLevel.Inherit,
196
192
  };
197
193
  }
198
194
  }
@@ -409,7 +405,7 @@ export class ModelSelectorComponent extends Container {
409
405
  if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
410
406
 
411
407
  const badge = makeInvertedBadge(tag, color ?? "success");
412
- const thinkingLabel = getThinkingMetadata(assigned.thinkingMode).label;
408
+ const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
413
409
  roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
414
410
  }
415
411
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
@@ -456,19 +452,18 @@ export class ModelSelectorComponent extends Container {
456
452
  this.#listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
457
453
  }
458
454
  }
459
- #getThinkingModesForModel(model: Model): ReadonlyArray<ThinkingMode> {
460
- return ["inherit", ...getAvailableThinkingLevels(supportsXhigh(model))];
455
+ #getThinkingLevelsForModel(model: Model): ReadonlyArray<ThinkingLevel> {
456
+ return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
461
457
  }
462
458
 
463
- #getCurrentRoleThinkingMode(role: ModelRole): ThinkingMode {
464
- return this.#roles[role]?.thinkingMode ?? "inherit";
459
+ #getCurrentRoleThinkingLevel(role: ModelRole): ThinkingLevel {
460
+ return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
465
461
  }
466
462
 
467
463
  #getThinkingPreselectIndex(role: ModelRole, model: Model): number {
468
- const options = this.#getThinkingModesForModel(model);
469
- const currentMode = this.#getCurrentRoleThinkingMode(role);
470
- const preferredMode = currentMode === "xhigh" && !options.includes("xhigh") ? "high" : currentMode;
471
- const foundIndex = options.indexOf(preferredMode);
464
+ const options = this.#getThinkingLevelsForModel(model);
465
+ const currentLevel = this.#getCurrentRoleThinkingLevel(role);
466
+ const foundIndex = options.indexOf(currentLevel);
472
467
  return foundIndex >= 0 ? foundIndex : 0;
473
468
  }
474
469
 
@@ -496,11 +491,11 @@ export class ModelSelectorComponent extends Container {
496
491
  if (!selectedModel) return;
497
492
 
498
493
  const showingThinking = this.#menuStep === "thinking" && this.#menuSelectedRole !== null;
499
- const thinkingOptions = showingThinking ? this.#getThinkingModesForModel(selectedModel.model) : [];
494
+ const thinkingOptions = showingThinking ? this.#getThinkingLevelsForModel(selectedModel.model) : [];
500
495
  const optionLines = showingThinking
501
- ? thinkingOptions.map((thinkingMode, index) => {
496
+ ? thinkingOptions.map((thinkingLevel, index) => {
502
497
  const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
503
- const label = getThinkingMetadata(thinkingMode).label;
498
+ const label = getThinkingLevelMetadata(thinkingLevel).label;
504
499
  return `${prefix}${label}`;
505
500
  })
506
501
  : MENU_ROLE_ACTIONS.map((action, index) => {
@@ -607,7 +602,7 @@ export class ModelSelectorComponent extends Container {
607
602
 
608
603
  const optionCount =
609
604
  this.#menuStep === "thinking" && this.#menuSelectedRole !== null
610
- ? this.#getThinkingModesForModel(selectedModel.model).length
605
+ ? this.#getThinkingLevelsForModel(selectedModel.model).length
611
606
  : MENU_ROLE_ACTIONS.length;
612
607
  if (optionCount === 0) return;
613
608
 
@@ -635,10 +630,10 @@ export class ModelSelectorComponent extends Container {
635
630
  }
636
631
 
637
632
  if (!this.#menuSelectedRole) return;
638
- const thinkingOptions = this.#getThinkingModesForModel(selectedModel.model);
639
- const thinkingMode = thinkingOptions[this.#menuSelectedIndex];
640
- if (!thinkingMode) return;
641
- this.#handleSelect(selectedModel.model, this.#menuSelectedRole, thinkingMode);
633
+ const thinkingOptions = this.#getThinkingLevelsForModel(selectedModel.model);
634
+ const thinkingLevel = thinkingOptions[this.#menuSelectedIndex];
635
+ if (!thinkingLevel) return;
636
+ this.#handleSelect(selectedModel.model, this.#menuSelectedRole, thinkingLevel);
642
637
  this.#closeMenu();
643
638
  return;
644
639
  }
@@ -657,28 +652,28 @@ export class ModelSelectorComponent extends Container {
657
652
  }
658
653
  }
659
654
 
660
- #formatRoleModelValue(model: Model, thinkingMode: ThinkingMode): string {
655
+ #formatRoleModelValue(model: Model, thinkingLevel: ThinkingLevel): string {
661
656
  const modelKey = `${model.provider}/${model.id}`;
662
- if (thinkingMode === "inherit") return modelKey;
663
- return `${modelKey}:${thinkingMode}`;
657
+ if (thinkingLevel === ThinkingLevel.Inherit) return modelKey;
658
+ return `${modelKey}:${thinkingLevel}`;
664
659
  }
665
- #handleSelect(model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode): void {
660
+ #handleSelect(model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel): void {
666
661
  // For temporary role, don't save to settings - just notify caller
667
662
  if (role === null) {
668
663
  this.#onSelectCallback(model, null);
669
664
  return;
670
665
  }
671
666
 
672
- const selectedThinkingMode = thinkingMode ?? this.#getCurrentRoleThinkingMode(role);
667
+ const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
673
668
 
674
669
  // Save to settings
675
- this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingMode));
670
+ this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingLevel));
676
671
 
677
672
  // Update local state for UI
678
- this.#roles[role] = { model, thinkingMode: selectedThinkingMode };
673
+ this.#roles[role] = { model, thinkingLevel: selectedThinkingLevel };
679
674
 
680
675
  // Notify caller (for updating agent state if needed)
681
- this.#onSelectCallback(model, role, selectedThinkingMode);
676
+ this.#onSelectCallback(model, role, selectedThinkingLevel);
682
677
 
683
678
  // Update list to show new badges
684
679
  this.#updateList();
@@ -7,7 +7,7 @@
7
7
  * 2. That's it - it appears in the UI automatically
8
8
  */
9
9
 
10
- import { getAvailableThinkingLevels, getThinkingMetadata } from "@oh-my-pi/pi-ai";
10
+ import { THINKING_EFFORTS } from "@oh-my-pi/pi-ai";
11
11
  import { TERMINAL } from "@oh-my-pi/pi-tui";
12
12
  import {
13
13
  getDefault,
@@ -19,6 +19,7 @@ import {
19
19
  type SettingPath,
20
20
  type SettingTab,
21
21
  } from "../../config/settings-schema";
22
+ import { getThinkingLevelMetadata } from "../../thinking";
22
23
 
23
24
  // ═══════════════════════════════════════════════════════════════════════════
24
25
  // UI Definition Types
@@ -68,6 +69,26 @@ type OptionList = ReadonlyArray<{ value: string; label: string; description?: st
68
69
  type OptionProvider = (() => OptionList) | OptionList;
69
70
 
70
71
  const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
72
+ // Context maintenance strategy
73
+ "compaction.strategy": [
74
+ { value: "context-full", label: "Context-full", description: "Summarize in-place and keep the current session" },
75
+ { value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
76
+ {
77
+ value: "off",
78
+ label: "Off",
79
+ description: "Disable automatic context maintenance (same behavior as Auto-compact off)",
80
+ },
81
+ ],
82
+ // Context maintenance threshold
83
+ "compaction.thresholdPercent": [
84
+ { value: "default", label: "Default", description: "Legacy reserve-based threshold" },
85
+ { value: "70", label: "70%", description: "Very early maintenance" },
86
+ { value: "75", label: "75%", description: "Early maintenance" },
87
+ { value: "80", label: "80%", description: "Balanced" },
88
+ { value: "85", label: "85%", description: "Typical threshold" },
89
+ { value: "90", label: "90%", description: "Aggressive context usage" },
90
+ { value: "95", label: "95%", description: "Near context limit" },
91
+ ],
71
92
  // Retry max retries
72
93
  "retry.maxRetries": [
73
94
  { value: "1", label: "1 retry" },
@@ -213,7 +234,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
213
234
  { value: "perplexity", label: "Perplexity", description: "Requires PERPLEXITY_COOKIES or PERPLEXITY_API_KEY" },
214
235
  { value: "anthropic", label: "Anthropic", description: "Uses Anthropic web search" },
215
236
  { value: "zai", label: "Z.AI", description: "Calls Z.AI webSearchPrime MCP" },
216
- { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY" },
237
+ { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
217
238
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
218
239
  ],
219
240
  "providers.image": [
@@ -231,7 +252,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
231
252
  { value: "on", label: "On", description: "Force websockets for OpenAI Codex models" },
232
253
  ],
233
254
  // Default thinking level
234
- defaultThinkingLevel: [...getAvailableThinkingLevels().map(getThinkingMetadata)],
255
+ defaultThinkingLevel: [...THINKING_EFFORTS.map(getThinkingLevelMetadata)],
235
256
  // Temperature
236
257
  temperature: [
237
258
  { value: "-1", label: "Default", description: "Use provider default" },
@@ -277,6 +298,14 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
277
298
  { value: "1.2", label: "1.2", description: "Balanced" },
278
299
  { value: "1.5", label: "1.5", description: "Strong penalty" },
279
300
  ],
301
+ serviceTier: [
302
+ { value: "none", label: "None", description: "Omit service_tier parameter" },
303
+ { value: "auto", label: "Auto", description: "Use provider default tier selection" },
304
+ { value: "default", label: "Default", description: "Standard priority processing" },
305
+ { value: "flex", label: "Flex", description: "Use flexible capacity tier when available" },
306
+ { value: "scale", label: "Scale", description: "Use Scale Tier credits when available" },
307
+ { value: "priority", label: "Priority", description: "Use Priority processing" },
308
+ ],
280
309
  // Symbol preset
281
310
  symbolPreset: [
282
311
  { value: "unicode", label: "Unicode", description: "Standard symbols (default)" },
@@ -1,4 +1,5 @@
1
- import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import type { Effort } from "@oh-my-pi/pi-ai";
2
3
  import {
3
4
  Container,
4
5
  matchesKey,
@@ -134,9 +135,9 @@ function getSettingsTabs(): Tab[] {
134
135
  */
135
136
  export interface SettingsRuntimeContext {
136
137
  /** Available thinking levels (from session) */
137
- availableThinkingLevels: ThinkingLevel[];
138
+ availableThinkingLevels: Effort[];
138
139
  /** Current thinking level (from session) */
139
- thinkingLevel: ThinkingLevel;
140
+ thinkingLevel: ThinkingLevel | undefined;
140
141
  /** Available themes */
141
142
  availableThemes: string[];
142
143
  /** Working directory for plugins tab */
@@ -272,7 +273,7 @@ export class SettingsSelectorComponent extends Container {
272
273
  id: def.path,
273
274
  label: def.label,
274
275
  description: def.description,
275
- currentValue: String(currentValue ?? ""),
276
+ currentValue: this.#getSubmenuCurrentValue(def.path, currentValue),
276
277
  submenu: (cv, done) => this.#createSubmenu(def, cv, done),
277
278
  };
278
279
  }
@@ -285,6 +286,14 @@ export class SettingsSelectorComponent extends Container {
285
286
  return settings.get(def.path);
286
287
  }
287
288
 
289
+ #getSubmenuCurrentValue(path: SettingPath, value: unknown): string {
290
+ const rawValue = String(value ?? "");
291
+ if (path === "compaction.thresholdPercent" && (rawValue === "-1" || rawValue === "")) {
292
+ return "default";
293
+ }
294
+ return rawValue;
295
+ }
296
+
288
297
  /**
289
298
  * Create a submenu for a submenu-type setting.
290
299
  */
@@ -382,7 +391,9 @@ export class SettingsSelectorComponent extends Container {
382
391
  #setSettingValue(path: SettingPath, value: string): void {
383
392
  // Handle number conversions
384
393
  const currentValue = settings.get(path);
385
- if (typeof currentValue === "number") {
394
+ if (path === "compaction.thresholdPercent" && value === "default") {
395
+ settings.set(path, -1 as never);
396
+ } else if (typeof currentValue === "number") {
386
397
  settings.set(path, Number(value) as never);
387
398
  } else if (typeof currentValue === "boolean") {
388
399
  settings.set(path, (value === "true") as never);
@@ -0,0 +1,68 @@
1
+ import type { ThemeColor } from "../../../modes/theme/theme";
2
+
3
+ export type ContextUsageLevel = "normal" | "warning" | "purple" | "error";
4
+
5
+ const CONTEXT_WARNING_PERCENT_THRESHOLD = 50;
6
+ const CONTEXT_WARNING_TOKEN_THRESHOLD = 150_000;
7
+ const CONTEXT_PURPLE_PERCENT_THRESHOLD = 70;
8
+ const CONTEXT_PURPLE_TOKEN_THRESHOLD = 270_000;
9
+ const CONTEXT_ERROR_PERCENT_THRESHOLD = 90;
10
+ const CONTEXT_ERROR_TOKEN_THRESHOLD = 500_000;
11
+
12
+ function reachesThreshold(
13
+ contextPercent: number,
14
+ contextWindow: number,
15
+ percentThreshold: number,
16
+ tokenThreshold: number,
17
+ ): boolean {
18
+ if (!Number.isFinite(contextPercent) || contextPercent <= 0) {
19
+ return false;
20
+ }
21
+
22
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
23
+ return contextPercent >= percentThreshold;
24
+ }
25
+
26
+ const tokenPercentThreshold = (tokenThreshold / contextWindow) * 100;
27
+ return contextPercent >= Math.min(percentThreshold, tokenPercentThreshold);
28
+ }
29
+
30
+ export function getContextUsageLevel(contextPercent: number, contextWindow: number): ContextUsageLevel {
31
+ if (
32
+ reachesThreshold(contextPercent, contextWindow, CONTEXT_ERROR_PERCENT_THRESHOLD, CONTEXT_ERROR_TOKEN_THRESHOLD)
33
+ ) {
34
+ return "error";
35
+ }
36
+
37
+ if (
38
+ reachesThreshold(contextPercent, contextWindow, CONTEXT_PURPLE_PERCENT_THRESHOLD, CONTEXT_PURPLE_TOKEN_THRESHOLD)
39
+ ) {
40
+ return "purple";
41
+ }
42
+
43
+ if (
44
+ reachesThreshold(
45
+ contextPercent,
46
+ contextWindow,
47
+ CONTEXT_WARNING_PERCENT_THRESHOLD,
48
+ CONTEXT_WARNING_TOKEN_THRESHOLD,
49
+ )
50
+ ) {
51
+ return "warning";
52
+ }
53
+
54
+ return "normal";
55
+ }
56
+
57
+ export function getContextUsageThemeColor(level: ContextUsageLevel): ThemeColor {
58
+ switch (level) {
59
+ case "error":
60
+ return "error";
61
+ case "purple":
62
+ return "thinkingHigh";
63
+ case "warning":
64
+ return "warning";
65
+ case "normal":
66
+ return "statusLineContext";
67
+ }
68
+ }
@@ -1,8 +1,10 @@
1
1
  import * as os from "node:os";
2
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
3
  import { TERMINAL } from "@oh-my-pi/pi-tui";
3
4
  import { formatDuration, formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
4
5
  import { theme } from "../../../modes/theme/theme";
5
6
  import { shortenPath } from "../../../tools/render-utils";
7
+ import { getContextUsageLevel, getContextUsageThemeColor } from "./context-thresholds";
6
8
  import type { RenderedSegment, SegmentContext, StatusLineSegment, StatusLineSegmentId } from "./types";
7
9
 
8
10
  export type { SegmentContext } from "./types";
@@ -44,10 +46,14 @@ const modelSegment: StatusLineSegment = {
44
46
 
45
47
  let content = withIcon(theme.icon.model, modelName);
46
48
 
49
+ if (ctx.session.isFastModeEnabled() && theme.icon.fast) {
50
+ content += ` ${theme.icon.fast}`;
51
+ }
52
+
47
53
  // Add thinking level with dot separator
48
- if (opts.showThinkingLevel !== false && state.model?.reasoning) {
49
- const level = state.thinkingLevel || "off";
50
- if (level !== "off") {
54
+ if (opts.showThinkingLevel !== false && state.model?.thinking) {
55
+ const level = state.thinkingLevel ?? ThinkingLevel.Off;
56
+ if (level !== ThinkingLevel.Off) {
51
57
  const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
52
58
  if (thinkingText) {
53
59
  content += `${theme.sep.dot}${thinkingText}`;
@@ -244,15 +250,8 @@ const contextPctSegment: StatusLineSegment = {
244
250
  const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
245
251
  const text = `${pct.toFixed(1)}%/${formatNumber(window)}${autoIcon}`;
246
252
 
247
- let content: string;
248
- if (pct > 90) {
249
- content = withIcon(theme.icon.context, theme.fg("error", text));
250
- } else if (pct > 70) {
251
- content = withIcon(theme.icon.context, theme.fg("warning", text));
252
- } else {
253
- const colored = theme.fg("statusLineContext", text);
254
- content = withIcon(theme.icon.context, colored);
255
- }
253
+ const color = getContextUsageThemeColor(getContextUsageLevel(pct, window));
254
+ const content = withIcon(theme.icon.context, theme.fg(color, text));
256
255
 
257
256
  return { content, visible: true };
258
257
  },
@@ -7,6 +7,7 @@ import { settings } from "../../config/settings";
7
7
  import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../config/settings-schema";
8
8
  import { theme } from "../../modes/theme/theme";
9
9
  import type { AgentSession } from "../../session/agent-session";
10
+ import { calculatePromptTokens } from "../../session/compaction/compaction";
10
11
  import { findGitHeadPathSync, sanitizeStatusText } from "../shared";
11
12
  import {
12
13
  canReuseCachedPr,
@@ -365,12 +366,7 @@ export class StatusLineComponent implements Component {
365
366
  .reverse()
366
367
  .find(m => m.role === "assistant" && m.stopReason !== "aborted") as AssistantMessage | undefined;
367
368
 
368
- const contextTokens = lastAssistantMessage
369
- ? lastAssistantMessage.usage.input +
370
- lastAssistantMessage.usage.output +
371
- lastAssistantMessage.usage.cacheRead +
372
- lastAssistantMessage.usage.cacheWrite
373
- : 0;
369
+ const contextTokens = lastAssistantMessage ? calculatePromptTokens(lastAssistantMessage.usage) : 0;
374
370
  const contextWindow = state.model?.contextWindow || 0;
375
371
  const contextPercent = contextWindow > 0 ? (contextTokens / contextWindow) * 100 : 0;
376
372
 
@@ -1,7 +1,7 @@
1
- import { getThinkingMetadata, type ThinkingLevel } from "@oh-my-pi/pi-ai";
2
-
1
+ import type { Effort } from "@oh-my-pi/pi-ai";
3
2
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
4
3
  import { getSelectListTheme } from "../../modes/theme/theme";
4
+ import { getThinkingLevelMetadata } from "../../thinking";
5
5
  import { DynamicBorder } from "./dynamic-border";
6
6
 
7
7
  /**
@@ -11,14 +11,14 @@ export class ThinkingSelectorComponent extends Container {
11
11
  #selectList: SelectList;
12
12
 
13
13
  constructor(
14
- currentLevel: ThinkingLevel,
15
- availableLevels: ThinkingLevel[],
16
- onSelect: (level: ThinkingLevel) => void,
14
+ currentLevel: Effort,
15
+ availableLevels: Effort[],
16
+ onSelect: (level: Effort) => void,
17
17
  onCancel: () => void,
18
18
  ) {
19
19
  super();
20
20
 
21
- const thinkingLevels: SelectItem[] = availableLevels.map(getThinkingMetadata);
21
+ const thinkingLevels: SelectItem[] = availableLevels.map(getThinkingLevelMetadata);
22
22
 
23
23
  // Add top border
24
24
  this.addChild(new DynamicBorder());
@@ -33,7 +33,7 @@ export class ThinkingSelectorComponent extends Container {
33
33
  }
34
34
 
35
35
  this.#selectList.onSelect = item => {
36
- onSelect(item.value as ThinkingLevel);
36
+ onSelect(item.value as Effort);
37
37
  };
38
38
 
39
39
  this.#selectList.onCancel = () => {
@@ -1,3 +1,4 @@
1
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
2
  import {
2
3
  type Component,
3
4
  Container,
@@ -382,7 +383,7 @@ class TreeList implements Component {
382
383
  parts.push("model", entry.model);
383
384
  break;
384
385
  case "thinking_level_change":
385
- parts.push("thinking", entry.thinkingLevel);
386
+ parts.push("thinking", entry.thinkingLevel ?? ThinkingLevel.Off);
386
387
  break;
387
388
  case "custom":
388
389
  parts.push("custom", entry.customType);
@@ -585,7 +586,7 @@ class TreeList implements Component {
585
586
  result = theme.fg("dim", `[model: ${entry.model}]`);
586
587
  break;
587
588
  case "thinking_level_change":
588
- result = theme.fg("dim", `[thinking: ${entry.thinkingLevel}]`);
589
+ result = theme.fg("dim", `[thinking: ${entry.thinkingLevel ?? ThinkingLevel.Off}]`);
589
590
  break;
590
591
  case "custom":
591
592
  result = theme.fg("dim", `[custom: ${entry.customType}]`);
@@ -25,7 +25,6 @@ import { getMarkdownTheme, getSymbolTheme, theme } from "../../modes/theme/theme
25
25
  import type { InteractiveModeContext } from "../../modes/types";
26
26
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
27
27
  import type { AuthStorage } from "../../session/auth-storage";
28
- import { createCompactionSummaryMessage } from "../../session/messages";
29
28
  import { outputMeta } from "../../tools/output-meta";
30
29
  import { resolveToCwd } from "../../tools/path-utils";
31
30
  import { replaceTabs } from "../../tools/render-utils";
@@ -776,18 +775,10 @@ export class CommandController {
776
775
  customInstructionsOrOptions && typeof customInstructionsOrOptions === "object"
777
776
  ? customInstructionsOrOptions
778
777
  : undefined;
779
- const result = await this.ctx.session.compact(instructions, options);
778
+ await this.ctx.session.compact(instructions, options);
780
779
 
781
780
  this.ctx.rebuildChatFromMessages();
782
781
 
783
- const msg = createCompactionSummaryMessage(
784
- result.summary,
785
- result.tokensBefore,
786
- new Date().toISOString(),
787
- result.shortSummary,
788
- );
789
- this.ctx.addMessageToChat(msg);
790
-
791
782
  this.ctx.statusLine.invalidate();
792
783
  this.ctx.updateEditorTopBorder();
793
784
  } catch (error) {
@@ -834,6 +825,9 @@ export class CommandController {
834
825
  this.ctx.chatContainer.addChild(
835
826
  new Text(`${theme.fg("accent", `${theme.status.success} New session started with handoff context`)}`, 1, 1),
836
827
  );
828
+ if (result.savedPath) {
829
+ this.ctx.showStatus(`Handoff document saved to: ${result.savedPath}`);
830
+ }
837
831
  } catch (error) {
838
832
  const message = error instanceof Error ? error.message : String(error);
839
833
  if (message === "Handoff cancelled" || (error instanceof Error && error.name === "AbortError")) {
@@ -959,9 +953,6 @@ function formatAccountLabel(limit: UsageLimit, report: UsageReport, index: numbe
959
953
  }
960
954
 
961
955
  function formatResetShort(limit: UsageLimit, nowMs: number): string | undefined {
962
- if (limit.window?.resetInMs !== undefined) {
963
- return formatDuration(limit.window.resetInMs);
964
- }
965
956
  if (limit.window?.resetsAt !== undefined) {
966
957
  return formatDuration(limit.window.resetsAt - nowMs);
967
958
  }
@@ -1021,19 +1012,13 @@ function formatAggregateAmount(limits: UsageLimit[]): string {
1021
1012
  }
1022
1013
 
1023
1014
  function resolveResetRange(limits: UsageLimit[], nowMs: number): string | null {
1024
- const resets = limits
1025
- .map(limit => limit.window?.resetInMs ?? undefined)
1026
- .filter((value): value is number => value !== undefined && Number.isFinite(value) && value > 0);
1027
- if (resets.length === 0) {
1028
- const absolute = limits
1029
- .map(limit => limit.window?.resetsAt)
1030
- .filter((value): value is number => value !== undefined && Number.isFinite(value) && value > nowMs);
1031
- if (absolute.length === 0) return null;
1032
- const earliest = Math.min(...absolute);
1033
- return `resets at ${new Date(earliest).toLocaleString()}`;
1034
- }
1035
- const minReset = Math.min(...resets);
1036
- const maxReset = Math.max(...resets);
1015
+ const absolute = limits
1016
+ .map(limit => limit.window?.resetsAt)
1017
+ .filter((value): value is number => value !== undefined && Number.isFinite(value) && value > nowMs);
1018
+ if (absolute.length === 0) return null;
1019
+ const offsets = absolute.map(value => value - nowMs);
1020
+ const minReset = Math.min(...offsets);
1021
+ const maxReset = Math.max(...offsets);
1037
1022
  if (maxReset - minReset > 60_000) {
1038
1023
  return `resets in ${formatDuration(minReset)}–${formatDuration(maxReset)}`;
1039
1024
  }
@@ -438,11 +438,12 @@ export class EventController {
438
438
  };
439
439
  this.ctx.statusContainer.clear();
440
440
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
441
+ const actionLabel = event.action === "handoff" ? "Auto-handoff" : "Auto context-full maintenance";
441
442
  this.ctx.autoCompactionLoader = new Loader(
442
443
  this.ctx.ui,
443
444
  spinner => theme.fg("accent", spinner),
444
445
  text => theme.fg("muted", text),
445
- `${reasonText}Auto-compacting… (esc to cancel)`,
446
+ `${reasonText}${actionLabel}… (esc to cancel)`,
446
447
  getSymbolTheme().spinnerFrames,
447
448
  );
448
449
  this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
@@ -460,8 +461,11 @@ export class EventController {
460
461
  this.ctx.autoCompactionLoader = undefined;
461
462
  this.ctx.statusContainer.clear();
462
463
  }
464
+ const isHandoffAction = event.action === "handoff";
463
465
  if (event.aborted) {
464
- this.ctx.showStatus("Auto-compaction cancelled");
466
+ this.ctx.showStatus(
467
+ isHandoffAction ? "Auto-handoff cancelled" : "Auto context-full maintenance cancelled",
468
+ );
465
469
  } else if (event.result) {
466
470
  this.ctx.chatContainer.clear();
467
471
  this.ctx.rebuildChatFromMessages();
@@ -474,8 +478,17 @@ export class EventController {
474
478
  });
475
479
  this.ctx.statusLine.invalidate();
476
480
  this.ctx.updateEditorTopBorder();
481
+ } else if (event.errorMessage) {
482
+ this.ctx.showWarning(event.errorMessage);
483
+ } else if (isHandoffAction) {
484
+ this.ctx.chatContainer.clear();
485
+ this.ctx.rebuildChatFromMessages();
486
+ this.ctx.statusLine.invalidate();
487
+ this.ctx.updateEditorTopBorder();
488
+ await this.ctx.reloadTodos();
489
+ this.ctx.showStatus("Auto-handoff completed");
477
490
  } else {
478
- this.ctx.showWarning("Auto-compaction failed; continuing without compaction");
491
+ this.ctx.showWarning("Auto context-full maintenance failed; continuing without maintenance");
479
492
  }
480
493
  await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
481
494
  this.ctx.ui.requestRender();
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import { type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
3
  import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
4
4
  import { $env } from "@oh-my-pi/pi-utils";
5
5
  import { settings } from "../../config/settings";
@@ -544,7 +544,9 @@ export class InputController {
544
544
  const roleLabel = result.role === "default" ? "default" : result.role;
545
545
  const roleLabelStyled = theme.bold(theme.fg("accent", roleLabel));
546
546
  const thinkingStr =
547
- result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
547
+ result.model.thinking && result.thinkingLevel !== ThinkingLevel.Off
548
+ ? ` (thinking: ${result.thinkingLevel})`
549
+ : "";
548
550
  const tempLabel = options?.temporary ? " (temporary)" : "";
549
551
  const cycleSeparator = theme.fg("dim", " > ");
550
552
  const cycleLabel = roleOrder
@@ -1,4 +1,5 @@
1
- import { getOAuthProviders, type OAuthProvider, type ThinkingLevel } from "@oh-my-pi/pi-ai";
1
+ import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
+ import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
2
3
  import type { Component } from "@oh-my-pi/pi-tui";
3
4
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
4
5
  import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
@@ -380,7 +381,7 @@ export class SelectorController {
380
381
  this.ctx.settings,
381
382
  this.ctx.session.modelRegistry,
382
383
  this.ctx.session.scopedModels,
383
- async (model, role, thinkingMode) => {
384
+ async (model, role, thinkingLevel) => {
384
385
  try {
385
386
  if (role === null) {
386
387
  // Temporary: update agent state but don't persist to settings
@@ -393,8 +394,8 @@ export class SelectorController {
393
394
  } else if (role === "default") {
394
395
  // Default: update agent state and persist
395
396
  await this.ctx.session.setModel(model, role);
396
- if (thinkingMode && thinkingMode !== "inherit") {
397
- this.ctx.session.setThinkingLevel(thinkingMode as ThinkingLevel);
397
+ if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
398
+ this.ctx.session.setThinkingLevel(thinkingLevel);
398
399
  }
399
400
  this.ctx.statusLine.invalidate();
400
401
  this.ctx.updateEditorBorderColor();