oh-pi 0.1.1 → 0.1.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.
- package/dist/index.js +30 -0
- package/dist/tui/confirm-apply.js +1 -0
- package/dist/tui/provider-setup.js +60 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.js +21 -0
- package/dist/utils/install.js +13 -7
- package/package.json +1 -1
- package/pi-package/extensions/custom-footer.ts +110 -0
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
1
2
|
import { welcome } from "./tui/welcome.js";
|
|
2
3
|
import { selectMode } from "./tui/mode-select.js";
|
|
3
4
|
import { setupProviders } from "./tui/provider-setup.js";
|
|
@@ -49,6 +50,34 @@ async function customFlow(env) {
|
|
|
49
50
|
const keybindings = await selectKeybindings();
|
|
50
51
|
const extensions = await selectExtensions();
|
|
51
52
|
const agents = await selectAgents();
|
|
53
|
+
// Advanced: auto-compaction threshold
|
|
54
|
+
const wantAdvanced = await p.confirm({
|
|
55
|
+
message: "Configure advanced settings? (compaction threshold, etc.)",
|
|
56
|
+
initialValue: false,
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(wantAdvanced)) {
|
|
59
|
+
p.cancel("Cancelled.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
let compactThreshold = 0.75;
|
|
63
|
+
if (wantAdvanced) {
|
|
64
|
+
const threshold = await p.text({
|
|
65
|
+
message: "Auto-compact when context reaches % of window (0-100):",
|
|
66
|
+
placeholder: "75",
|
|
67
|
+
initialValue: "75",
|
|
68
|
+
validate: (v) => {
|
|
69
|
+
const n = Number(v);
|
|
70
|
+
if (isNaN(n) || n < 10 || n > 100)
|
|
71
|
+
return "Must be a number between 10 and 100";
|
|
72
|
+
return undefined;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (p.isCancel(threshold)) {
|
|
76
|
+
p.cancel("Cancelled.");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
compactThreshold = Number(threshold) / 100;
|
|
80
|
+
}
|
|
52
81
|
return {
|
|
53
82
|
providers,
|
|
54
83
|
theme,
|
|
@@ -58,5 +87,6 @@ async function customFlow(env) {
|
|
|
58
87
|
prompts: ["review", "fix", "explain", "commit", "test", "refactor", "optimize", "security", "document", "pr"],
|
|
59
88
|
agents,
|
|
60
89
|
thinking: "medium",
|
|
90
|
+
compactThreshold,
|
|
61
91
|
};
|
|
62
92
|
}
|
|
@@ -12,6 +12,7 @@ export async function confirmApply(config, env) {
|
|
|
12
12
|
`Theme: ${chalk.cyan(config.theme)}`,
|
|
13
13
|
`Keybindings: ${chalk.cyan(config.keybindings)}`,
|
|
14
14
|
`Thinking: ${chalk.cyan(config.thinking)}`,
|
|
15
|
+
`Compaction: ${chalk.cyan(`${Math.round((config.compactThreshold ?? 0.75) * 100)}% of context`)}`,
|
|
15
16
|
`Extensions: ${chalk.cyan(config.extensions.join(", ") || "none")}`,
|
|
16
17
|
`Skills: ${chalk.cyan(config.skills.join(", ") || "none")}`,
|
|
17
18
|
`Prompts: ${chalk.cyan(`${config.prompts.length} templates`)}`,
|
|
@@ -142,7 +142,66 @@ async function setupCustomProvider() {
|
|
|
142
142
|
defaultModel = model;
|
|
143
143
|
}
|
|
144
144
|
p.log.success(`${name} configured (${baseUrl})`);
|
|
145
|
-
|
|
145
|
+
// Model capabilities (optional)
|
|
146
|
+
const wantCaps = await p.confirm({
|
|
147
|
+
message: "Configure model capabilities? (context window, multimodal, reasoning)",
|
|
148
|
+
initialValue: false,
|
|
149
|
+
});
|
|
150
|
+
if (p.isCancel(wantCaps)) {
|
|
151
|
+
p.cancel("Cancelled.");
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
let contextWindow;
|
|
155
|
+
let maxTokens;
|
|
156
|
+
let reasoning;
|
|
157
|
+
let multimodal;
|
|
158
|
+
if (wantCaps) {
|
|
159
|
+
const ctxInput = await p.text({
|
|
160
|
+
message: "Context window size (tokens):",
|
|
161
|
+
placeholder: "128000",
|
|
162
|
+
initialValue: "128000",
|
|
163
|
+
validate: (v) => {
|
|
164
|
+
const n = Number(v);
|
|
165
|
+
if (isNaN(n) || n < 1024)
|
|
166
|
+
return "Must be a number ≥ 1024";
|
|
167
|
+
return undefined;
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
if (p.isCancel(ctxInput)) {
|
|
171
|
+
p.cancel("Cancelled.");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
contextWindow = Number(ctxInput);
|
|
175
|
+
const maxTokInput = await p.text({
|
|
176
|
+
message: "Max output tokens:",
|
|
177
|
+
placeholder: "8192",
|
|
178
|
+
initialValue: "8192",
|
|
179
|
+
validate: (v) => {
|
|
180
|
+
const n = Number(v);
|
|
181
|
+
if (isNaN(n) || n < 256)
|
|
182
|
+
return "Must be a number ≥ 256";
|
|
183
|
+
return undefined;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (p.isCancel(maxTokInput)) {
|
|
187
|
+
p.cancel("Cancelled.");
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
maxTokens = Number(maxTokInput);
|
|
191
|
+
const isMultimodal = await p.confirm({ message: "Supports image input (multimodal)?", initialValue: false });
|
|
192
|
+
if (p.isCancel(isMultimodal)) {
|
|
193
|
+
p.cancel("Cancelled.");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
multimodal = isMultimodal;
|
|
197
|
+
const isReasoning = await p.confirm({ message: "Supports extended thinking (reasoning)?", initialValue: false });
|
|
198
|
+
if (p.isCancel(isReasoning)) {
|
|
199
|
+
p.cancel("Cancelled.");
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
reasoning = isReasoning;
|
|
203
|
+
}
|
|
204
|
+
return { name, apiKey, defaultModel, baseUrl, contextWindow, maxTokens, reasoning, multimodal };
|
|
146
205
|
}
|
|
147
206
|
async function selectModel(label, staticModels, baseUrl, apiKey) {
|
|
148
207
|
let models = staticModels;
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface ProviderConfig {
|
|
|
3
3
|
apiKey: string;
|
|
4
4
|
defaultModel?: string;
|
|
5
5
|
baseUrl?: string;
|
|
6
|
+
contextWindow?: number;
|
|
7
|
+
maxTokens?: number;
|
|
8
|
+
reasoning?: boolean;
|
|
9
|
+
multimodal?: boolean;
|
|
6
10
|
}
|
|
7
11
|
export interface OhPConfig {
|
|
8
12
|
providers: ProviderConfig[];
|
|
@@ -13,7 +17,16 @@ export interface OhPConfig {
|
|
|
13
17
|
prompts: string[];
|
|
14
18
|
agents: string;
|
|
15
19
|
thinking: string;
|
|
20
|
+
compactThreshold?: number;
|
|
16
21
|
}
|
|
22
|
+
/** Official model capabilities for known providers */
|
|
23
|
+
export interface ModelCapabilities {
|
|
24
|
+
contextWindow: number;
|
|
25
|
+
maxTokens: number;
|
|
26
|
+
reasoning: boolean;
|
|
27
|
+
input: ("text" | "image")[];
|
|
28
|
+
}
|
|
29
|
+
export declare const MODEL_CAPABILITIES: Record<string, ModelCapabilities>;
|
|
17
30
|
export declare const PROVIDERS: Record<string, {
|
|
18
31
|
env: string;
|
|
19
32
|
label: string;
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
export const MODEL_CAPABILITIES = {
|
|
2
|
+
// Anthropic
|
|
3
|
+
"claude-sonnet-4-20250514": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
4
|
+
"claude-opus-4-0520": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
5
|
+
// OpenAI
|
|
6
|
+
"gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
7
|
+
"o3-mini": { contextWindow: 128000, maxTokens: 65536, reasoning: true, input: ["text"] },
|
|
8
|
+
// Google
|
|
9
|
+
"gemini-2.5-pro": { contextWindow: 1048576, maxTokens: 65536, reasoning: true, input: ["text", "image"] },
|
|
10
|
+
"gemini-2.5-flash": { contextWindow: 1048576, maxTokens: 65536, reasoning: true, input: ["text", "image"] },
|
|
11
|
+
// Groq
|
|
12
|
+
"llama-3.3-70b-versatile": { contextWindow: 128000, maxTokens: 32768, reasoning: false, input: ["text"] },
|
|
13
|
+
// OpenRouter
|
|
14
|
+
"anthropic/claude-sonnet-4": { contextWindow: 200000, maxTokens: 16384, reasoning: true, input: ["text", "image"] },
|
|
15
|
+
"openai/gpt-4o": { contextWindow: 128000, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
16
|
+
// xAI
|
|
17
|
+
"grok-3": { contextWindow: 131072, maxTokens: 16384, reasoning: false, input: ["text", "image"] },
|
|
18
|
+
// Mistral
|
|
19
|
+
"mistral-large-latest": { contextWindow: 128000, maxTokens: 8192, reasoning: false, input: ["text"] },
|
|
20
|
+
};
|
|
1
21
|
export const PROVIDERS = {
|
|
2
22
|
anthropic: { env: "ANTHROPIC_API_KEY", label: "Anthropic (Claude)", models: ["claude-sonnet-4-20250514", "claude-opus-4-0520"] },
|
|
3
23
|
openai: { env: "OPENAI_API_KEY", label: "OpenAI (GPT)", models: ["gpt-4o", "o3-mini"] },
|
|
@@ -21,6 +41,7 @@ export const EXTENSIONS = [
|
|
|
21
41
|
{ name: "safe-guard", label: "🛡️ Safe Guard — Dangerous command confirm + path protection", default: true },
|
|
22
42
|
{ name: "git-guard", label: "📦 Git Guard — Auto stash checkpoint + dirty repo warning + notify", default: true },
|
|
23
43
|
{ name: "auto-session-name", label: "📝 Auto Session Name — Name sessions from first message", default: true },
|
|
44
|
+
{ name: "custom-footer", label: "📊 Custom Footer — Enhanced status bar with tokens, cost, time, git, cwd", default: false },
|
|
24
45
|
{ name: "ant-colony", label: "🐜 Ant Colony — Autonomous multi-agent swarm with adaptive concurrency", default: false },
|
|
25
46
|
];
|
|
26
47
|
export const KEYBINDING_SCHEMES = {
|
package/dist/utils/install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
-
import { KEYBINDING_SCHEMES, PROVIDERS } from "../types.js";
|
|
6
|
+
import { KEYBINDING_SCHEMES, MODEL_CAPABILITIES, PROVIDERS } from "../types.js";
|
|
7
7
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
8
|
function ensureDir(dir) {
|
|
9
9
|
mkdirSync(dir, { recursive: true });
|
|
@@ -35,13 +35,18 @@ export function applyConfig(config) {
|
|
|
35
35
|
// 2. settings.json
|
|
36
36
|
const primary = config.providers[0];
|
|
37
37
|
const providerInfo = primary ? PROVIDERS[primary.name] : undefined;
|
|
38
|
+
const compactThreshold = config.compactThreshold ?? 0.75;
|
|
39
|
+
const primaryModel = primary?.defaultModel ?? providerInfo?.models[0];
|
|
40
|
+
const primaryCaps = primaryModel ? MODEL_CAPABILITIES[primaryModel] : undefined;
|
|
41
|
+
const contextWindow = primary?.contextWindow ?? primaryCaps?.contextWindow ?? 128000;
|
|
42
|
+
const reserveTokens = Math.round(contextWindow * (1 - compactThreshold));
|
|
38
43
|
const settings = {
|
|
39
44
|
defaultProvider: primary?.name,
|
|
40
|
-
defaultModel:
|
|
45
|
+
defaultModel: primaryModel,
|
|
41
46
|
defaultThinkingLevel: config.thinking,
|
|
42
47
|
theme: config.theme,
|
|
43
48
|
enableSkillCommands: true,
|
|
44
|
-
compaction: { enabled: true, reserveTokens
|
|
49
|
+
compaction: { enabled: true, reserveTokens, keepRecentTokens: 20000 },
|
|
45
50
|
retry: { enabled: true, maxRetries: 3 },
|
|
46
51
|
};
|
|
47
52
|
if (config.providers.length > 1) {
|
|
@@ -56,6 +61,7 @@ export function applyConfig(config) {
|
|
|
56
61
|
if (customProviders.length > 0) {
|
|
57
62
|
const models = {};
|
|
58
63
|
for (const cp of customProviders) {
|
|
64
|
+
const caps = cp.defaultModel ? MODEL_CAPABILITIES[cp.defaultModel] : undefined;
|
|
59
65
|
models[cp.name] = {
|
|
60
66
|
baseUrl: cp.baseUrl,
|
|
61
67
|
apiKey: cp.apiKey === "none" ? undefined : cp.apiKey,
|
|
@@ -63,10 +69,10 @@ export function applyConfig(config) {
|
|
|
63
69
|
models: cp.defaultModel ? [{
|
|
64
70
|
id: cp.defaultModel,
|
|
65
71
|
name: cp.defaultModel,
|
|
66
|
-
reasoning: false,
|
|
67
|
-
input: ["text"],
|
|
68
|
-
contextWindow: 128000,
|
|
69
|
-
maxTokens: 8192,
|
|
72
|
+
reasoning: cp.reasoning ?? caps?.reasoning ?? false,
|
|
73
|
+
input: cp.multimodal ? ["text", "image"] : (caps?.input ?? ["text"]),
|
|
74
|
+
contextWindow: cp.contextWindow ?? caps?.contextWindow ?? 128000,
|
|
75
|
+
maxTokens: cp.maxTokens ?? caps?.maxTokens ?? 8192,
|
|
70
76
|
}] : [],
|
|
71
77
|
};
|
|
72
78
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Footer Extension — Enhanced status bar
|
|
3
|
+
*
|
|
4
|
+
* Displays: ↑input ↓output Rremaining $cost percent/contextWindow (auto) | ⏱ elapsed | 📂 cwd | 🌿 branch | model • thinking
|
|
5
|
+
* Color-coded context usage: green <50%, yellow 50-75%, red >75%
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
|
+
|
|
12
|
+
export default function (pi: ExtensionAPI) {
|
|
13
|
+
let sessionStart = Date.now();
|
|
14
|
+
|
|
15
|
+
function formatElapsed(ms: number): string {
|
|
16
|
+
const s = Math.floor(ms / 1000);
|
|
17
|
+
if (s < 60) return `${s}s`;
|
|
18
|
+
const m = Math.floor(s / 60);
|
|
19
|
+
const rs = s % 60;
|
|
20
|
+
if (m < 60) return `${m}m${rs > 0 ? rs + "s" : ""}`;
|
|
21
|
+
const h = Math.floor(m / 60);
|
|
22
|
+
const rm = m % 60;
|
|
23
|
+
return `${h}h${rm > 0 ? rm + "m" : ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fmt(n: number): string {
|
|
27
|
+
if (n < 1000) return `${n}`;
|
|
28
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
32
|
+
sessionStart = Date.now();
|
|
33
|
+
|
|
34
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
35
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
36
|
+
const timer = setInterval(() => tui.requestRender(), 30000);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
dispose() { unsub(); clearInterval(timer); },
|
|
40
|
+
invalidate() {},
|
|
41
|
+
render(width: number): string[] {
|
|
42
|
+
// --- Tokens & Cost ---
|
|
43
|
+
let input = 0, output = 0, cost = 0;
|
|
44
|
+
for (const e of ctx.sessionManager.getBranch()) {
|
|
45
|
+
if (e.type === "message" && e.message.role === "assistant") {
|
|
46
|
+
const m = e.message as AssistantMessage;
|
|
47
|
+
input += m.usage.input;
|
|
48
|
+
output += m.usage.output;
|
|
49
|
+
cost += m.usage.cost.total;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Context usage ---
|
|
54
|
+
const usage = ctx.getContextUsage();
|
|
55
|
+
const tokens = usage?.tokens ?? 0;
|
|
56
|
+
const ctxWindow = usage?.contextWindow ?? 0;
|
|
57
|
+
const pct = usage?.percent ?? 0;
|
|
58
|
+
const remaining = Math.max(0, ctxWindow - tokens);
|
|
59
|
+
|
|
60
|
+
// Color by usage level
|
|
61
|
+
const pctColor = pct > 75 ? "error" : pct > 50 ? "warning" : "success";
|
|
62
|
+
const pctStr = `${pct.toFixed(1)}%/${fmt(ctxWindow)}`;
|
|
63
|
+
|
|
64
|
+
const tokenStats = [
|
|
65
|
+
theme.fg("accent", `↑${fmt(input)}`),
|
|
66
|
+
theme.fg("dim", ` ↓${fmt(output)}`),
|
|
67
|
+
theme.fg("muted", ` R${fmt(remaining)}`),
|
|
68
|
+
theme.fg("warning", ` $${cost.toFixed(3)}`),
|
|
69
|
+
" ",
|
|
70
|
+
theme.fg(pctColor, pctStr),
|
|
71
|
+
theme.fg("dim", " (auto)"),
|
|
72
|
+
].join("");
|
|
73
|
+
|
|
74
|
+
// --- Elapsed ---
|
|
75
|
+
const elapsed = theme.fg("dim", `⏱ ${formatElapsed(Date.now() - sessionStart)}`);
|
|
76
|
+
|
|
77
|
+
// --- CWD (last 2 segments) ---
|
|
78
|
+
const cwd = process.cwd();
|
|
79
|
+
const parts = cwd.split("/");
|
|
80
|
+
const short = parts.length > 2 ? parts.slice(-2).join("/") : cwd;
|
|
81
|
+
const cwdStr = theme.fg("muted", `📂 ${short}`);
|
|
82
|
+
|
|
83
|
+
// --- Git branch ---
|
|
84
|
+
const branch = footerData.getGitBranch();
|
|
85
|
+
const branchStr = branch ? theme.fg("accent", `🌿 ${branch}`) : "";
|
|
86
|
+
|
|
87
|
+
// --- Right: model + thinking ---
|
|
88
|
+
const thinking = pi.getThinkingLevel();
|
|
89
|
+
const modelId = ctx.model?.id || "no-model";
|
|
90
|
+
const right = theme.fg("dim", `${modelId} • ${thinking}`);
|
|
91
|
+
|
|
92
|
+
// --- Layout ---
|
|
93
|
+
const sep = theme.fg("dim", " │ ");
|
|
94
|
+
const leftParts = [tokenStats, elapsed, cwdStr];
|
|
95
|
+
if (branchStr) leftParts.push(branchStr);
|
|
96
|
+
const left = leftParts.join(sep);
|
|
97
|
+
|
|
98
|
+
const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(sep) - visibleWidth(right)));
|
|
99
|
+
return [truncateToWidth(left + pad + right, width)];
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on("session_switch", async (event, _ctx) => {
|
|
106
|
+
if (event.reason === "new") {
|
|
107
|
+
sessionStart = Date.now();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|