@openlines/opl-pi-sht 0.1.14
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/LICENSE +21 -0
- package/README.md +191 -0
- package/configs/opl-browser.json.sample +8 -0
- package/configs/opl-footer.json +48 -0
- package/configs/opl-footer.json.sample +48 -0
- package/configs/opl-input.json +22 -0
- package/configs/opl-input.json.sample +22 -0
- package/configs/opl-modes.json +291 -0
- package/configs/opl-modes.json.sample +285 -0
- package/configs/opl-simplebench.json +8 -0
- package/configs/opl-simplebench.json.sample +8 -0
- package/configs/opl-todo.json +12 -0
- package/configs/opl-todo.json.sample +12 -0
- package/configs/opl-webaccess.json +31 -0
- package/configs/opl-webaccess.json.sample +31 -0
- package/extensions/opl-browser/README.md +83 -0
- package/extensions/opl-browser/browser.ts +185 -0
- package/extensions/opl-browser/config.ts +37 -0
- package/extensions/opl-browser/index.ts +89 -0
- package/extensions/opl-browser/package-lock.json +60 -0
- package/extensions/opl-browser/package.json +10 -0
- package/extensions/opl-ctxtrim/README.md +78 -0
- package/extensions/opl-ctxtrim/index.ts +151 -0
- package/extensions/opl-footer/README.md +193 -0
- package/extensions/opl-footer/config.ts +91 -0
- package/extensions/opl-footer/git-status.ts +170 -0
- package/extensions/opl-footer/icons.ts +49 -0
- package/extensions/opl-footer/index.ts +368 -0
- package/extensions/opl-footer/segments/caveman.ts +25 -0
- package/extensions/opl-footer/segments/chat-mode.ts +24 -0
- package/extensions/opl-footer/segments/context.ts +92 -0
- package/extensions/opl-footer/segments/cost.ts +25 -0
- package/extensions/opl-footer/segments/git.ts +47 -0
- package/extensions/opl-footer/segments/helpers.ts +18 -0
- package/extensions/opl-footer/segments/index.ts +51 -0
- package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
- package/extensions/opl-footer/segments/model.ts +22 -0
- package/extensions/opl-footer/segments/path.ts +34 -0
- package/extensions/opl-footer/segments/pi.ts +11 -0
- package/extensions/opl-footer/segments/plan-mode.ts +24 -0
- package/extensions/opl-footer/segments/separator.ts +9 -0
- package/extensions/opl-footer/segments/session-stats.ts +82 -0
- package/extensions/opl-footer/segments/thinking.ts +51 -0
- package/extensions/opl-footer/segments/tokens.ts +66 -0
- package/extensions/opl-footer/theme.ts +113 -0
- package/extensions/opl-footer/types.ts +172 -0
- package/extensions/opl-init/README.md +23 -0
- package/extensions/opl-init/index.ts +451 -0
- package/extensions/opl-input/README.md +83 -0
- package/extensions/opl-input/config.ts +137 -0
- package/extensions/opl-input/index.ts +284 -0
- package/extensions/opl-input/mode-style.ts +38 -0
- package/extensions/opl-input/utils.ts +378 -0
- package/extensions/opl-modes/README.md +132 -0
- package/extensions/opl-modes/config.ts +473 -0
- package/extensions/opl-modes/index.ts +1081 -0
- package/extensions/opl-modes/menus.ts +60 -0
- package/extensions/opl-modes/state.ts +152 -0
- package/extensions/opl-modes/types.ts +164 -0
- package/extensions/opl-modes/utils.ts +138 -0
- package/extensions/opl-questionnaire/README.md +26 -0
- package/extensions/opl-questionnaire/index.ts +445 -0
- package/extensions/opl-questionnaire/types.ts +55 -0
- package/extensions/opl-simplebench/README.md +242 -0
- package/extensions/opl-simplebench/artifact.ts +41 -0
- package/extensions/opl-simplebench/benchmark.ts +873 -0
- package/extensions/opl-simplebench/coding.ts +282 -0
- package/extensions/opl-simplebench/index.ts +203 -0
- package/extensions/opl-simplebench/llama-server.ts +290 -0
- package/extensions/opl-simplebench/metrics.ts +73 -0
- package/extensions/opl-simplebench/report.ts +46 -0
- package/extensions/opl-simplebench/research.ts +200 -0
- package/extensions/opl-simplebench/scoring.ts +48 -0
- package/extensions/opl-simplebench/tests.ts +39 -0
- package/extensions/opl-simplebench/types.ts +6 -0
- package/extensions/opl-simplebench/util/config.ts +170 -0
- package/extensions/opl-simplebench/util/debug.ts +32 -0
- package/extensions/opl-simplebench/util/format.ts +402 -0
- package/extensions/opl-simplebench/util/providers.ts +841 -0
- package/extensions/opl-todo/README.md +43 -0
- package/extensions/opl-todo/config.ts +71 -0
- package/extensions/opl-todo/index.ts +415 -0
- package/extensions/opl-webaccess/README.md +47 -0
- package/extensions/opl-webaccess/config.ts +55 -0
- package/extensions/opl-webaccess/extract.ts +83 -0
- package/extensions/opl-webaccess/index.ts +232 -0
- package/extensions/opl-webaccess/package-lock.json +345 -0
- package/extensions/opl-webaccess/package.json +18 -0
- package/extensions/opl-webaccess/pdf.ts +8 -0
- package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
- package/extensions/opl-webaccess/providers/exa.ts +74 -0
- package/extensions/opl-webaccess/providers/gemini.ts +60 -0
- package/extensions/opl-webaccess/providers/searxng.ts +86 -0
- package/extensions/opl-webaccess/providers/tavily.ts +60 -0
- package/extensions/opl-webaccess/search.ts +36 -0
- package/extensions/opl-webaccess/storage.ts +51 -0
- package/extensions/opl-webaccess/turndown.d.ts +1 -0
- package/extensions/opl-webaccess/types.ts +26 -0
- package/extensions/opl-webaccess/utils.ts +28 -0
- package/package.json +55 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { applyColor } from "../theme.js";
|
|
3
|
+
import { color } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export const modelSegment = {
|
|
6
|
+
id: "model" as const,
|
|
7
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
8
|
+
let modelName = ctx.model?.name || ctx.model?.id || "no-model";
|
|
9
|
+
|
|
10
|
+
if (modelName.startsWith("Claude ")) {
|
|
11
|
+
modelName = modelName.slice(7);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let content = color(ctx, "model", modelName);
|
|
15
|
+
|
|
16
|
+
if (ctx.model?.provider) {
|
|
17
|
+
content += ` ${applyColor(ctx.theme, "dim", `(${ctx.model.provider})`)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return { content, visible: true };
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
3
|
+
import { color, withIcon } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export const pathSegment = {
|
|
6
|
+
id: "path" as const,
|
|
7
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
8
|
+
const opts = ctx.options.path ?? {};
|
|
9
|
+
const mode = opts.mode ?? "basename";
|
|
10
|
+
|
|
11
|
+
let pwd = process.cwd();
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
13
|
+
|
|
14
|
+
if (mode === "basename") {
|
|
15
|
+
pwd = basename(pwd) || pwd;
|
|
16
|
+
} else {
|
|
17
|
+
if (home && pwd.startsWith(home)) {
|
|
18
|
+
pwd = `~${pwd.slice(home.length)}`;
|
|
19
|
+
}
|
|
20
|
+
if (pwd.startsWith("/work/")) {
|
|
21
|
+
pwd = pwd.slice(6);
|
|
22
|
+
}
|
|
23
|
+
if (mode === "abbreviated") {
|
|
24
|
+
const maxLen = opts.maxLength ?? 40;
|
|
25
|
+
if (pwd.length > maxLen) {
|
|
26
|
+
pwd = `…${pwd.slice(-(maxLen - 1))}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const content = withIcon(ctx.icons.folder, pwd);
|
|
32
|
+
return { content: color(ctx, "path", content), visible: true };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { color } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
export const piSegment = {
|
|
5
|
+
id: "pi" as const,
|
|
6
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
7
|
+
if (!ctx.icons.pi) return { content: "", visible: false };
|
|
8
|
+
const content = `${ctx.icons.pi} `;
|
|
9
|
+
return { content: color(ctx, "pi", content), visible: true };
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { applyColor } from "../theme.js";
|
|
3
|
+
import { color } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
interface PlanModeState {
|
|
6
|
+
mode: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function readPlanModeState(): PlanModeState | undefined {
|
|
10
|
+
return (globalThis as Record<string, unknown>).__planMode as PlanModeState | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const planModeSegment = {
|
|
14
|
+
id: "plan_mode" as const,
|
|
15
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
16
|
+
const state = readPlanModeState();
|
|
17
|
+
if (!state) return { content: "", visible: false };
|
|
18
|
+
|
|
19
|
+
const label = applyColor(ctx.theme, "dim", "Plan mode:");
|
|
20
|
+
const value = state.mode === "off" ? "OFF" : "ON";
|
|
21
|
+
|
|
22
|
+
return { content: `${label} ${color(ctx, "modeIndicator", value)}`, visible: true };
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { color } from "./helpers.js";
|
|
3
|
+
|
|
4
|
+
export const separatorSegment = {
|
|
5
|
+
id: "separator" as const,
|
|
6
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
7
|
+
return { content: color(ctx, "separator", ctx.icons.separator), visible: true };
|
|
8
|
+
},
|
|
9
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { applyColor } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
function dim(ctx: SegmentContext, s: string): string {
|
|
5
|
+
return applyColor(ctx.theme, "dim", s);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function val(ctx: SegmentContext, s: string): string {
|
|
9
|
+
return applyColor(ctx.theme, "text", s);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatMs(ms: number): string {
|
|
13
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
14
|
+
const m = Math.floor(ms / 60_000);
|
|
15
|
+
const s = Math.round((ms % 60_000) / 1000);
|
|
16
|
+
return `${m}m ${s}s`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const sessionStatsSegment = {
|
|
20
|
+
id: "session_stats" as const,
|
|
21
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
22
|
+
const { turns, steps, modelRequests, modelToolCalls } = ctx.sessionStats;
|
|
23
|
+
if (turns === 0) return { content: "", visible: false };
|
|
24
|
+
|
|
25
|
+
const parts: string[] = [
|
|
26
|
+
val(ctx, `${turns}`) + dim(ctx, " turns"),
|
|
27
|
+
val(ctx, `${steps}`) + dim(ctx, " steps"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Add model stats if available
|
|
31
|
+
if (modelRequests > 0) {
|
|
32
|
+
parts.push(val(ctx, `${modelRequests}`) + dim(ctx, " mreq"));
|
|
33
|
+
}
|
|
34
|
+
if (modelToolCalls > 0) {
|
|
35
|
+
parts.push(val(ctx, `${modelToolCalls}`) + dim(ctx, " mtool"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { content: parts.join(dim(ctx, " · ")), visible: true };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const perfStatsSegment = {
|
|
43
|
+
id: "perf_stats" as const,
|
|
44
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
45
|
+
const { turns, llmMs, toolMs, ttftSamples, lastTurnaroundMs } = ctx.sessionStats;
|
|
46
|
+
if (turns === 0) return { content: "", visible: false };
|
|
47
|
+
|
|
48
|
+
const { input, output, cacheRead } = ctx.usageStats;
|
|
49
|
+
|
|
50
|
+
const parts: string[] = []
|
|
51
|
+
|
|
52
|
+
// LLM (last-turn TAT) · Tool time
|
|
53
|
+
const tat = lastTurnaroundMs > 0
|
|
54
|
+
? dim(ctx, " (") + val(ctx, formatMs(lastTurnaroundMs)) + dim(ctx, ")")
|
|
55
|
+
: "";
|
|
56
|
+
parts.push(
|
|
57
|
+
dim(ctx, "LLM ") + val(ctx, formatMs(llmMs)) + tat +
|
|
58
|
+
dim(ctx, " · Tool ") + val(ctx, formatMs(toolMs)),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// TTFT avg · tok/s
|
|
62
|
+
const ttftAvg = ttftSamples.length > 0
|
|
63
|
+
? ttftSamples.reduce((a, b) => a + b, 0) / ttftSamples.length
|
|
64
|
+
: 0;
|
|
65
|
+
const tokPerSec = llmMs > 0 ? Math.round(output / (llmMs / 1000)) : 0;
|
|
66
|
+
if (ttftAvg > 0 || tokPerSec > 0) {
|
|
67
|
+
parts.push(
|
|
68
|
+
dim(ctx, "TTFT ") + val(ctx, `${(ttftAvg / 1000).toFixed(1)}s`) +
|
|
69
|
+
dim(ctx, " · ") + val(ctx, `${tokPerSec}`) + dim(ctx, " tok/s"),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Cache hit %
|
|
74
|
+
const total = input + cacheRead;
|
|
75
|
+
if (total > 0 && cacheRead > 0) {
|
|
76
|
+
const pct = Math.round((cacheRead / total) * 100);
|
|
77
|
+
parts.push(dim(ctx, "Cache ") + val(ctx, `${pct}%`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { content: parts.join(dim(ctx, " | ")), visible: true };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext, SemanticColor } from "../types.js";
|
|
2
|
+
import { applyColor, rainbow } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
const LEVEL_CAPS: Record<string, string> = {
|
|
5
|
+
off: "OFF",
|
|
6
|
+
minimal: "MINIMAL",
|
|
7
|
+
low: "LOW",
|
|
8
|
+
medium: "MEDIUM",
|
|
9
|
+
high: "HIGH",
|
|
10
|
+
xhigh: "EXTRA HIGH",
|
|
11
|
+
max: "MAX",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const LEVEL_COLOR_KEY: Record<string, SemanticColor> = {
|
|
15
|
+
off: "thinkingOff",
|
|
16
|
+
minimal: "thinkingMinimal",
|
|
17
|
+
low: "thinkingLow",
|
|
18
|
+
medium: "thinkingMedium",
|
|
19
|
+
high: "thinkingHigh",
|
|
20
|
+
// xhigh/max absent from DEFAULT_COLORS — presence here enables user override
|
|
21
|
+
xhigh: "thinkingXhigh",
|
|
22
|
+
max: "thinkingMax",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const LEVEL_COLOR_FALLBACK: Record<string, string> = {
|
|
26
|
+
off: "dim",
|
|
27
|
+
minimal: "muted",
|
|
28
|
+
low: "warning",
|
|
29
|
+
medium: "success",
|
|
30
|
+
high: "#afb9fe",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const thinkingSegment = {
|
|
34
|
+
id: "thinking" as const,
|
|
35
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
36
|
+
const level = ctx.thinkingLevel || "off";
|
|
37
|
+
|
|
38
|
+
const label = applyColor(ctx.theme, "dim", "Thinking:");
|
|
39
|
+
const text = LEVEL_CAPS[level] || level.toUpperCase();
|
|
40
|
+
|
|
41
|
+
const colorKey = LEVEL_COLOR_KEY[level];
|
|
42
|
+
const configured = colorKey !== undefined ? ctx.colors[colorKey] : undefined;
|
|
43
|
+
const textColor = configured ?? LEVEL_COLOR_FALLBACK[level] ?? "muted";
|
|
44
|
+
|
|
45
|
+
// Rainbow is the default for xhigh/max; an explicit color config overrides it
|
|
46
|
+
const useRainbow = !configured && (level === "xhigh" || level === "max");
|
|
47
|
+
const coloredText = useRainbow ? rainbow(text) : applyColor(ctx.theme, textColor as any, text);
|
|
48
|
+
|
|
49
|
+
return { content: `${label} ${coloredText}`, visible: true };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { RenderedSegment, SegmentContext } from "../types.js";
|
|
2
|
+
import { applyColor } from "../theme.js";
|
|
3
|
+
import { color, formatTokens } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
function lbl(ctx: SegmentContext, text: string): string {
|
|
6
|
+
return applyColor(ctx.theme, "dim", text);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function val(ctx: SegmentContext, text: string): string {
|
|
10
|
+
return color(ctx, "tokens", text);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const tokenTotalSegment = {
|
|
14
|
+
id: "token_total" as const,
|
|
15
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
16
|
+
const { input, output, cacheRead, cacheWrite } = ctx.usageStats;
|
|
17
|
+
const total = input + output + cacheRead + cacheWrite;
|
|
18
|
+
const cached = cacheRead + cacheWrite;
|
|
19
|
+
|
|
20
|
+
const content =
|
|
21
|
+
lbl(ctx, "T:") + " " + val(ctx, formatTokens(total)) +
|
|
22
|
+
" " + lbl(ctx, "(") + val(ctx, formatTokens(cached)) + lbl(ctx, " cached)") +
|
|
23
|
+
" " + lbl(ctx, "↑") + " " + val(ctx, formatTokens(input)) +
|
|
24
|
+
" " + lbl(ctx, "↓") + " " + val(ctx, formatTokens(output));
|
|
25
|
+
|
|
26
|
+
return { content, visible: true };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const tokenInSegment = {
|
|
31
|
+
id: "token_in" as const,
|
|
32
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
33
|
+
return {
|
|
34
|
+
content: lbl(ctx, "↑") + " " + val(ctx, formatTokens(ctx.usageStats.input)),
|
|
35
|
+
visible: true,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const tokenOutSegment = {
|
|
41
|
+
id: "token_out" as const,
|
|
42
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
43
|
+
return {
|
|
44
|
+
content: lbl(ctx, "↓") + " " + val(ctx, formatTokens(ctx.usageStats.output)),
|
|
45
|
+
visible: true,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const cacheReadSegment = {
|
|
51
|
+
id: "cache_read" as const,
|
|
52
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
53
|
+
const { cacheRead } = ctx.usageStats;
|
|
54
|
+
if (!cacheRead) return { content: "", visible: false };
|
|
55
|
+
return { content: val(ctx, formatTokens(cacheRead)), visible: true };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const cacheWriteSegment = {
|
|
60
|
+
id: "cache_write" as const,
|
|
61
|
+
render(ctx: SegmentContext): RenderedSegment {
|
|
62
|
+
const { cacheWrite } = ctx.usageStats;
|
|
63
|
+
if (!cacheWrite) return { content: "", visible: false };
|
|
64
|
+
return { content: val(ctx, formatTokens(cacheWrite)), visible: true };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ColorScheme, ColorValue, SemanticColor } from "./types.js";
|
|
3
|
+
|
|
4
|
+
// Default color scheme — thinkingXhigh and thinkingMax are intentionally absent;
|
|
5
|
+
// their absence signals "use rainbow" in thinking.ts.
|
|
6
|
+
const DEFAULT_COLORS: ColorScheme = {
|
|
7
|
+
pi: "accent",
|
|
8
|
+
model: "#c07898",
|
|
9
|
+
path: "dim",
|
|
10
|
+
git: "success",
|
|
11
|
+
gitDirty: "warning",
|
|
12
|
+
gitClean: "success",
|
|
13
|
+
thinking: "muted",
|
|
14
|
+
thinkingOff: "dim",
|
|
15
|
+
thinkingMinimal: "muted",
|
|
16
|
+
thinkingLow: "warning",
|
|
17
|
+
thinkingMedium: "success",
|
|
18
|
+
thinkingHigh: "#afb9fe",
|
|
19
|
+
context: "dim",
|
|
20
|
+
contextWarn: "warning",
|
|
21
|
+
contextError: "error",
|
|
22
|
+
contextLabel: "muted",
|
|
23
|
+
cost: "muted",
|
|
24
|
+
tokens: "muted",
|
|
25
|
+
separator: "#4e4c49",
|
|
26
|
+
modeIndicator: "muted",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function isHexColor(color: ColorValue): color is `#${string}` {
|
|
30
|
+
return typeof color === "string" && color.startsWith("#");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hexToAnsi(hex: string): string {
|
|
34
|
+
const h = hex.replace("#", "");
|
|
35
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return "";
|
|
36
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
37
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
38
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
39
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function applyColor(
|
|
43
|
+
theme: Theme,
|
|
44
|
+
color: ColorValue,
|
|
45
|
+
text: string
|
|
46
|
+
): string {
|
|
47
|
+
if (isHexColor(color)) {
|
|
48
|
+
return `${hexToAnsi(color)}${text}\x1b[0m`;
|
|
49
|
+
}
|
|
50
|
+
return theme.fg(color as ThemeColor, text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fg(
|
|
54
|
+
theme: Theme,
|
|
55
|
+
semantic: SemanticColor,
|
|
56
|
+
text: string,
|
|
57
|
+
presetColors?: ColorScheme
|
|
58
|
+
): string {
|
|
59
|
+
const color = presetColors?.[semantic] ?? DEFAULT_COLORS[semantic];
|
|
60
|
+
if (color === undefined) return text;
|
|
61
|
+
return applyColor(theme, color, text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hslToAnsi(h: number, s: number, l: number): string {
|
|
65
|
+
if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l)) return "";
|
|
66
|
+
const a = s * Math.min(l, 1 - l);
|
|
67
|
+
const f = (n: number) => {
|
|
68
|
+
const k = (n + h / 30) % 12;
|
|
69
|
+
const v = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
70
|
+
return Math.round(v * 255);
|
|
71
|
+
};
|
|
72
|
+
return `\x1b[38;2;${f(0)};${f(8)};${f(4)}m`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Spread hues evenly across all non-space characters
|
|
76
|
+
export function rainbow(text: string): string {
|
|
77
|
+
const visibleChars = [...text].filter(c => c !== " " && c !== ":").length;
|
|
78
|
+
let result = "";
|
|
79
|
+
let colorIndex = 0;
|
|
80
|
+
for (const char of text) {
|
|
81
|
+
if (char === " " || char === ":") {
|
|
82
|
+
result += char;
|
|
83
|
+
} else {
|
|
84
|
+
const hue = (colorIndex / Math.max(visibleChars - 1, 1)) * 300;
|
|
85
|
+
result += hslToAnsi(hue, 0.85, 0.65) + char;
|
|
86
|
+
colorIndex++;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result + "\x1b[0m";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getDefaultColors(): ColorScheme {
|
|
93
|
+
return { ...DEFAULT_COLORS };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Resolve any ColorValue to an RGB triple for gradient math.
|
|
97
|
+
// Named theme tokens are probed by applying them to a dummy string and parsing
|
|
98
|
+
// the resulting ANSI 24-bit escape sequence. Returns null when the theme uses a
|
|
99
|
+
// non-24-bit code (e.g. 256-colour or named colour) — callers should fall back.
|
|
100
|
+
export function resolveColorToRgb(
|
|
101
|
+
theme: Theme,
|
|
102
|
+
color: ColorValue
|
|
103
|
+
): { r: number; g: number; b: number } | null {
|
|
104
|
+
if (isHexColor(color)) {
|
|
105
|
+
const h = color.replace("#", "");
|
|
106
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
107
|
+
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16) };
|
|
108
|
+
}
|
|
109
|
+
const probed = theme.fg(color as ThemeColor, "X");
|
|
110
|
+
const match = probed.match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
|
|
111
|
+
if (!match) return null;
|
|
112
|
+
return { r: parseInt(match[1]), g: parseInt(match[2]), b: parseInt(match[3]) };
|
|
113
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { IconSet } from "./icons.js";
|
|
3
|
+
|
|
4
|
+
// Theme color - either a pi theme color name or a custom hex color
|
|
5
|
+
export type ColorValue = ThemeColor | `#${string}`;
|
|
6
|
+
|
|
7
|
+
// Semantic color names for segments
|
|
8
|
+
export type SemanticColor =
|
|
9
|
+
| "pi"
|
|
10
|
+
| "model"
|
|
11
|
+
| "path"
|
|
12
|
+
| "git"
|
|
13
|
+
| "gitDirty"
|
|
14
|
+
| "gitClean"
|
|
15
|
+
| "thinking"
|
|
16
|
+
| "thinkingOff"
|
|
17
|
+
| "thinkingMinimal"
|
|
18
|
+
| "thinkingLow"
|
|
19
|
+
| "thinkingMedium"
|
|
20
|
+
| "thinkingHigh"
|
|
21
|
+
| "thinkingXhigh"
|
|
22
|
+
| "thinkingMax"
|
|
23
|
+
| "context"
|
|
24
|
+
| "contextWarn"
|
|
25
|
+
| "contextError"
|
|
26
|
+
| "contextLabel"
|
|
27
|
+
| "cost"
|
|
28
|
+
| "tokens"
|
|
29
|
+
| "separator"
|
|
30
|
+
| "modeIndicator";
|
|
31
|
+
|
|
32
|
+
// Color scheme mapping semantic names to actual colors
|
|
33
|
+
export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
|
|
34
|
+
|
|
35
|
+
// Segment identifiers
|
|
36
|
+
export type StatusLineSegmentId =
|
|
37
|
+
| "pi"
|
|
38
|
+
| "model"
|
|
39
|
+
| "path"
|
|
40
|
+
| "git"
|
|
41
|
+
| "token_in"
|
|
42
|
+
| "token_out"
|
|
43
|
+
| "token_total"
|
|
44
|
+
| "cost"
|
|
45
|
+
| "context_pct"
|
|
46
|
+
| "context_total"
|
|
47
|
+
| "cache_read"
|
|
48
|
+
| "cache_write"
|
|
49
|
+
| "thinking"
|
|
50
|
+
| "caveman"
|
|
51
|
+
| "plan_mode"
|
|
52
|
+
| "chat_mode"
|
|
53
|
+
| "mode_switcher"
|
|
54
|
+
| "session_stats"
|
|
55
|
+
| "perf_stats"
|
|
56
|
+
| "separator"
|
|
57
|
+
| `text:${string}`;
|
|
58
|
+
|
|
59
|
+
// Per-segment options
|
|
60
|
+
export interface StatusLineSegmentOptions {
|
|
61
|
+
path?: {
|
|
62
|
+
mode?: "basename" | "abbreviated" | "full";
|
|
63
|
+
maxLength?: number;
|
|
64
|
+
};
|
|
65
|
+
git?: {
|
|
66
|
+
showBranch?: boolean;
|
|
67
|
+
showStaged?: boolean;
|
|
68
|
+
showUnstaged?: boolean;
|
|
69
|
+
showUntracked?: boolean;
|
|
70
|
+
};
|
|
71
|
+
contextBar?: {
|
|
72
|
+
barWidth?: number;
|
|
73
|
+
filledChar?: string;
|
|
74
|
+
unfilledChar?: string;
|
|
75
|
+
unfilledColor?: ColorValue;
|
|
76
|
+
gradientStart?: ColorValue;
|
|
77
|
+
gradientMid?: ColorValue;
|
|
78
|
+
gradientEnd?: ColorValue;
|
|
79
|
+
gradientMidPoint?: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Git status data
|
|
84
|
+
export interface GitStatus {
|
|
85
|
+
branch: string | null;
|
|
86
|
+
staged: number;
|
|
87
|
+
unstaged: number;
|
|
88
|
+
untracked: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Usage statistics
|
|
92
|
+
export interface UsageStats {
|
|
93
|
+
input: number;
|
|
94
|
+
output: number;
|
|
95
|
+
cacheRead: number;
|
|
96
|
+
cacheWrite: number;
|
|
97
|
+
cost: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Session performance stats accumulated via events
|
|
101
|
+
export interface SessionStats {
|
|
102
|
+
turns: number;
|
|
103
|
+
steps: number;
|
|
104
|
+
modelRequests: number;
|
|
105
|
+
modelToolCalls: number;
|
|
106
|
+
llmMs: number;
|
|
107
|
+
toolMs: number;
|
|
108
|
+
ttftSamples: number[];
|
|
109
|
+
lastTurnaroundMs: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Context passed to segment render functions
|
|
113
|
+
export interface SegmentContext {
|
|
114
|
+
model: { id: string; name?: string; reasoning?: boolean; contextWindow?: number; provider?: string; baseUrl?: string } | undefined;
|
|
115
|
+
isLocalModel: boolean;
|
|
116
|
+
thinkingLevel: string;
|
|
117
|
+
sessionId: string | undefined;
|
|
118
|
+
usageStats: UsageStats;
|
|
119
|
+
contextPercent: number;
|
|
120
|
+
contextWindow: number;
|
|
121
|
+
usingSubscription: boolean;
|
|
122
|
+
sessionStartTime: number;
|
|
123
|
+
git: GitStatus;
|
|
124
|
+
options: StatusLineSegmentOptions;
|
|
125
|
+
width: number;
|
|
126
|
+
theme: Theme;
|
|
127
|
+
colors: ColorScheme;
|
|
128
|
+
icons: IconSet;
|
|
129
|
+
sessionStats: SessionStats;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Minimal event shapes used by the opl-footer handlers
|
|
133
|
+
export interface ToolResultEvent {
|
|
134
|
+
toolName: string;
|
|
135
|
+
input?: { command?: string };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface UserBashEvent {
|
|
139
|
+
command: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Minimal session event shapes used for opl-footer stats
|
|
143
|
+
export interface ThinkingLevelEvent {
|
|
144
|
+
type: "thinking_level_change";
|
|
145
|
+
thinkingLevel?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface AssistantMessageEvent {
|
|
149
|
+
type: "message";
|
|
150
|
+
message: { role: string };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type SessionEvent = ThinkingLevelEvent | AssistantMessageEvent | { type: string };
|
|
154
|
+
|
|
155
|
+
// Rendered segment output
|
|
156
|
+
export interface RenderedSegment {
|
|
157
|
+
content: string;
|
|
158
|
+
visible: boolean;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// User configuration from opl-footer.json
|
|
162
|
+
export interface FooterUserConfig {
|
|
163
|
+
row1LeftSegments?: StatusLineSegmentId[];
|
|
164
|
+
row1RightSegments?: StatusLineSegmentId[];
|
|
165
|
+
row2LeftSegments?: StatusLineSegmentId[];
|
|
166
|
+
row2RightSegments?: StatusLineSegmentId[];
|
|
167
|
+
row3LeftSegments?: StatusLineSegmentId[];
|
|
168
|
+
row3RightSegments?: StatusLineSegmentId[];
|
|
169
|
+
colors?: ColorScheme;
|
|
170
|
+
segmentOptions?: StatusLineSegmentOptions;
|
|
171
|
+
icons?: Partial<IconSet>;
|
|
172
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# opl-init
|
|
2
|
+
|
|
3
|
+
Generates or refreshes a repository-specific `AGENTS.md` guide through `/init`.
|
|
4
|
+
|
|
5
|
+
## Commands, flags, and shortcuts
|
|
6
|
+
|
|
7
|
+
- `/init`: crawl the current repository and create or update `AGENTS.md` when its fingerprint is missing or stale.
|
|
8
|
+
- No flags or shortcuts.
|
|
9
|
+
|
|
10
|
+
## Extension features
|
|
11
|
+
|
|
12
|
+
- Produces evidence-based guidance for project purpose, commands, architecture, configuration, testing, operational quirks, and key files.
|
|
13
|
+
- Crawls deterministically to depth 3, ignores generated/dependency directories, caps directory listings at 40 entries, and caps the rendered tree at 300 lines with explicit omission markers.
|
|
14
|
+
- Re-walks declared `pnpm-workspace.yaml` and Cargo workspace members with their own depth-3 budget; reports `turbo.json` and `nx.json` as contextual manifests.
|
|
15
|
+
- Collects extension counts, known manifests, and npm scripts without embedding full manifest bodies.
|
|
16
|
+
- Incorporates relevant Cursor, Copilot, Claude Code, Windsurf, Cline, and Devin rule sources without copying them verbatim or treating them as more authoritative than repository evidence.
|
|
17
|
+
- Writes a deterministic baseline `AGENTS.md` before dispatching the model turn, so lower-capability models cannot leave the guide only in chat; the model is then prompted to refine it with repository-specific guidance and verify the file.
|
|
18
|
+
- Skips the model turn when the final `<!-- opl-init:fp <fingerprint> -->` marker is current.
|
|
19
|
+
- Excludes `AGENTS.md` itself from the Git status fingerprint, preventing the act of updating the guide from making it immediately stale.
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
`index.ts` owns crawling, fingerprinting, baseline writing, foreign-rule discovery, and prompt dispatch. Git repositories fingerprint `HEAD` plus relevant `git status --porcelain=v1 --untracked-files=all` output, excluding `AGENTS.md` itself; non-Git repositories use sorted path, size, and mtime tuples. Crawl context remains in the active Pi transcript only; cross-session structured retrieval is not implemented. The model prompt explicitly requires a write-tool call and a read-back verification, but the baseline write is the reliability boundary.
|