@oh-my-pi/pi-coding-agent 16.0.3 → 16.0.5
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 +49 -0
- package/dist/cli.js +697 -337
- package/dist/types/advisor/advise-tool.d.ts +9 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +6 -0
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +92 -3
- package/dist/types/edit/file-snapshot-store.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +5 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -7
- package/dist/types/extensibility/shared-events.d.ts +22 -1
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +1 -1
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +0 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/session-persistence.d.ts +4 -0
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tui/code-cell.d.ts +2 -0
- package/dist/types/utils/image-vision-fallback.d.ts +28 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +59 -0
- package/src/advisor/advise-tool.ts +13 -0
- package/src/cli/args.ts +4 -0
- package/src/cli/bench-cli.ts +30 -7
- package/src/cli/flag-tables.ts +9 -0
- package/src/collab/host.ts +2 -2
- package/src/commands/launch.ts +6 -0
- package/src/config/settings-schema.ts +85 -3
- package/src/edit/file-snapshot-store.ts +12 -3
- package/src/eval/py/runner.py +44 -0
- package/src/extensibility/extensions/runner.ts +20 -2
- package/src/extensibility/extensions/types.ts +16 -5
- package/src/extensibility/shared-events.ts +24 -0
- package/src/internal-urls/docs-index.generated.ts +81 -81
- package/src/main.ts +18 -9
- package/src/modes/components/branch-summary-message.ts +1 -0
- package/src/modes/components/collab-prompt-message.ts +9 -7
- package/src/modes/components/compaction-summary-message.ts +1 -0
- package/src/modes/components/custom-message.ts +1 -0
- package/src/modes/components/footer.ts +6 -5
- package/src/modes/components/hook-message.ts +1 -0
- package/src/modes/components/read-tool-group.ts +9 -3
- package/src/modes/components/skill-message.ts +1 -0
- package/src/modes/components/status-line/component.ts +131 -14
- package/src/modes/components/status-line/context-thresholds.ts +0 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/todo-reminder.ts +1 -0
- package/src/modes/components/ttsr-notification.ts +1 -0
- package/src/modes/components/user-message.ts +6 -6
- package/src/modes/controllers/event-controller.ts +2 -7
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/utils/context-usage.ts +28 -15
- package/src/prompts/system/system-prompt.md +2 -0
- package/src/prompts/tools/image-attachment-describe-system.md +8 -0
- package/src/prompts/tools/image-attachment-describe.md +10 -0
- package/src/sdk.ts +14 -18
- package/src/session/agent-session.ts +571 -235
- package/src/session/session-loader.ts +19 -32
- package/src/session/session-persistence.ts +27 -11
- package/src/ssh/connection-manager.ts +3 -2
- package/src/task/executor.ts +1 -1
- package/src/tools/image-gen.ts +67 -25
- package/src/tools/read.ts +54 -6
- package/src/tui/code-cell.ts +44 -3
- package/src/utils/image-vision-fallback.ts +197 -0
- package/src/web/search/index.ts +12 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +56 -18
package/src/main.ts
CHANGED
|
@@ -103,6 +103,10 @@ function maybeShowStartupSplash(options: {
|
|
|
103
103
|
//process.stdout.write(`${chalk.dim(`omp ${options.version}`)}\n${chalk.dim("Initializing session…")}\n`);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
export function writeStartupNotice(parsedArgs: Pick<Args, "mode">, text: string): void {
|
|
107
|
+
(parsedArgs.mode === "json" ? process.stderr : process.stdout).write(text);
|
|
108
|
+
}
|
|
109
|
+
|
|
106
110
|
async function checkForNewVersion(currentVersion: string): Promise<string | undefined> {
|
|
107
111
|
if (!settings.get("startup.checkUpdate")) {
|
|
108
112
|
return;
|
|
@@ -124,11 +128,9 @@ async function checkForNewVersion(currentVersion: string): Promise<string | unde
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
// Todo settings are caller-controlled in protocol modes. Do not host-default them:
|
|
132
|
+
// embedders need project-level opt-outs for reminder/prelude prompt injection.
|
|
127
133
|
const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
|
128
|
-
"todo.enabled",
|
|
129
|
-
"todo.reminders",
|
|
130
|
-
"todo.reminders.max",
|
|
131
|
-
"todo.eager",
|
|
132
134
|
"task.isolation.mode",
|
|
133
135
|
"task.isolation.merge",
|
|
134
136
|
"task.isolation.commits",
|
|
@@ -760,6 +762,9 @@ async function buildSessionOptions(
|
|
|
760
762
|
cwd: parsed.cwd ?? getProjectDir(),
|
|
761
763
|
autoApprove: parsed.autoApprove ?? false,
|
|
762
764
|
};
|
|
765
|
+
if (parsed.maxTime !== undefined) {
|
|
766
|
+
options.deadline = Date.now() + parsed.maxTime * 1000;
|
|
767
|
+
}
|
|
763
768
|
|
|
764
769
|
// Auto-discover SYSTEM.md if no CLI system prompt provided
|
|
765
770
|
const systemPromptSource = parsed.systemPrompt ?? discoverSystemPromptFile();
|
|
@@ -946,7 +951,7 @@ export async function runRootCommand(
|
|
|
946
951
|
const modelRegistry = logger.time("modelRegistry:init", () => new ModelRegistry(authStorage));
|
|
947
952
|
|
|
948
953
|
if (parsedArgs.version) {
|
|
949
|
-
|
|
954
|
+
writeStartupNotice(parsedArgs, `${VERSION}\n`);
|
|
950
955
|
process.exit(0);
|
|
951
956
|
}
|
|
952
957
|
|
|
@@ -961,7 +966,7 @@ export async function runRootCommand(
|
|
|
961
966
|
process.stderr.write(`${chalk.red(`Error: ${message}`)}\n`);
|
|
962
967
|
process.exit(1);
|
|
963
968
|
}
|
|
964
|
-
|
|
969
|
+
writeStartupNotice(parsedArgs, `Exported to: ${result}\n`);
|
|
965
970
|
process.exit(0);
|
|
966
971
|
}
|
|
967
972
|
|
|
@@ -1041,6 +1046,10 @@ export async function runRootCommand(
|
|
|
1041
1046
|
if (parsedArgs.hideThinking) {
|
|
1042
1047
|
settingsInstance.override("hideThinkingBlock", true);
|
|
1043
1048
|
}
|
|
1049
|
+
// Apply --advisor CLI flag (ephemeral, not persisted)
|
|
1050
|
+
if (parsedArgs.advisor) {
|
|
1051
|
+
settingsInstance.override("advisor.enabled", true);
|
|
1052
|
+
}
|
|
1044
1053
|
|
|
1045
1054
|
await logger.time(
|
|
1046
1055
|
"initTheme:final",
|
|
@@ -1093,7 +1102,7 @@ export async function runRootCommand(
|
|
|
1093
1102
|
// message rather than letting the decline bubble up as an uncaught exception
|
|
1094
1103
|
// (see issue #1668).
|
|
1095
1104
|
if (typeof parsedArgs.resume === "string" && !sessionManager) {
|
|
1096
|
-
|
|
1105
|
+
writeStartupNotice(parsedArgs, `${chalk.dim("Resume cancelled: session is in another project.")}\n`);
|
|
1097
1106
|
return;
|
|
1098
1107
|
}
|
|
1099
1108
|
|
|
@@ -1107,7 +1116,7 @@ export async function runRootCommand(
|
|
|
1107
1116
|
// picker can still open in all-projects scope instead of dead-ending.
|
|
1108
1117
|
preloadedAllSessions = await logger.time("SessionManager.listAll", SessionManager.listAll);
|
|
1109
1118
|
if (preloadedAllSessions.length === 0) {
|
|
1110
|
-
|
|
1119
|
+
writeStartupNotice(parsedArgs, `${chalk.dim("No sessions found")}\n`);
|
|
1111
1120
|
return;
|
|
1112
1121
|
}
|
|
1113
1122
|
startInAllScope = true;
|
|
@@ -1119,7 +1128,7 @@ export async function runRootCommand(
|
|
|
1119
1128
|
});
|
|
1120
1129
|
resumeStartupWatchdog();
|
|
1121
1130
|
if (!selected) {
|
|
1122
|
-
|
|
1131
|
+
writeStartupNotice(parsedArgs, `${chalk.dim("No session selected")}\n`);
|
|
1123
1132
|
return;
|
|
1124
1133
|
}
|
|
1125
1134
|
// Resuming a session from another project: switch the process into that
|
|
@@ -12,7 +12,9 @@ export class CollabPromptMessageComponent extends Container {
|
|
|
12
12
|
constructor(message: CustomMessage<CollabPromptDetails>) {
|
|
13
13
|
super();
|
|
14
14
|
const from = message.details?.from?.trim() || "guest";
|
|
15
|
-
|
|
15
|
+
const authorText = new Text(theme.fg("accent", `\x1b[1m«${from}»\x1b[22m ›`), 1, 0);
|
|
16
|
+
authorText.setIgnoreTight(true);
|
|
17
|
+
this.addChild(authorText);
|
|
16
18
|
const text =
|
|
17
19
|
typeof message.content === "string"
|
|
18
20
|
? message.content
|
|
@@ -20,11 +22,11 @@ export class CollabPromptMessageComponent extends Container {
|
|
|
20
22
|
.filter((content): content is TextContent => content.type === "text")
|
|
21
23
|
.map(content => content.text)
|
|
22
24
|
.join("");
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
);
|
|
25
|
+
const md = new Markdown(text, 1, 1, getMarkdownTheme(), {
|
|
26
|
+
bgColor: (value: string) => theme.bg("userMessageBg", value),
|
|
27
|
+
color: (value: string) => theme.fg("userMessageText", value),
|
|
28
|
+
});
|
|
29
|
+
md.setIgnoreTight(true);
|
|
30
|
+
this.addChild(md);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -62,6 +62,7 @@ class SummaryDividerComponent implements Component {
|
|
|
62
62
|
#detailBox(): Box {
|
|
63
63
|
if (this.#detail) return this.#detail;
|
|
64
64
|
const box = new Box(1, 1, t => theme.bg("customMessageBg", t));
|
|
65
|
+
box.setIgnoreTight(true);
|
|
65
66
|
box.addChild(
|
|
66
67
|
new Markdown(this.options.detailMarkdown(), 0, 0, getMarkdownTheme(), {
|
|
67
68
|
color: (text: string) => theme.fg("customMessageText", text),
|
|
@@ -4,6 +4,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
4
4
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
5
5
|
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { settings } from "../../config/settings";
|
|
7
8
|
import { theme } from "../../modes/theme/theme";
|
|
8
9
|
import type { AgentSession } from "../../session/agent-session";
|
|
9
10
|
import { shortenPath } from "../../tools/render-utils";
|
|
@@ -58,6 +59,8 @@ export class FooterComponent implements Component {
|
|
|
58
59
|
this.#gitWatcher = null;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
if (!settings.get("git.enabled")) return;
|
|
63
|
+
|
|
61
64
|
void git.head
|
|
62
65
|
.resolve(getProjectDir())
|
|
63
66
|
.then(head => {
|
|
@@ -102,6 +105,7 @@ export class FooterComponent implements Component {
|
|
|
102
105
|
* Returns null if not in a git repo, branch name otherwise.
|
|
103
106
|
*/
|
|
104
107
|
#getCurrentBranch(): string | null {
|
|
108
|
+
if (!settings.get("git.enabled")) return null;
|
|
105
109
|
if (this.#cachedBranch !== undefined) {
|
|
106
110
|
return this.#cachedBranch;
|
|
107
111
|
}
|
|
@@ -182,11 +186,8 @@ export class FooterComponent implements Component {
|
|
|
182
186
|
// Colorize context percentage based on usage
|
|
183
187
|
let contextPercentStr: string;
|
|
184
188
|
const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
|
|
185
|
-
const contextPercentDisplay = `${formatContextUsage(
|
|
186
|
-
|
|
187
|
-
contextWindow,
|
|
188
|
-
)}${autoIndicator}`;
|
|
189
|
-
if (contextUsage?.percent !== null && contextUsage?.percent !== undefined) {
|
|
189
|
+
const contextPercentDisplay = `${formatContextUsage(contextPercentValue, contextWindow)}${autoIndicator}`;
|
|
190
|
+
if (contextUsage) {
|
|
190
191
|
const color = getContextUsageThemeColor(getContextUsageLevel(contextPercentValue, contextWindow));
|
|
191
192
|
contextPercentStr =
|
|
192
193
|
color === "statusLineContext" ? contextPercentDisplay : theme.fg(color, contextPercentDisplay);
|
|
@@ -57,6 +57,7 @@ type ReadToolResultDetails = {
|
|
|
57
57
|
displayContent?: {
|
|
58
58
|
text?: string;
|
|
59
59
|
startLine?: number;
|
|
60
|
+
lineNumbers?: Array<number | null>;
|
|
60
61
|
};
|
|
61
62
|
meta?: {
|
|
62
63
|
source?: {
|
|
@@ -86,6 +87,8 @@ type ReadEntry = {
|
|
|
86
87
|
correctedFrom?: string;
|
|
87
88
|
contentText?: string;
|
|
88
89
|
conflictCount?: number;
|
|
90
|
+
codeStartLine?: number;
|
|
91
|
+
codeLineNumbers?: Array<number | null>;
|
|
89
92
|
};
|
|
90
93
|
|
|
91
94
|
/** Number of code lines to show in collapsed preview mode */
|
|
@@ -379,11 +382,12 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
379
382
|
entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
|
|
380
383
|
// Store clean display content for preview/expanded display when the read
|
|
381
384
|
// tool provides it; fall back to model-facing text for legacy results.
|
|
382
|
-
const displayContent =
|
|
383
|
-
typeof details?.displayContent?.text === "string" ? details.displayContent.text : undefined;
|
|
385
|
+
const displayContent = details?.displayContent;
|
|
384
386
|
const textContent = result.content?.find(c => c.type === "text")?.text;
|
|
385
387
|
if (displayContent !== undefined || textContent !== undefined) {
|
|
386
|
-
entry.contentText = displayContent ?? textContent;
|
|
388
|
+
entry.contentText = displayContent?.text ?? textContent;
|
|
389
|
+
entry.codeStartLine = displayContent?.startLine;
|
|
390
|
+
entry.codeLineNumbers = displayContent?.lineNumbers;
|
|
387
391
|
}
|
|
388
392
|
this.#updateDisplay();
|
|
389
393
|
}
|
|
@@ -636,6 +640,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
636
640
|
status: entry.status === "success" ? "complete" : entry.status,
|
|
637
641
|
expanded,
|
|
638
642
|
codeMaxLines: expanded ? undefined : COLLAPSED_PREVIEW_LINES,
|
|
643
|
+
codeStartLine: entry.codeStartLine,
|
|
644
|
+
codeLineNumbers: entry.codeLineNumbers,
|
|
639
645
|
width,
|
|
640
646
|
},
|
|
641
647
|
theme,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
4
|
+
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
4
5
|
import { type Component, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { getProjectDir } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import { $ } from "bun";
|
|
@@ -55,21 +56,70 @@ function messageFingerprint(msg: AgentMessage): string {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
} else if (role === "assistant") {
|
|
59
|
+
const assistantMsg = msg as AssistantMessage;
|
|
60
|
+
const usageExt = assistantMsg.usage as unknown as { promptTokensDetails?: unknown };
|
|
61
|
+
const usageTotal = assistantMsg.usage?.totalTokens ?? 0;
|
|
62
|
+
const promptBuckets = usageExt?.promptTokensDetails ? 1 : 0;
|
|
63
|
+
const stopReason = assistantMsg.stopReason ?? "";
|
|
64
|
+
|
|
65
|
+
let signatureLen = 0;
|
|
66
|
+
let redactedLen = 0;
|
|
67
|
+
const msgExt = assistantMsg as unknown as {
|
|
68
|
+
thinkingSignature?: string;
|
|
69
|
+
textSignature?: string;
|
|
70
|
+
thoughtSignature?: string;
|
|
71
|
+
redactedThinking?: { data?: string };
|
|
72
|
+
};
|
|
73
|
+
const thinkingSignature = msgExt.thinkingSignature;
|
|
74
|
+
if (typeof thinkingSignature === "string") {
|
|
75
|
+
signatureLen += thinkingSignature.length;
|
|
76
|
+
}
|
|
77
|
+
const textSignature = msgExt.textSignature;
|
|
78
|
+
if (typeof textSignature === "string") {
|
|
79
|
+
signatureLen += textSignature.length;
|
|
80
|
+
}
|
|
81
|
+
const thoughtSignature = msgExt.thoughtSignature;
|
|
82
|
+
if (typeof thoughtSignature === "string") {
|
|
83
|
+
signatureLen += thoughtSignature.length;
|
|
84
|
+
}
|
|
85
|
+
const redactedData = msgExt.redactedThinking?.data;
|
|
86
|
+
if (typeof redactedData === "string") {
|
|
87
|
+
redactedLen += redactedData.length;
|
|
88
|
+
}
|
|
89
|
+
|
|
58
90
|
const content = (msg as { content?: unknown }).content;
|
|
59
91
|
if (Array.isArray(content)) {
|
|
60
92
|
blocks = content.length;
|
|
61
93
|
for (const block of content) {
|
|
62
94
|
if (!block || typeof block !== "object") continue;
|
|
63
|
-
const b = block as {
|
|
95
|
+
const b = block as {
|
|
96
|
+
type?: string;
|
|
97
|
+
text?: string;
|
|
98
|
+
thinking?: string;
|
|
99
|
+
thinkingSignature?: string;
|
|
100
|
+
signature?: string;
|
|
101
|
+
textSignature?: string;
|
|
102
|
+
thoughtSignature?: string;
|
|
103
|
+
data?: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
arguments?: unknown;
|
|
106
|
+
};
|
|
64
107
|
if (b.type === "text" && typeof b.text === "string") textLen += b.text.length;
|
|
65
|
-
else if (b.type === "thinking"
|
|
66
|
-
|
|
108
|
+
else if (b.type === "thinking") {
|
|
109
|
+
if (typeof b.thinking === "string") textLen += b.thinking.length;
|
|
110
|
+
if (typeof b.thinkingSignature === "string") signatureLen += b.thinkingSignature.length;
|
|
111
|
+
if (typeof b.signature === "string") signatureLen += b.signature.length;
|
|
112
|
+
if (typeof b.textSignature === "string") signatureLen += b.textSignature.length;
|
|
113
|
+
if (typeof b.thoughtSignature === "string") signatureLen += b.thoughtSignature.length;
|
|
114
|
+
} else if (b.type === "redactedThinking" && typeof b.data === "string") {
|
|
115
|
+
redactedLen += b.data.length;
|
|
116
|
+
} else if (b.type === "toolCall") {
|
|
67
117
|
if (typeof b.name === "string") textLen += b.name.length;
|
|
68
|
-
// Argument bytes vary; a length proxy is enough to detect in-place edits.
|
|
69
118
|
textLen += b.arguments === undefined ? 0 : JSON.stringify(b.arguments).length;
|
|
70
119
|
}
|
|
71
120
|
}
|
|
72
121
|
}
|
|
122
|
+
return `${role}:${ts}:${textLen}:${blocks}:${images}:${signatureLen}:${redactedLen}:${usageTotal}:${promptBuckets}:${stopReason}`;
|
|
73
123
|
} else if (role === "toolResult" || role === "hookMessage") {
|
|
74
124
|
const content = (msg as { content?: unknown }).content;
|
|
75
125
|
if (typeof content === "string") {
|
|
@@ -95,8 +145,12 @@ interface ContextUsageMemo {
|
|
|
95
145
|
length: number;
|
|
96
146
|
lastFingerprint: string | undefined;
|
|
97
147
|
modelContextWindow: number;
|
|
98
|
-
|
|
148
|
+
contextUsageRevision: number;
|
|
149
|
+
usedTokens: number;
|
|
99
150
|
contextWindow: number;
|
|
151
|
+
systemPromptRef: readonly string[] | undefined;
|
|
152
|
+
toolsRef: readonly any[] | undefined;
|
|
153
|
+
skillsRef: readonly any[] | undefined;
|
|
100
154
|
}
|
|
101
155
|
|
|
102
156
|
const EMPTY_MESSAGES: readonly AgentMessage[] = [];
|
|
@@ -104,6 +158,16 @@ const EMPTY_MESSAGES: readonly AgentMessage[] = [];
|
|
|
104
158
|
function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
105
159
|
return segments.includes("context_pct") || segments.includes("context_total");
|
|
106
160
|
}
|
|
161
|
+
function hasGitSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
162
|
+
return segments.includes("git");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hasPrSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
166
|
+
return segments.includes("pr");
|
|
167
|
+
}
|
|
168
|
+
function hasGitBackedSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
|
169
|
+
return hasGitSegment(segments) || hasPrSegment(segments);
|
|
170
|
+
}
|
|
107
171
|
|
|
108
172
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
109
173
|
// StatusLineComponent
|
|
@@ -166,6 +230,15 @@ export class StatusLineComponent implements Component {
|
|
|
166
230
|
transparent: settings.get("statusLine.transparent"),
|
|
167
231
|
};
|
|
168
232
|
}
|
|
233
|
+
#gitEnabled(): boolean {
|
|
234
|
+
return settings.get("git.enabled");
|
|
235
|
+
}
|
|
236
|
+
#hasGitBackedSegment(): boolean {
|
|
237
|
+
const effectiveSettings = this.#resolveSettings();
|
|
238
|
+
return (
|
|
239
|
+
hasGitBackedSegment(effectiveSettings.leftSegments) || hasGitBackedSegment(effectiveSettings.rightSegments)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
169
242
|
|
|
170
243
|
/**
|
|
171
244
|
* Re-point the status line at another session (focus proxy). Invalidate: model/context/usage all derive
|
|
@@ -183,6 +256,7 @@ export class StatusLineComponent implements Component {
|
|
|
183
256
|
updateSettings(settings: StatusLineSettings): void {
|
|
184
257
|
this.#settings = settings;
|
|
185
258
|
this.#effectiveSettings = undefined;
|
|
259
|
+
if (this.#onBranchChange) this.#setupGitWatcher();
|
|
186
260
|
}
|
|
187
261
|
|
|
188
262
|
getEffectiveSettingsForTest(): EffectiveStatusLineSettings {
|
|
@@ -241,6 +315,11 @@ export class StatusLineComponent implements Component {
|
|
|
241
315
|
this.#gitWatcher = null;
|
|
242
316
|
}
|
|
243
317
|
|
|
318
|
+
if (!this.#gitEnabled() || !this.#hasGitBackedSegment()) {
|
|
319
|
+
this.#invalidateGitCaches();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
244
323
|
const repository = git.repo.resolveSync(getProjectDir());
|
|
245
324
|
if (!repository) return;
|
|
246
325
|
|
|
@@ -286,6 +365,8 @@ export class StatusLineComponent implements Component {
|
|
|
286
365
|
this.#cachedPrContext = undefined;
|
|
287
366
|
}
|
|
288
367
|
#getCurrentBranch(): string | null {
|
|
368
|
+
if (!this.#gitEnabled()) return null;
|
|
369
|
+
|
|
289
370
|
const cwd = getProjectDir();
|
|
290
371
|
if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === cwd) {
|
|
291
372
|
return this.#cachedBranch;
|
|
@@ -322,6 +403,7 @@ export class StatusLineComponent implements Component {
|
|
|
322
403
|
}
|
|
323
404
|
|
|
324
405
|
#getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
|
406
|
+
if (!this.#gitEnabled()) return null;
|
|
325
407
|
if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
|
|
326
408
|
return this.#cachedGitStatus;
|
|
327
409
|
}
|
|
@@ -343,6 +425,8 @@ export class StatusLineComponent implements Component {
|
|
|
343
425
|
}
|
|
344
426
|
|
|
345
427
|
#lookupPr(): { number: number; url: string } | null {
|
|
428
|
+
if (!this.#gitEnabled()) return null;
|
|
429
|
+
|
|
346
430
|
const branch = this.#getCurrentBranch();
|
|
347
431
|
const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
|
|
348
432
|
|
|
@@ -515,11 +599,19 @@ export class StatusLineComponent implements Component {
|
|
|
515
599
|
* (right after compaction, before the next response). Exposed (non-private)
|
|
516
600
|
* for unit tests and the collab host's state broadcast.
|
|
517
601
|
*/
|
|
518
|
-
getCachedContextBreakdown(): { usedTokens: number
|
|
602
|
+
getCachedContextBreakdown(): { usedTokens: number; contextWindow: number } {
|
|
519
603
|
const messages = this.session.messages ?? EMPTY_MESSAGES;
|
|
520
604
|
const modelContextWindow = this.session.model?.contextWindow ?? 0;
|
|
521
605
|
const length = messages.length;
|
|
522
606
|
const lastFingerprint = length > 0 ? messageFingerprint(messages[length - 1]!) : undefined;
|
|
607
|
+
// Bumps when the in-flight pending snapshot is set/cleared. Without it a
|
|
608
|
+
// value computed mid-turn (estimate of the active tail) would survive after
|
|
609
|
+
// the turn ends/aborts, since clearing the snapshot touches no message.
|
|
610
|
+
const contextUsageRevision = this.session.contextUsageRevision ?? 0;
|
|
611
|
+
|
|
612
|
+
const systemPrompt = this.session.systemPrompt;
|
|
613
|
+
const tools = this.session.agent?.state?.tools;
|
|
614
|
+
const skills = this.session.skills;
|
|
523
615
|
|
|
524
616
|
const cache = this.#contextUsageCache;
|
|
525
617
|
if (
|
|
@@ -527,21 +619,29 @@ export class StatusLineComponent implements Component {
|
|
|
527
619
|
cache.messagesRef === messages &&
|
|
528
620
|
cache.length === length &&
|
|
529
621
|
cache.lastFingerprint === lastFingerprint &&
|
|
530
|
-
cache.modelContextWindow === modelContextWindow
|
|
622
|
+
cache.modelContextWindow === modelContextWindow &&
|
|
623
|
+
cache.contextUsageRevision === contextUsageRevision &&
|
|
624
|
+
cache.systemPromptRef === systemPrompt &&
|
|
625
|
+
cache.toolsRef === tools &&
|
|
626
|
+
cache.skillsRef === skills
|
|
531
627
|
) {
|
|
532
628
|
return { usedTokens: cache.usedTokens, contextWindow: cache.contextWindow };
|
|
533
629
|
}
|
|
534
630
|
|
|
535
631
|
const usage = this.session.getContextUsage();
|
|
536
|
-
const usedTokens = usage?.tokens ??
|
|
632
|
+
const usedTokens = usage?.tokens ?? 0;
|
|
537
633
|
const contextWindow = usage?.contextWindow ?? modelContextWindow;
|
|
538
634
|
this.#contextUsageCache = {
|
|
539
635
|
messagesRef: messages,
|
|
540
636
|
length,
|
|
541
637
|
lastFingerprint,
|
|
542
638
|
modelContextWindow,
|
|
639
|
+
contextUsageRevision,
|
|
543
640
|
usedTokens,
|
|
544
641
|
contextWindow,
|
|
642
|
+
systemPromptRef: systemPrompt,
|
|
643
|
+
toolsRef: tools,
|
|
644
|
+
skillsRef: skills,
|
|
545
645
|
};
|
|
546
646
|
return { usedTokens, contextWindow };
|
|
547
647
|
}
|
|
@@ -550,6 +650,8 @@ export class StatusLineComponent implements Component {
|
|
|
550
650
|
width: number,
|
|
551
651
|
segmentOptions: StatusLineSettings["segmentOptions"],
|
|
552
652
|
includeContext: boolean,
|
|
653
|
+
includeGit: boolean,
|
|
654
|
+
includePr: boolean,
|
|
553
655
|
): SegmentContext {
|
|
554
656
|
const state = this.session.state;
|
|
555
657
|
|
|
@@ -575,8 +677,7 @@ export class StatusLineComponent implements Component {
|
|
|
575
677
|
if (includeContext) {
|
|
576
678
|
const breakdown = this.getCachedContextBreakdown();
|
|
577
679
|
contextWindow = breakdown.contextWindow || contextWindow;
|
|
578
|
-
contextPercent =
|
|
579
|
-
breakdown.usedTokens === null ? null : contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
|
|
680
|
+
contextPercent = contextWindow > 0 ? (breakdown.usedTokens / contextWindow) * 100 : 0;
|
|
580
681
|
}
|
|
581
682
|
|
|
582
683
|
// Collab guest: context comes from the host's state frames — the local
|
|
@@ -587,6 +688,10 @@ export class StatusLineComponent implements Component {
|
|
|
587
688
|
contextPercent = collabState.contextUsage.percent ?? contextPercent;
|
|
588
689
|
}
|
|
589
690
|
|
|
691
|
+
const gitBranch = includeGit || includePr ? this.#getCurrentBranch() : null;
|
|
692
|
+
const gitStatus = includeGit ? this.#getGitStatus() : null;
|
|
693
|
+
const gitPr = includePr ? this.#lookupPr() : null;
|
|
694
|
+
|
|
590
695
|
return {
|
|
591
696
|
session: this.session,
|
|
592
697
|
focusedAgentId: this.#focusedAgentId,
|
|
@@ -603,9 +708,9 @@ export class StatusLineComponent implements Component {
|
|
|
603
708
|
subagentCount: this.#subagentCount,
|
|
604
709
|
sessionStartTime: this.#sessionStartTime,
|
|
605
710
|
git: {
|
|
606
|
-
branch:
|
|
607
|
-
status:
|
|
608
|
-
pr:
|
|
711
|
+
branch: gitBranch,
|
|
712
|
+
status: gitStatus,
|
|
713
|
+
pr: gitPr,
|
|
609
714
|
},
|
|
610
715
|
usage: this.#cachedUsage,
|
|
611
716
|
};
|
|
@@ -656,7 +761,19 @@ export class StatusLineComponent implements Component {
|
|
|
656
761
|
const effectiveSettings = this.#resolveSettings();
|
|
657
762
|
const includeContext =
|
|
658
763
|
hasContextSegment(effectiveSettings.leftSegments) || hasContextSegment(effectiveSettings.rightSegments);
|
|
659
|
-
const
|
|
764
|
+
const gitEnabled = this.#gitEnabled();
|
|
765
|
+
const includeGit =
|
|
766
|
+
gitEnabled &&
|
|
767
|
+
(hasGitSegment(effectiveSettings.leftSegments) || hasGitSegment(effectiveSettings.rightSegments));
|
|
768
|
+
const includePr =
|
|
769
|
+
gitEnabled && (hasPrSegment(effectiveSettings.leftSegments) || hasPrSegment(effectiveSettings.rightSegments));
|
|
770
|
+
const ctx = this.#buildSegmentContext(
|
|
771
|
+
width,
|
|
772
|
+
effectiveSettings.segmentOptions,
|
|
773
|
+
includeContext,
|
|
774
|
+
includeGit,
|
|
775
|
+
includePr,
|
|
776
|
+
);
|
|
660
777
|
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
|
661
778
|
|
|
662
779
|
// `transparent` reuses the empty-string sentinel (`\x1b[49m`) so the bar
|
|
@@ -58,7 +58,6 @@ export function getContextUsageLevel(contextPercent: number, contextWindow: numb
|
|
|
58
58
|
/**
|
|
59
59
|
* Format context usage as `<percent>%/<window>` (e.g. `5.1%/1M`), matching the
|
|
60
60
|
* status line's context gauge so subagent and footer renderers stay in sync.
|
|
61
|
-
* A `null`/`undefined` percent (unknown, e.g. right after compaction) renders as `?`.
|
|
62
61
|
*/
|
|
63
62
|
export function formatContextUsage(contextPercent: number | null | undefined, contextWindow: number): string {
|
|
64
63
|
const pct = contextPercent === null || contextPercent === undefined ? "?" : `${contextPercent.toFixed(1)}%`;
|
|
@@ -19,4 +19,5 @@ Press ctrl+r to search your prompt history and reuse a past message
|
|
|
19
19
|
`/shake` rips heavy tool results out of context to reclaim tokens without a full /compact — `/shake images` drops just images
|
|
20
20
|
Pair up live: `/collab` shares your session through an end-to-end encrypted relay link — a teammate runs `/join <link>` to watch tool calls stream and prompt the agent from their own omp
|
|
21
21
|
Press ← ← to drill into a running or finished agent and inspect its tool calls and transcript
|
|
22
|
-
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
22
|
+
Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
|
|
23
|
+
No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
|
|
@@ -25,6 +25,7 @@ export class TtsrNotificationComponent extends Container {
|
|
|
25
25
|
|
|
26
26
|
// Use inverse warning color for yellow background effect
|
|
27
27
|
this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
|
|
28
|
+
this.#box.setIgnoreTight(true);
|
|
28
29
|
this.addChild(this.#box);
|
|
29
30
|
|
|
30
31
|
this.#rebuild();
|
|
@@ -42,12 +42,12 @@ export class UserMessageComponent extends Container {
|
|
|
42
42
|
? imageReferenceHyperlink(label, index, imageLinks, imageLabel)
|
|
43
43
|
: theme.fg("accent", `\x1b[1m${label}\x1b[22m`),
|
|
44
44
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
45
|
+
const md = new Markdown(text, 1, 1, getMarkdownTheme(), {
|
|
46
|
+
bgColor,
|
|
47
|
+
color,
|
|
48
|
+
});
|
|
49
|
+
md.setIgnoreTight(true);
|
|
50
|
+
this.addChild(md);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
override render(width: number): readonly string[] {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import {
|
|
3
|
-
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
4
3
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
4
|
import { extractTextContent } from "../../commit/utils";
|
|
6
5
|
import { settings } from "../../config/settings";
|
|
@@ -1107,11 +1106,7 @@ export class EventController {
|
|
|
1107
1106
|
}
|
|
1108
1107
|
|
|
1109
1108
|
#currentContextTokens(): number {
|
|
1110
|
-
|
|
1111
|
-
.slice()
|
|
1112
|
-
.reverse()
|
|
1113
|
-
.find((m): m is AssistantMessage => m.role === "assistant" && m.stopReason !== "aborted");
|
|
1114
|
-
return lastAssistant?.usage ? calculatePromptTokens(lastAssistant.usage) : 0;
|
|
1109
|
+
return this.ctx.viewSession.getContextUsage()?.tokens ?? 0;
|
|
1115
1110
|
}
|
|
1116
1111
|
|
|
1117
1112
|
sendCompletionNotification(): void {
|
|
@@ -3,7 +3,7 @@ import { PASTE_CODE_LOGIN_PROVIDERS } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
4
4
|
import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types";
|
|
5
5
|
import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
|
|
6
|
-
import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { Input, Loader, Spacer, setTuiTight, Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getAgentDbPath, getProjectDir, normalizePathForComparison } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
9
9
|
import { getRoleInfo } from "../../config/model-roles";
|
|
@@ -67,7 +67,6 @@ import { TranscriptBlock } from "../components/transcript-container";
|
|
|
67
67
|
import { TreeSelectorComponent } from "../components/tree-selector";
|
|
68
68
|
import { UserMessageSelectorComponent } from "../components/user-message-selector";
|
|
69
69
|
import type { SessionObserverRegistry } from "../session-observer-registry";
|
|
70
|
-
import { computeContextBreakdown } from "../utils/context-usage";
|
|
71
70
|
import { buildCopyTargets } from "../utils/copy-targets";
|
|
72
71
|
|
|
73
72
|
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
|
|
@@ -325,6 +324,13 @@ export class SelectorController {
|
|
|
325
324
|
}
|
|
326
325
|
}
|
|
327
326
|
break;
|
|
327
|
+
case "tui.tight":
|
|
328
|
+
setTuiTight(value as boolean);
|
|
329
|
+
this.ctx.ui.invalidate();
|
|
330
|
+
this.ctx.updateEditorTopBorder();
|
|
331
|
+
this.ctx.ui.requestRender();
|
|
332
|
+
break;
|
|
333
|
+
|
|
328
334
|
case "theme": {
|
|
329
335
|
setTheme(value as string, true).then(result => {
|
|
330
336
|
this.ctx.statusLine.invalidate();
|
|
@@ -380,6 +386,7 @@ export class SelectorController {
|
|
|
380
386
|
this.ctx.session.agent.repetitionPenalty = repetitionPenalty >= 0 ? repetitionPenalty : undefined;
|
|
381
387
|
break;
|
|
382
388
|
}
|
|
389
|
+
case "git.enabled":
|
|
383
390
|
case "statusLinePreset":
|
|
384
391
|
case "statusLine.preset":
|
|
385
392
|
case "statusLineSeparator":
|
|
@@ -443,7 +450,7 @@ export class SelectorController {
|
|
|
443
450
|
}
|
|
444
451
|
|
|
445
452
|
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
446
|
-
const currentContextTokens =
|
|
453
|
+
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
|
|
447
454
|
this.showSelector(done => {
|
|
448
455
|
const selector = new ModelSelectorComponent(
|
|
449
456
|
this.ctx.ui,
|