@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.
- package/CHANGELOG.md +64 -0
- package/examples/sdk/02-custom-model.ts +2 -1
- package/package.json +7 -7
- package/src/cli/args.ts +10 -6
- package/src/cli/list-models.ts +2 -2
- package/src/commands/launch.ts +3 -3
- package/src/config/model-registry.ts +136 -38
- package/src/config/model-resolver.ts +47 -21
- package/src/config/settings-schema.ts +56 -2
- package/src/discovery/helpers.ts +3 -3
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/loader.ts +3 -2
- package/src/extensibility/extensions/types.ts +10 -7
- package/src/extensibility/hooks/types.ts +2 -0
- package/src/main.ts +5 -22
- package/src/memories/index.ts +7 -3
- package/src/modes/components/footer.ts +10 -8
- package/src/modes/components/model-selector.ts +33 -38
- package/src/modes/components/settings-defs.ts +32 -3
- package/src/modes/components/settings-selector.ts +16 -5
- package/src/modes/components/status-line/context-thresholds.ts +68 -0
- package/src/modes/components/status-line/segments.ts +11 -12
- package/src/modes/components/status-line.ts +2 -6
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/tree-selector.ts +3 -2
- package/src/modes/controllers/command-controller.ts +11 -26
- package/src/modes/controllers/event-controller.ts +16 -3
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/selector-controller.ts +5 -4
- package/src/modes/interactive-mode.ts +2 -2
- package/src/modes/rpc/rpc-client.ts +5 -10
- package/src/modes/rpc/rpc-types.ts +5 -5
- package/src/modes/theme/theme.ts +8 -3
- package/src/priority.json +1 -0
- package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
- package/src/prompts/system/system-prompt.md +18 -2
- package/src/prompts/tools/hashline.md +139 -83
- package/src/sdk.ts +24 -16
- package/src/session/agent-session.ts +261 -118
- package/src/session/agent-storage.ts +14 -14
- package/src/session/compaction/compaction.ts +500 -13
- package/src/session/messages.ts +12 -1
- package/src/session/session-manager.ts +77 -19
- package/src/slash-commands/builtin-registry.ts +48 -0
- package/src/task/agents.ts +3 -2
- package/src/task/executor.ts +2 -2
- package/src/task/types.ts +2 -1
- package/src/thinking.ts +87 -0
- package/src/tools/browser.ts +15 -6
- package/src/tools/fetch.ts +118 -100
- package/src/tools/index.ts +2 -1
- package/src/web/kagi.ts +62 -7
- package/src/web/search/providers/exa.ts +74 -3
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
|
27
|
+
thinkingLevel?: string;
|
|
33
28
|
}
|
|
34
29
|
|
|
35
30
|
interface RoleAssignment {
|
|
36
31
|
model: Model;
|
|
37
|
-
|
|
32
|
+
thinkingLevel: ThinkingLevel;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
type RoleSelectCallback = (model: Model, role: ModelRole | null,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
460
|
-
return [
|
|
455
|
+
#getThinkingLevelsForModel(model: Model): ReadonlyArray<ThinkingLevel> {
|
|
456
|
+
return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
|
|
461
457
|
}
|
|
462
458
|
|
|
463
|
-
#
|
|
464
|
-
return this.#roles[role]?.
|
|
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.#
|
|
469
|
-
const
|
|
470
|
-
const
|
|
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.#
|
|
494
|
+
const thinkingOptions = showingThinking ? this.#getThinkingLevelsForModel(selectedModel.model) : [];
|
|
500
495
|
const optionLines = showingThinking
|
|
501
|
-
? thinkingOptions.map((
|
|
496
|
+
? thinkingOptions.map((thinkingLevel, index) => {
|
|
502
497
|
const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
|
|
503
|
-
const 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.#
|
|
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.#
|
|
639
|
-
const
|
|
640
|
-
if (!
|
|
641
|
-
this.#handleSelect(selectedModel.model, this.#menuSelectedRole,
|
|
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,
|
|
655
|
+
#formatRoleModelValue(model: Model, thinkingLevel: ThinkingLevel): string {
|
|
661
656
|
const modelKey = `${model.provider}/${model.id}`;
|
|
662
|
-
if (
|
|
663
|
-
return `${modelKey}:${
|
|
657
|
+
if (thinkingLevel === ThinkingLevel.Inherit) return modelKey;
|
|
658
|
+
return `${modelKey}:${thinkingLevel}`;
|
|
664
659
|
}
|
|
665
|
-
#handleSelect(model: Model, role: ModelRole | null,
|
|
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
|
|
667
|
+
const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
|
673
668
|
|
|
674
669
|
// Save to settings
|
|
675
|
-
this.#settings.setModelRole(role, this.#formatRoleModelValue(model,
|
|
670
|
+
this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingLevel));
|
|
676
671
|
|
|
677
672
|
// Update local state for UI
|
|
678
|
-
this.#roles[role] = { model,
|
|
673
|
+
this.#roles[role] = { model, thinkingLevel: selectedThinkingLevel };
|
|
679
674
|
|
|
680
675
|
// Notify caller (for updating agent state if needed)
|
|
681
|
-
this.#onSelectCallback(model, role,
|
|
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 {
|
|
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: [...
|
|
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-
|
|
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:
|
|
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:
|
|
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 (
|
|
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?.
|
|
49
|
-
const level = state.thinkingLevel
|
|
50
|
-
if (level !==
|
|
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
|
-
|
|
248
|
-
|
|
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 {
|
|
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:
|
|
15
|
-
availableLevels:
|
|
16
|
-
onSelect: (level:
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
1025
|
-
.map(limit => limit.window?.
|
|
1026
|
-
.filter((value): value is number => value !== undefined && Number.isFinite(value) && value >
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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}
|
|
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(
|
|
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-
|
|
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
|
|
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.
|
|
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 {
|
|
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,
|
|
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 (
|
|
397
|
-
this.ctx.session.setThinkingLevel(
|
|
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();
|