pi-cc-extensions 0.1.1

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 (39) hide show
  1. package/README.md +141 -0
  2. package/assets/readme/context-preview.webp +0 -0
  3. package/assets/readme/context-usage.webp +0 -0
  4. package/assets/readme/hero.svg +101 -0
  5. package/assets/readme/session-reference.webp +0 -0
  6. package/assets/readme/showcase.webp +0 -0
  7. package/extensions/README.md +3 -0
  8. package/extensions/ccstyle-powerline/bash-mode/completion.ts +556 -0
  9. package/extensions/ccstyle-powerline/bash-mode/editor.ts +442 -0
  10. package/extensions/ccstyle-powerline/bash-mode/history.ts +151 -0
  11. package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +286 -0
  12. package/extensions/ccstyle-powerline/bash-mode/transcript.ts +108 -0
  13. package/extensions/ccstyle-powerline/bash-mode/types.ts +59 -0
  14. package/extensions/ccstyle-powerline/colors.ts +69 -0
  15. package/extensions/ccstyle-powerline/context-usage.ts +46 -0
  16. package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +107 -0
  17. package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +1317 -0
  18. package/extensions/ccstyle-powerline/git-status.ts +218 -0
  19. package/extensions/ccstyle-powerline/icons.ts +181 -0
  20. package/extensions/ccstyle-powerline/powerline-config.ts +234 -0
  21. package/extensions/ccstyle-powerline/powerline.ts +3150 -0
  22. package/extensions/ccstyle-powerline/presets.ts +109 -0
  23. package/extensions/ccstyle-powerline/render-scheduler.ts +24 -0
  24. package/extensions/ccstyle-powerline/segments.ts +476 -0
  25. package/extensions/ccstyle-powerline/separators.ts +41 -0
  26. package/extensions/ccstyle-powerline/shortcuts.ts +57 -0
  27. package/extensions/ccstyle-powerline/theme.json +26 -0
  28. package/extensions/ccstyle-powerline/theme.ts +241 -0
  29. package/extensions/ccstyle-powerline/types.ts +197 -0
  30. package/extensions/ccstyle-powerline/welcome-dismiss.ts +34 -0
  31. package/extensions/ccstyle-powerline/welcome.ts +611 -0
  32. package/extensions/ccstyle-powerline/working-vibes.ts +695 -0
  33. package/extensions/claude-code-style.ts +987 -0
  34. package/extensions/context.ts +362 -0
  35. package/extensions/session-reference/README.md +28 -0
  36. package/extensions/session-reference/core.test.ts +74 -0
  37. package/extensions/session-reference/core.ts +258 -0
  38. package/extensions/session-reference/index.ts +379 -0
  39. package/package.json +33 -0
@@ -0,0 +1,57 @@
1
+ import { matchesKey } from "@earendil-works/pi-tui";
2
+
3
+ const CTRL_NAVIGATION_SHORTCUT_PATTERNS = new Map<string, RegExp>([
4
+ // xterm/Windows Terminal, Kitty keyboard protocol, and modifyOtherKeys.
5
+ ["ctrl+up", /^\x1b\[(?:1;5(?::[12])?A|57419;5(?::[12])?u|27;5;65~)$/],
6
+ ["ctrl+down", /^\x1b\[(?:1;5(?::[12])?B|57420;5(?::[12])?u|27;5;66~)$/],
7
+ ["ctrl+end", /^\x1b\[(?:1;5(?::[12])?F|4;5(?::[12])?~|57424;5(?::[12])?u|27;5;70~)$/],
8
+ ]);
9
+
10
+ const SUPER_SHORTCUT_PATTERNS = new Map<string, RegExp>([
11
+ ["super+up", /^\x1b\[(?:1;9(?::[12])?[AH]|574(?:19|23);9(?::[12])?u|7;9(?::[12])?~|27;9;65~)$/],
12
+ ["super+down", /^\x1b\[(?:1;9(?::[12])?[BF]|574(?:20|24);9(?::[12])?u|8;9(?::[12])?~|27;9;66~)$/],
13
+ ["super+home", /^\x1b\[(?:1;9(?::[12])?H|57423;9(?::[12])?u|7;9(?::[12])?~)$/],
14
+ ["super+end", /^\x1b\[(?:1;9(?::[12])?F|57424;9(?::[12])?u|8;9(?::[12])?~)$/],
15
+ ["super+pageup", /^\x1b\[(?:5;9(?::[12])?~|57421;9(?::[12])?u)$/],
16
+ ["super+pagedown", /^\x1b\[(?:6;9(?::[12])?~|57422;9(?::[12])?u)$/],
17
+ ["super+shift+up", /^\x1b\[(?:1;10(?::[12])?[AH]|574(?:19|23);10(?::[12])?u|7;10(?::[12])?~|27;10;65~)$/],
18
+ ["super+shift+down", /^\x1b\[(?:1;10(?::[12])?[BF]|574(?:20|24);10(?::[12])?u|8;10(?::[12])?~|27;10;66~)$/],
19
+ ["super+shift+home", /^\x1b\[(?:1;10(?::[12])?H|57423;10(?::[12])?u|7;10(?::[12])?~)$/],
20
+ ["super+shift+end", /^\x1b\[(?:1;10(?::[12])?F|57424;10(?::[12])?u|8;10(?::[12])?~)$/],
21
+ ]);
22
+
23
+ export function shortcutUsesSuper(shortcut: string): boolean {
24
+ const parts = shortcut.toLowerCase().split("+");
25
+ return parts.slice(0, -1).includes("super");
26
+ }
27
+
28
+ export function isSupportedSuperShortcut(shortcut: string): boolean {
29
+ return SUPER_SHORTCUT_PATTERNS.has(shortcut.toLowerCase());
30
+ }
31
+
32
+ export function shortcutConflictKey(shortcut: string): string {
33
+ switch (shortcut.toLowerCase()) {
34
+ case "super+home":
35
+ return "super+up";
36
+ case "super+end":
37
+ return "super+down";
38
+ case "super+shift+home":
39
+ return "super+shift+up";
40
+ case "super+shift+end":
41
+ return "super+shift+down";
42
+ default:
43
+ return shortcut;
44
+ }
45
+ }
46
+
47
+ export function matchesConfiguredShortcut(data: string, shortcut: string): boolean {
48
+ const normalizedShortcut = shortcut.toLowerCase();
49
+ if (shortcutUsesSuper(normalizedShortcut)) {
50
+ return SUPER_SHORTCUT_PATTERNS.get(normalizedShortcut)?.test(data) ?? false;
51
+ }
52
+
53
+ // Some Windows terminal/protocol combinations are not normalized by
54
+ // pi-tui's matchesKey(), so recognize raw Ctrl+navigation encodings too.
55
+ if (CTRL_NAVIGATION_SHORTCUT_PATTERNS.get(normalizedShortcut)?.test(data)) return true;
56
+ return matchesKey(data, normalizedShortcut);
57
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "colors": {
3
+ "model": "#3794FF",
4
+ "shellMode": "#4FC1FF",
5
+ "path": "#4EC9B0",
6
+ "gitDirty": "#F48771",
7
+ "gitClean": "#89D185",
8
+ "thinking": "#808080",
9
+ "thinkingMinimal": "#064675",
10
+ "thinkingLow": "#125489",
11
+ "thinkingMedium": "#1F5F99",
12
+ "thinkingHigh": "#3F87C5",
13
+ "thinkingXhigh": "#66A9DC",
14
+ "thinkingMax": "#8FC5F0",
15
+ "context": "#808080",
16
+ "contextWarn": "#DCDCAA",
17
+ "contextError": "#F14C4C",
18
+ "cost": "#CE9178",
19
+ "tokens": "#9CDCFE",
20
+ "separator": "#6A6A6A",
21
+ "border": "#3E3E42"
22
+ },
23
+ "icons": {
24
+ "context": ""
25
+ }
26
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Theme system for powerline-footer
3
+ *
4
+ * Colors are resolved in order:
5
+ * 1. User overrides from theme.json (if exists)
6
+ * 2. Preset colors
7
+ * 3. Default colors
8
+ */
9
+
10
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
11
+ import { existsSync, readFileSync } from "node:fs";
12
+ import { join, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import type { ColorScheme, ColorValue, SemanticColor, ThemeLike } from "./types.ts";
15
+
16
+ export interface PowerlineThemeConfig {
17
+ colors?: unknown;
18
+ icons?: unknown;
19
+ }
20
+
21
+ // VS Code Modern Dark-inspired fallback defaults. Native pi themes take
22
+ // precedence for thinking-level colors when available.
23
+ const DEFAULT_COLORS: Required<ColorScheme> = {
24
+ model: "#3794ff",
25
+ shellMode: "#4fc1ff",
26
+ path: "#4ec9b0",
27
+ gitDirty: "#f48771",
28
+ gitClean: "#89d185",
29
+ thinking: "#808080",
30
+ thinkingOff: "#808080",
31
+ thinkingMinimal: "#064675",
32
+ thinkingLow: "#125489",
33
+ thinkingMedium: "#1f5f99",
34
+ thinkingHigh: "#3f87c5",
35
+ thinkingXhigh: "#66a9dc",
36
+ thinkingMax: "#8fc5f0",
37
+ context: "#808080",
38
+ contextWarn: "#dcdcaa",
39
+ contextError: "#f14c4c",
40
+ cost: "#ce9178",
41
+ tokens: "#9cdcfe",
42
+ separator: "#6a6a6a",
43
+ border: "#3e3e42",
44
+ };
45
+
46
+ // Cache for user theme overrides
47
+ let userThemeCache: ColorScheme | null = null;
48
+ let userThemeCacheTime = 0;
49
+ let themeConfigCache: PowerlineThemeConfig | null = null;
50
+ let themeConfigCacheTime = 0;
51
+ const CACHE_TTL = 5000; // 5 seconds
52
+ const warnedInvalidThemeColors = new Set<string>();
53
+
54
+ /** Force the next render to reload theme.json, including after Pi /reload. */
55
+ export function invalidateThemeCache(): void {
56
+ userThemeCache = null;
57
+ userThemeCacheTime = 0;
58
+ themeConfigCache = null;
59
+ themeConfigCacheTime = 0;
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function sanitizeUserThemeOverrides(value: unknown): ColorScheme {
67
+ if (!isRecord(value)) {
68
+ return {};
69
+ }
70
+
71
+ const sanitized: ColorScheme = {};
72
+ for (const [key, rawColor] of Object.entries(value)) {
73
+ if (!Object.prototype.hasOwnProperty.call(DEFAULT_COLORS, key)) {
74
+ continue;
75
+ }
76
+ if (typeof rawColor !== "string") {
77
+ continue;
78
+ }
79
+
80
+ const color = rawColor.trim();
81
+ if (!color) {
82
+ continue;
83
+ }
84
+
85
+ sanitized[key as SemanticColor] = color as ColorValue;
86
+ }
87
+
88
+ return sanitized;
89
+ }
90
+
91
+ /**
92
+ * Get the path to the theme.json file
93
+ */
94
+ function getThemePath(): string {
95
+ const extDir = dirname(fileURLToPath(import.meta.url));
96
+ return join(extDir, "theme.json");
97
+ }
98
+
99
+ /**
100
+ * Load user theme config from theme.json
101
+ */
102
+ export function loadThemeConfig(): PowerlineThemeConfig {
103
+ const now = Date.now();
104
+ if (themeConfigCache && now - themeConfigCacheTime < CACHE_TTL) {
105
+ return themeConfigCache;
106
+ }
107
+
108
+ const themePath = getThemePath();
109
+ try {
110
+ if (existsSync(themePath)) {
111
+ const content = readFileSync(themePath, "utf-8");
112
+ const parsed = JSON.parse(content);
113
+ themeConfigCache = isRecord(parsed) ? parsed : {};
114
+ themeConfigCacheTime = now;
115
+ return themeConfigCache;
116
+ }
117
+ } catch (error) {
118
+ // Theme overrides are optional. If the file is unreadable or malformed,
119
+ // keep rendering with built-in defaults instead of breaking the footer.
120
+ console.debug(`[powerline-theme] Failed to load ${themePath}:`, error);
121
+ }
122
+
123
+ themeConfigCache = {};
124
+ themeConfigCacheTime = now;
125
+ return themeConfigCache;
126
+ }
127
+
128
+ function loadUserTheme(): ColorScheme {
129
+ const now = Date.now();
130
+ if (userThemeCache && now - userThemeCacheTime < CACHE_TTL) {
131
+ return userThemeCache;
132
+ }
133
+
134
+ userThemeCache = sanitizeUserThemeOverrides(loadThemeConfig().colors);
135
+ userThemeCacheTime = now;
136
+ return userThemeCache;
137
+ }
138
+
139
+ /**
140
+ * Resolve a semantic color to an actual color value
141
+ */
142
+ export function resolveColor(
143
+ semantic: SemanticColor,
144
+ presetColors?: ColorScheme
145
+ ): ColorValue {
146
+ const userTheme = loadUserTheme();
147
+
148
+ // Priority: user overrides > preset colors > defaults
149
+ return userTheme[semantic]
150
+ ?? presetColors?.[semantic]
151
+ ?? DEFAULT_COLORS[semantic];
152
+ }
153
+
154
+ /**
155
+ * Check if a color value is a hex color
156
+ */
157
+ function isHexColor(color: ColorValue): color is `#${string}` {
158
+ return typeof color === "string" && /^#[0-9a-fA-F]{6}$/.test(color);
159
+ }
160
+
161
+ /**
162
+ * Convert hex color to ANSI escape code
163
+ */
164
+ function hexToAnsi(hex: string): string {
165
+ const h = hex.replace("#", "");
166
+ const r = parseInt(h.slice(0, 2), 16);
167
+ const g = parseInt(h.slice(2, 4), 16);
168
+ const b = parseInt(h.slice(4, 6), 16);
169
+ return `\x1b[38;2;${r};${g};${b}m`;
170
+ }
171
+
172
+ /**
173
+ * Apply a color to text using the pi theme or custom hex
174
+ */
175
+ export function applyColor(
176
+ theme: ThemeLike,
177
+ color: ColorValue,
178
+ text: string
179
+ ): string {
180
+ if (isHexColor(color)) {
181
+ return `${hexToAnsi(color)}${text}\x1b[0m`;
182
+ }
183
+
184
+ try {
185
+ return theme.fg(color as ThemeColor, text);
186
+ } catch (error) {
187
+ const key = String(color);
188
+ if (!warnedInvalidThemeColors.has(key)) {
189
+ warnedInvalidThemeColors.add(key);
190
+ if (warnedInvalidThemeColors.size > 200) {
191
+ warnedInvalidThemeColors.clear();
192
+ }
193
+ console.debug(`[powerline-theme] Invalid theme color "${key}"; falling back to "text".`, error);
194
+ }
195
+ return theme.fg("text", text);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Apply a semantic color to text
201
+ */
202
+ export function fg(
203
+ theme: ThemeLike,
204
+ semantic: SemanticColor,
205
+ text: string,
206
+ presetColors?: ColorScheme
207
+ ): string {
208
+ const color = resolveColor(semantic, presetColors);
209
+ return applyColor(theme, color, text);
210
+ }
211
+
212
+ const THINKING_SEMANTIC_COLORS: Record<string, SemanticColor> = {
213
+ off: "thinkingOff",
214
+ minimal: "thinkingMinimal",
215
+ low: "thinkingLow",
216
+ medium: "thinkingMedium",
217
+ high: "thinkingHigh",
218
+ xhigh: "thinkingXhigh",
219
+ max: "thinkingMax",
220
+ };
221
+
222
+ /**
223
+ * Apply the extension's monotonic blue scale so stronger thinking levels
224
+ * become brighter and more prominent without switching to unrelated hues.
225
+ */
226
+ export function colorThinking(
227
+ theme: ThemeLike,
228
+ level: string,
229
+ text: string,
230
+ presetColors?: ColorScheme,
231
+ ): string {
232
+ const semantic = THINKING_SEMANTIC_COLORS[level] ?? "thinkingOff";
233
+ return fg(theme, semantic, text, presetColors);
234
+ }
235
+
236
+ /**
237
+ * Get the default color scheme
238
+ */
239
+ export function getDefaultColors(): Required<ColorScheme> {
240
+ return { ...DEFAULT_COLORS };
241
+ }
@@ -0,0 +1,197 @@
1
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
+
3
+ // Theme color - either a pi theme color name or a custom hex color
4
+ export type ColorValue = ThemeColor | `#${string}`;
5
+ export type ThemeLike = Pick<Theme, "fg">;
6
+
7
+ // Semantic color names for segments
8
+ export type SemanticColor =
9
+ | "model"
10
+ | "shellMode"
11
+ | "path"
12
+ | "gitDirty"
13
+ | "gitClean"
14
+ | "thinking"
15
+ | "thinkingOff"
16
+ | "thinkingMinimal"
17
+ | "thinkingLow"
18
+ | "thinkingMedium"
19
+ | "thinkingHigh"
20
+ | "thinkingXhigh"
21
+ | "thinkingMax"
22
+ | "context"
23
+ | "contextWarn"
24
+ | "contextError"
25
+ | "cost"
26
+ | "tokens"
27
+ | "separator"
28
+ | "border";
29
+
30
+ // Color scheme mapping semantic names to actual colors
31
+ export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
32
+
33
+ // Built-in segment identifiers
34
+ export type BuiltinStatusLineSegmentId =
35
+ | "model"
36
+ | "shell_mode"
37
+ | "path"
38
+ | "git"
39
+ | "subagents"
40
+ | "token_in"
41
+ | "token_out"
42
+ | "token_total"
43
+ | "cost"
44
+ | "context_pct"
45
+ | "context_total"
46
+ | "time_spent"
47
+ | "time"
48
+ | "session"
49
+ | "hostname"
50
+ | "cache_read"
51
+ | "cache_write"
52
+ | "thinking"
53
+ | "extension_statuses";
54
+
55
+ // Segment identifiers (built-in + dynamically registered custom items)
56
+ export type StatusLineSegmentId = BuiltinStatusLineSegmentId | `custom:${string}`;
57
+
58
+ // Separator styles
59
+ export type StatusLineSeparatorStyle =
60
+ | "powerline"
61
+ | "powerline-thin"
62
+ | "slash"
63
+ | "pipe"
64
+ | "block"
65
+ | "none"
66
+ | "ascii"
67
+ | "dot"
68
+ | "chevron"
69
+ | "star";
70
+
71
+ // Preset names
72
+ export type StatusLinePreset =
73
+ | "default"
74
+ | "minimal"
75
+ | "compact"
76
+ | "full"
77
+ | "nerd"
78
+ | "ascii"
79
+ | "custom";
80
+
81
+ // Per-segment options
82
+ export interface StatusLineSegmentOptions {
83
+ model?: { showThinkingLevel?: boolean };
84
+ path?: {
85
+ mode?: "basename" | "abbreviated" | "full";
86
+ maxLength?: number;
87
+ };
88
+ git?: {
89
+ showBranch?: boolean;
90
+ showStaged?: boolean;
91
+ showUnstaged?: boolean;
92
+ showUntracked?: boolean;
93
+ polling?: "full" | "branch" | "off";
94
+ };
95
+ time?: { format?: "12h" | "24h"; showSeconds?: boolean };
96
+ }
97
+
98
+ export type CustomItemPosition = "left" | "right" | "secondary";
99
+
100
+ export interface CustomStatusItem {
101
+ id: string;
102
+ statusKey: string;
103
+ position: CustomItemPosition;
104
+ color?: ColorValue;
105
+ prefix?: string;
106
+ hideWhenMissing: boolean;
107
+ excludeFromExtensionStatuses: boolean;
108
+ }
109
+
110
+ // Preset definition
111
+ export interface PresetDef {
112
+ leftSegments: BuiltinStatusLineSegmentId[];
113
+ rightSegments: BuiltinStatusLineSegmentId[];
114
+ /** Secondary row segments (shown in footer, above sub bar) */
115
+ secondarySegments?: BuiltinStatusLineSegmentId[];
116
+ separator: StatusLineSeparatorStyle;
117
+ segmentOptions?: StatusLineSegmentOptions;
118
+ /** Color scheme for this preset */
119
+ colors?: ColorScheme;
120
+ }
121
+
122
+ // Separator definition
123
+ export interface SeparatorDef {
124
+ left: string;
125
+ right: string;
126
+ endCaps?: {
127
+ left: string;
128
+ right: string;
129
+ useBgAsFg: boolean;
130
+ };
131
+ }
132
+
133
+ // Git status data
134
+ export interface GitStatus {
135
+ branch: string | null;
136
+ staged: number;
137
+ unstaged: number;
138
+ untracked: number;
139
+ }
140
+
141
+ // Usage statistics
142
+ export interface UsageStats {
143
+ input: number;
144
+ output: number;
145
+ cacheRead: number;
146
+ cacheWrite: number;
147
+ cost: number;
148
+ }
149
+
150
+ // Context passed to segment render functions
151
+ export interface SegmentContext {
152
+ // From pi-mono
153
+ model: { id: string; name?: string; reasoning?: boolean; contextWindow?: number } | undefined;
154
+ thinkingLevel: string;
155
+ sessionId: string | undefined;
156
+ cwd?: string;
157
+
158
+ // Computed
159
+ usageStats: UsageStats;
160
+ contextPercent: number | null;
161
+ contextWindow: number;
162
+ autoCompactEnabled: boolean;
163
+ customCompactionEnabled: boolean;
164
+ usingSubscription: boolean;
165
+ sessionStartTime: number;
166
+ shellModeActive: boolean;
167
+ shellRunning: boolean;
168
+ shellName: string | null;
169
+ shellCwd: string | null;
170
+
171
+ // Git
172
+ git: GitStatus;
173
+
174
+ // Extension statuses
175
+ extensionStatuses: ReadonlyMap<string, string>;
176
+ hiddenExtensionStatusKeys: ReadonlySet<string>;
177
+ customItemsById: ReadonlyMap<string, CustomStatusItem>;
178
+
179
+ // Options
180
+ options: StatusLineSegmentOptions;
181
+
182
+ // Theming
183
+ theme: ThemeLike;
184
+ colors: ColorScheme;
185
+ }
186
+
187
+ // Rendered segment output
188
+ export interface RenderedSegment {
189
+ content: string;
190
+ visible: boolean;
191
+ }
192
+
193
+ // Segment definition
194
+ export interface StatusLineSegment {
195
+ id: BuiltinStatusLineSegmentId;
196
+ render(ctx: SegmentContext): RenderedSegment;
197
+ }
@@ -0,0 +1,34 @@
1
+ export interface WelcomeDismissScheduler<Context> {
2
+ schedule(ctx: Context): void;
3
+ cancel(): void;
4
+ }
5
+
6
+ interface WelcomeDismissSchedulerOptions<Context> {
7
+ dismiss(ctx: Context): void;
8
+ getGeneration(): number;
9
+ isEnabled(): boolean;
10
+ }
11
+
12
+ export function createWelcomeDismissScheduler<Context>(
13
+ options: WelcomeDismissSchedulerOptions<Context>,
14
+ ): WelcomeDismissScheduler<Context> {
15
+ let timer: ReturnType<typeof setTimeout> | null = null;
16
+
17
+ return {
18
+ schedule(ctx) {
19
+ if (timer) return;
20
+
21
+ const generation = options.getGeneration();
22
+ timer = setTimeout(() => {
23
+ timer = null;
24
+ if (!options.isEnabled() || generation !== options.getGeneration()) return;
25
+ options.dismiss(ctx);
26
+ }, 0);
27
+ },
28
+ cancel() {
29
+ if (!timer) return;
30
+ clearTimeout(timer);
31
+ timer = null;
32
+ },
33
+ };
34
+ }