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

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 (50) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/examples/sdk/02-custom-model.ts +2 -1
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +6 -5
  5. package/src/cli/list-models.ts +2 -2
  6. package/src/commands/launch.ts +3 -3
  7. package/src/config/model-registry.ts +85 -39
  8. package/src/config/model-resolver.ts +47 -21
  9. package/src/config/settings-schema.ts +56 -2
  10. package/src/discovery/helpers.ts +2 -2
  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 +31 -2
  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/thinking-selector.ts +7 -7
  24. package/src/modes/components/tree-selector.ts +3 -2
  25. package/src/modes/controllers/command-controller.ts +11 -26
  26. package/src/modes/controllers/event-controller.ts +16 -3
  27. package/src/modes/controllers/input-controller.ts +4 -2
  28. package/src/modes/controllers/selector-controller.ts +5 -4
  29. package/src/modes/interactive-mode.ts +2 -2
  30. package/src/modes/rpc/rpc-client.ts +5 -10
  31. package/src/modes/rpc/rpc-types.ts +5 -5
  32. package/src/modes/theme/theme.ts +8 -3
  33. package/src/priority.json +1 -0
  34. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  35. package/src/prompts/system/system-prompt.md +18 -2
  36. package/src/prompts/tools/hashline.md +139 -83
  37. package/src/sdk.ts +22 -14
  38. package/src/session/agent-session.ts +259 -117
  39. package/src/session/agent-storage.ts +14 -14
  40. package/src/session/compaction/compaction.ts +500 -13
  41. package/src/session/messages.ts +12 -1
  42. package/src/session/session-manager.ts +77 -19
  43. package/src/slash-commands/builtin-registry.ts +48 -0
  44. package/src/task/agents.ts +3 -2
  45. package/src/task/executor.ts +2 -2
  46. package/src/task/types.ts +2 -1
  47. package/src/thinking.ts +87 -0
  48. package/src/tools/browser.ts +15 -6
  49. package/src/tools/fetch.ts +118 -100
  50. package/src/web/search/providers/exa.ts +74 -3
@@ -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
  },
@@ -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();
@@ -3,7 +3,7 @@
3
3
  * Handles TUI rendering and user interaction, delegating business logic to AgentSession.
4
4
  */
5
5
  import * as path from "node:path";
6
- import type { Agent, AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
8
8
  import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
9
9
  import {
@@ -423,7 +423,7 @@ export class InteractiveMode implements InteractiveModeContext {
423
423
  } else if (this.isPythonMode) {
424
424
  this.editor.borderColor = theme.getPythonModeBorderColor();
425
425
  } else {
426
- const level = this.session.thinkingLevel || "off";
426
+ const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
427
427
  this.editor.borderColor = theme.getThinkingBorderColor(level);
428
428
  }
429
429
  this.updateEditorTopBorder();
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
7
- import type { ImageContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
6
+ import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
8
8
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
@@ -34,12 +34,7 @@ export interface RpcClientOptions {
34
34
  args?: string[];
35
35
  }
36
36
 
37
- export interface ModelInfo {
38
- provider: string;
39
- id: string;
40
- contextWindow: number;
41
- reasoning: boolean;
42
- }
37
+ export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
43
38
 
44
39
  export type RpcEventListener = (event: AgentEvent) => void;
45
40
 
@@ -284,7 +279,7 @@ export class RpcClient {
284
279
  */
285
280
  async cycleModel(): Promise<{
286
281
  model: { provider: string; id: string };
287
- thinkingLevel: ThinkingLevel;
282
+ thinkingLevel: ThinkingLevel | undefined;
288
283
  isScoped: boolean;
289
284
  } | null> {
290
285
  const response = await this.#send({ type: "cycle_model" });
@@ -309,7 +304,7 @@ export class RpcClient {
309
304
  /**
310
305
  * Cycle thinking level.
311
306
  */
312
- async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {
307
+ async cycleThinkingLevel(): Promise<{ level: Effort } | null> {
313
308
  const response = await this.#send({ type: "cycle_thinking_level" });
314
309
  return this.#getData(response);
315
310
  }
@@ -4,8 +4,8 @@
4
4
  * Commands are sent as JSON lines on stdin.
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
- import type { ImageContent, Model, ThinkingLevel } from "@oh-my-pi/pi-ai";
7
+ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
@@ -70,7 +70,7 @@ export type RpcCommand =
70
70
 
71
71
  export interface RpcSessionState {
72
72
  model?: Model;
73
- thinkingLevel: ThinkingLevel;
73
+ thinkingLevel: ThinkingLevel | undefined;
74
74
  isStreaming: boolean;
75
75
  isCompacting: boolean;
76
76
  steeringMode: "all" | "one-at-a-time";
@@ -114,7 +114,7 @@ export type RpcResponse =
114
114
  type: "response";
115
115
  command: "cycle_model";
116
116
  success: true;
117
- data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
117
+ data: { model: Model; thinkingLevel: ThinkingLevel | undefined; isScoped: boolean } | null;
118
118
  }
119
119
  | {
120
120
  id?: string;
@@ -131,7 +131,7 @@ export type RpcResponse =
131
131
  type: "response";
132
132
  command: "cycle_thinking_level";
133
133
  success: true;
134
- data: { level: ThinkingLevel } | null;
134
+ data: { level: Effort } | null;
135
135
  }
136
136
 
137
137
  // Queue modes
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
3
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
+ import type { Effort } from "@oh-my-pi/pi-ai";
4
5
  import {
5
6
  detectMacOSAppearance,
6
7
  type HighlightColors as NativeHighlightColors,
@@ -108,6 +109,7 @@ export type SymbolKey =
108
109
  | "icon.warning"
109
110
  | "icon.rewind"
110
111
  | "icon.auto"
112
+ | "icon.fast"
111
113
  | "icon.extensionSkill"
112
114
  | "icon.extensionTool"
113
115
  | "icon.extensionSlashCommand"
@@ -268,6 +270,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
268
270
  "icon.warning": "⚠",
269
271
  "icon.rewind": "↶",
270
272
  "icon.auto": "⟲",
273
+ "icon.fast": "⚡",
271
274
  "icon.extensionSkill": "✦",
272
275
  "icon.extensionTool": "🛠",
273
276
  "icon.extensionSlashCommand": "⌘",
@@ -499,7 +502,7 @@ const NERD_SYMBOLS: SymbolMap = {
499
502
  "icon.rewind": "\uf0e2",
500
503
  // pick: 󰁨 | alt:   
501
504
  "icon.auto": "\u{f0068}",
502
- // pick:  | alt:  
505
+ "icon.fast": "\uf0e7",
503
506
  "icon.extensionSkill": "\uf0eb",
504
507
  // pick:  | alt:  
505
508
  "icon.extensionTool": "\uf0ad",
@@ -680,6 +683,7 @@ const ASCII_SYMBOLS: SymbolMap = {
680
683
  "icon.warning": "[!]",
681
684
  "icon.rewind": "<-",
682
685
  "icon.auto": "[A]",
686
+ "icon.fast": ">>",
683
687
  "icon.extensionSkill": "SK",
684
688
  "icon.extensionTool": "TL",
685
689
  "icon.extensionSlashCommand": "/",
@@ -1220,7 +1224,7 @@ export class Theme {
1220
1224
  return this.mode;
1221
1225
  }
1222
1226
 
1223
- getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
1227
+ getThinkingBorderColor(level: ThinkingLevel | Effort): (str: string) => string {
1224
1228
  // Map thinking levels to dedicated theme colors
1225
1229
  switch (level) {
1226
1230
  case "off":
@@ -1381,6 +1385,7 @@ export class Theme {
1381
1385
  warning: this.#symbols["icon.warning"],
1382
1386
  rewind: this.#symbols["icon.rewind"],
1383
1387
  auto: this.#symbols["icon.auto"],
1388
+ fast: this.#symbols["icon.fast"],
1384
1389
  extensionSkill: this.#symbols["icon.extensionSkill"],
1385
1390
  extensionTool: this.#symbols["icon.extensionTool"],
1386
1391
  extensionSlashCommand: this.#symbols["icon.extensionSlashCommand"],
package/src/priority.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "mini"
11
11
  ],
12
12
  "slow": [
13
+ "gpt-5.4",
13
14
  "gpt-5.3-codex",
14
15
  "gpt-5.3",
15
16
  "gpt-5.2-codex",
@@ -0,0 +1 @@
1
+ Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.
@@ -186,7 +186,7 @@ Use the Task tool unless the change is:
186
186
  - A direct answer or explanation with no code changes
187
187
  - A command the user asked you to run yourself
188
188
 
189
- For everything else — multi-file changes, refactors, new features, test additions, investigations — break the work into tasks and delegate. Err on the side of delegating. You are an orchestrator first, a coder second.
189
+ For everything else — multi-file changes, refactors, new features, test additions, investigations — break the work into tasks and delegate once the target design is settled. Err on the side of delegating after the architectural direction is fixed.
190
190
  </eager-tasks>
191
191
  {{/if}}
192
192
 
@@ -218,6 +218,18 @@ These are inviolable. Violation is system failure.
218
218
  6. You **MUST NOT** ask for information obtainable from tools, repo context, or files. File referenced → you **MUST** locate and read it. Path implied → you **MUST** resolve it.
219
219
  7. Full CUTOVER is **REQUIRED**. You **MUST** replace old usage everywhere you touch — no backwards-compat shims, no gradual migration, no "keeping both for now." The old way is dead; lingering instances **MUST** be treated as bugs.
220
220
 
221
+ # Design Integrity
222
+ - You **MUST** prefer a coherent final design over a minimally invasive patch.
223
+ - You **MUST NOT** preserve obsolete abstractions to reduce edit scope.
224
+ - Temporary bridges are **PROHIBITED** unless the user explicitly asks for a migration path.
225
+ - If a refactor introduces a new canonical abstraction, you **MUST** migrate consumers to it instead of wrapping it in compatibility helpers.
226
+ - Parallel APIs that express the same concept are a bug, not a convenience.
227
+ - Boolean compatibility helpers that collapse richer capability models are **PROHIBITED**.
228
+ - You **MUST NOT** collapse structured capability data into lossy booleans or convenience wrappers unless the domain is truly boolean.
229
+ - If a change removes a field, type, or API, all fixtures, tests, docs, and callsites using it **MUST** be updated in the same change.
230
+ - You **MUST** optimize for the next maintainer's edit, not for minimizing the current diff.
231
+ - "Works" is insufficient. The result **MUST** also be singular, obvious, and maintainable.
232
+
221
233
  # Procedure
222
234
  ## 1. Scope
223
235
  {{#if skills.length}}- If a skill matches the domain, you **MUST** read it before starting.{{/if}}
@@ -245,6 +257,8 @@ Justify sequential work; default parallel. Cannot articulate why B depends on A
245
257
  - You **MUST** write idiomatic, simple, maintainable code. Complexity **MUST** earn its place.
246
258
  - You **MUST** fix in the place the bug lives. You **MUST NOT** bandaid the problem within the caller.
247
259
  - You **MUST** clean up unused code ruthlessly: dead parameters, unused helpers, orphaned types. You **MUST** delete them and update callers. Resulting code **MUST** be pristine.
260
+ - For every new abstraction, you **MUST** identify what becomes redundant: old helpers, fallback branches, compatibility adapters, duplicate tests, stale fixtures, and docs that describe removed behavior.
261
+ - You **MUST** delete or rewrite redundant code in the same change. Leaving obsolete code reachable, compilable, or tested is a failure of cutover.
248
262
  - You **MUST NOT** leave breadcrumbs. When you delete or move code, you **MUST** remove it cleanly — no `// moved to X` comments, no `// relocated` markers, no re-exports from the old location. The old location **MUST** be removed without trace.
249
263
  - You **MUST** fix from first principles. You **MUST NOT** apply bandaids. The root cause **MUST** be found and fixed at its source. A symptom suppressed is a bug deferred.
250
264
  - When a tool call fails or returns unexpected output, you **MUST** read the full error and diagnose it.
@@ -297,8 +311,10 @@ Today is '{{date}}', and your work begins now. Get it right.
297
311
 
298
312
  <critical>
299
313
  - You **MUST** use the most specialized tool, **NEVER** `cat` if there's tool.bash, `rg/grep`:tool.grep, `find`:tool.find, `sed`:tool.edit…
300
- - Every turn **MUST** advance the deliverable. A non-final turn without at least one side-effect is **PROHIBITED**.
314
+ - Every turn **MUST** materially advance the deliverable.
301
315
  - You **MUST** default to action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
316
+ - You **MUST NOT** make speculative edits before understanding the surrounding design.
317
+ - You **MUST** default to informed action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
302
318
  - You **MUST NOT** ask when the answer may be obtained from available tools or repo context/files.
303
319
  - You **MUST** verify the effect. When a task involves a behavioral change, you **MUST** confirm the change is observable before yielding: run the specific test, command, or scenario that covers your change.
304
320
  </critical>