@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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -0
  3. package/configs/opl-browser.json.sample +8 -0
  4. package/configs/opl-footer.json +48 -0
  5. package/configs/opl-footer.json.sample +48 -0
  6. package/configs/opl-input.json +22 -0
  7. package/configs/opl-input.json.sample +22 -0
  8. package/configs/opl-modes.json +291 -0
  9. package/configs/opl-modes.json.sample +285 -0
  10. package/configs/opl-simplebench.json +8 -0
  11. package/configs/opl-simplebench.json.sample +8 -0
  12. package/configs/opl-todo.json +12 -0
  13. package/configs/opl-todo.json.sample +12 -0
  14. package/configs/opl-webaccess.json +31 -0
  15. package/configs/opl-webaccess.json.sample +31 -0
  16. package/extensions/opl-browser/README.md +83 -0
  17. package/extensions/opl-browser/browser.ts +185 -0
  18. package/extensions/opl-browser/config.ts +37 -0
  19. package/extensions/opl-browser/index.ts +89 -0
  20. package/extensions/opl-browser/package-lock.json +60 -0
  21. package/extensions/opl-browser/package.json +10 -0
  22. package/extensions/opl-ctxtrim/README.md +78 -0
  23. package/extensions/opl-ctxtrim/index.ts +151 -0
  24. package/extensions/opl-footer/README.md +193 -0
  25. package/extensions/opl-footer/config.ts +91 -0
  26. package/extensions/opl-footer/git-status.ts +170 -0
  27. package/extensions/opl-footer/icons.ts +49 -0
  28. package/extensions/opl-footer/index.ts +368 -0
  29. package/extensions/opl-footer/segments/caveman.ts +25 -0
  30. package/extensions/opl-footer/segments/chat-mode.ts +24 -0
  31. package/extensions/opl-footer/segments/context.ts +92 -0
  32. package/extensions/opl-footer/segments/cost.ts +25 -0
  33. package/extensions/opl-footer/segments/git.ts +47 -0
  34. package/extensions/opl-footer/segments/helpers.ts +18 -0
  35. package/extensions/opl-footer/segments/index.ts +51 -0
  36. package/extensions/opl-footer/segments/mode-switcher.ts +24 -0
  37. package/extensions/opl-footer/segments/model.ts +22 -0
  38. package/extensions/opl-footer/segments/path.ts +34 -0
  39. package/extensions/opl-footer/segments/pi.ts +11 -0
  40. package/extensions/opl-footer/segments/plan-mode.ts +24 -0
  41. package/extensions/opl-footer/segments/separator.ts +9 -0
  42. package/extensions/opl-footer/segments/session-stats.ts +82 -0
  43. package/extensions/opl-footer/segments/thinking.ts +51 -0
  44. package/extensions/opl-footer/segments/tokens.ts +66 -0
  45. package/extensions/opl-footer/theme.ts +113 -0
  46. package/extensions/opl-footer/types.ts +172 -0
  47. package/extensions/opl-init/README.md +23 -0
  48. package/extensions/opl-init/index.ts +451 -0
  49. package/extensions/opl-input/README.md +83 -0
  50. package/extensions/opl-input/config.ts +137 -0
  51. package/extensions/opl-input/index.ts +284 -0
  52. package/extensions/opl-input/mode-style.ts +38 -0
  53. package/extensions/opl-input/utils.ts +378 -0
  54. package/extensions/opl-modes/README.md +132 -0
  55. package/extensions/opl-modes/config.ts +473 -0
  56. package/extensions/opl-modes/index.ts +1081 -0
  57. package/extensions/opl-modes/menus.ts +60 -0
  58. package/extensions/opl-modes/state.ts +152 -0
  59. package/extensions/opl-modes/types.ts +164 -0
  60. package/extensions/opl-modes/utils.ts +138 -0
  61. package/extensions/opl-questionnaire/README.md +26 -0
  62. package/extensions/opl-questionnaire/index.ts +445 -0
  63. package/extensions/opl-questionnaire/types.ts +55 -0
  64. package/extensions/opl-simplebench/README.md +242 -0
  65. package/extensions/opl-simplebench/artifact.ts +41 -0
  66. package/extensions/opl-simplebench/benchmark.ts +873 -0
  67. package/extensions/opl-simplebench/coding.ts +282 -0
  68. package/extensions/opl-simplebench/index.ts +203 -0
  69. package/extensions/opl-simplebench/llama-server.ts +290 -0
  70. package/extensions/opl-simplebench/metrics.ts +73 -0
  71. package/extensions/opl-simplebench/report.ts +46 -0
  72. package/extensions/opl-simplebench/research.ts +200 -0
  73. package/extensions/opl-simplebench/scoring.ts +48 -0
  74. package/extensions/opl-simplebench/tests.ts +39 -0
  75. package/extensions/opl-simplebench/types.ts +6 -0
  76. package/extensions/opl-simplebench/util/config.ts +170 -0
  77. package/extensions/opl-simplebench/util/debug.ts +32 -0
  78. package/extensions/opl-simplebench/util/format.ts +402 -0
  79. package/extensions/opl-simplebench/util/providers.ts +841 -0
  80. package/extensions/opl-todo/README.md +43 -0
  81. package/extensions/opl-todo/config.ts +71 -0
  82. package/extensions/opl-todo/index.ts +415 -0
  83. package/extensions/opl-webaccess/README.md +47 -0
  84. package/extensions/opl-webaccess/config.ts +55 -0
  85. package/extensions/opl-webaccess/extract.ts +83 -0
  86. package/extensions/opl-webaccess/index.ts +232 -0
  87. package/extensions/opl-webaccess/package-lock.json +345 -0
  88. package/extensions/opl-webaccess/package.json +18 -0
  89. package/extensions/opl-webaccess/pdf.ts +8 -0
  90. package/extensions/opl-webaccess/providers/ddgs.ts +72 -0
  91. package/extensions/opl-webaccess/providers/exa.ts +74 -0
  92. package/extensions/opl-webaccess/providers/gemini.ts +60 -0
  93. package/extensions/opl-webaccess/providers/searxng.ts +86 -0
  94. package/extensions/opl-webaccess/providers/tavily.ts +60 -0
  95. package/extensions/opl-webaccess/search.ts +36 -0
  96. package/extensions/opl-webaccess/storage.ts +51 -0
  97. package/extensions/opl-webaccess/turndown.d.ts +1 -0
  98. package/extensions/opl-webaccess/types.ts +26 -0
  99. package/extensions/opl-webaccess/utils.ts +28 -0
  100. 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.