@quandev104/pi-style 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 (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -0
  4. package/dist/extensions/pi-style.js +7897 -0
  5. package/dist/extensions/pi-style.js.map +1 -0
  6. package/extension-src/pi-style/app/command-service.ts +244 -0
  7. package/extension-src/pi-style/app/commands.ts +12 -0
  8. package/extension-src/pi-style/app/config-storage.ts +98 -0
  9. package/extension-src/pi-style/app/doctor.ts +53 -0
  10. package/extension-src/pi-style/app/index.ts +322 -0
  11. package/extension-src/pi-style/app/providers.ts +268 -0
  12. package/extension-src/pi-style/app/render-scheduler.ts +48 -0
  13. package/extension-src/pi-style/app/runtime.ts +404 -0
  14. package/extension-src/pi-style/app/snapshot.ts +15 -0
  15. package/extension-src/pi-style/domain/capabilities.ts +34 -0
  16. package/extension-src/pi-style/domain/config-authorization.ts +20 -0
  17. package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
  18. package/extension-src/pi-style/domain/config-diff.ts +30 -0
  19. package/extension-src/pi-style/domain/config-migrations.ts +52 -0
  20. package/extension-src/pi-style/domain/config-normalization.ts +567 -0
  21. package/extension-src/pi-style/domain/config-presets.ts +51 -0
  22. package/extension-src/pi-style/domain/config-types.ts +115 -0
  23. package/extension-src/pi-style/domain/providers.ts +37 -0
  24. package/extension-src/pi-style/domain/status-presets.ts +60 -0
  25. package/extension-src/pi-style/domain/status-renderer.ts +142 -0
  26. package/extension-src/pi-style/domain/status.ts +430 -0
  27. package/extension-src/pi-style/domain/theme.ts +232 -0
  28. package/extension-src/pi-style/features/.gitkeep +0 -0
  29. package/extension-src/pi-style/features/editor/index.ts +304 -0
  30. package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
  31. package/extension-src/pi-style/features/messages/index.ts +145 -0
  32. package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
  33. package/extension-src/pi-style/features/startup/index.ts +564 -0
  34. package/extension-src/pi-style/features/startup/logo.ts +151 -0
  35. package/extension-src/pi-style/features/status-line/index.ts +319 -0
  36. package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
  37. package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
  38. package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
  39. package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
  40. package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
  41. package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
  42. package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
  43. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
  44. package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
  45. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
  46. package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
  47. package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
  48. package/extension-src/pi-style/features/tools/index.ts +480 -0
  49. package/extension-src/pi-style/pi/commands.ts +17 -0
  50. package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
  51. package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
  52. package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
  53. package/extension-src/pi-style/pi/config-host.ts +52 -0
  54. package/extension-src/pi-style/pi/config-session.ts +105 -0
  55. package/extension-src/pi-style/pi/index.ts +77 -0
  56. package/extension-src/pi-style/pi/operational-state.ts +29 -0
  57. package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
  58. package/extension-src/pi-style/pi/session-usage.ts +62 -0
  59. package/extension-src/pi-style/pi/startup-resources.ts +70 -0
  60. package/extension-src/pi-style/shared/.gitkeep +0 -0
  61. package/extension-src/pi-style/shared/ansi.ts +386 -0
  62. package/extension-src/pi-style/shared/box.ts +805 -0
  63. package/extension-src/pi-style/shared/disposable-store.ts +28 -0
  64. package/extension-src/pi-style/shared/elapsed.ts +88 -0
  65. package/extension-src/pi-style/shared/render-budget.ts +367 -0
  66. package/extension-src/pi-style/shared/split-diff.ts +692 -0
  67. package/extension-src/pi-style/shared/theme-extras.ts +292 -0
  68. package/package.json +68 -0
  69. package/themes/.gitkeep +0 -0
@@ -0,0 +1,187 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
3
+ import type { ConfigFilePort } from "../app/config-storage.js";
4
+ import { createPiStyleApp, type PiStyleApp } from "../app/index.js";
5
+ import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
6
+ import { setToolsRenderConfig } from "../features/tools/boxed/session-config.js";
7
+ import { createCompatibilityCoordinator } from "./compatibility-coordinator.js";
8
+ import {
9
+ type CompatibilityCleanupResult,
10
+ type CompatibilityProbeReport,
11
+ disposePiCompatibilityProbe,
12
+ } from "./compatibility-probe.js";
13
+ import { createPiConfigFilePort, defaultStoragePaths } from "./config-host.js";
14
+ import { createConfigSourceAdapter, readSessionAuthorization } from "./config-session.js";
15
+ import { buildOperationalState } from "./operational-state.js";
16
+ import { collectToolDetails } from "./startup-resources.js";
17
+
18
+ export type CompatibilityTestHooks = {
19
+ dispose?: (report: CompatibilityProbeReport) => CompatibilityCleanupResult;
20
+ filePort?: ConfigFilePort;
21
+ paths?: (cwd: string) => { globalPath: string; projectPath: string };
22
+ /** Test-only capability seam; Pi's ExtensionContext does not provide a Git runner. */
23
+ gitRunner?: import("../domain/providers.js").GitCommandRunner;
24
+ /** Override the thinking-cycle key binding consumed to suppress Pi's status toast. */
25
+ thinkingCycleKey?: string;
26
+ };
27
+
28
+ const THINKING_CYCLE = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
29
+
30
+ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: CompatibilityTestHooks = {}) {
31
+ const filePort = hooks.filePort ?? createPiConfigFilePort();
32
+ const gitRunner =
33
+ hooks.gitRunner ??
34
+ ({
35
+ run: async (args, commandCwd, timeoutMs, signal) => {
36
+ const result = await pi.exec("git", [...args], {
37
+ cwd: commandCwd,
38
+ timeout: timeoutMs,
39
+ ...(signal ? { signal } : {}),
40
+ });
41
+ return { stdout: result.stdout, stderr: result.stderr, code: result.code };
42
+ },
43
+ } satisfies import("../domain/providers.js").GitCommandRunner);
44
+ let cwd = process.cwd();
45
+ let active = false;
46
+ let tuiSession = false;
47
+ let terminalInputUnsubscribe: (() => void) | undefined;
48
+ const source = createConfigSourceAdapter(
49
+ pi,
50
+ filePort,
51
+ hooks.paths ?? ((sessionCwd) => defaultStoragePaths(sessionCwd)),
52
+ );
53
+ const compatibility = createCompatibilityCoordinator(
54
+ (report) => hooks.dispose?.(report) ?? disposePiCompatibilityProbe(report),
55
+ );
56
+ // Initial read may be empty in real Pi (flag values apply after extension load);
57
+ // start() re-reads and re-captures at every session_start.
58
+ let authorization = readSessionAuthorization(pi);
59
+ let productGate: "omitted" | "allow" | "deny" = "omitted";
60
+ const syncOperational = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
61
+ app.setOperationalState(
62
+ buildOperationalState(
63
+ config,
64
+ authorization,
65
+ compatibility,
66
+ app.runtime.current?.installationState,
67
+ app.productPolicy.corePatchGate,
68
+ ),
69
+ );
70
+ };
71
+ const app: PiStyleApp = createPiStyleApp(
72
+ undefined,
73
+ {
74
+ load: async (trusted) => {
75
+ source.setSession(cwd, trusted);
76
+ return source.load();
77
+ },
78
+ },
79
+ (config) => {
80
+ productGate = app.productPolicy.corePatchGate;
81
+ if (!active) return;
82
+ if (compatibility.report) {
83
+ const cleanup = compatibility.dispose();
84
+ if (!cleanup.complete) {
85
+ syncOperational(config);
86
+ return;
87
+ }
88
+ }
89
+ compatibility.install(config, tuiSession, productGate);
90
+ syncOperational(config);
91
+ },
92
+ );
93
+
94
+ return {
95
+ app,
96
+ async start(event: { reason: string }, ctx: ExtensionContext): Promise<void> {
97
+ // Authorization is session-bound: Pi applies extension flag values only after
98
+ // extension modules finish loading, so flags must be read here (session_start),
99
+ // never at coordinator creation time.
100
+ authorization = readSessionAuthorization(pi);
101
+ compatibility.captureAuthorization(
102
+ authorization.core,
103
+ authorization.user,
104
+ authorization.assistant,
105
+ authorization.specialBlocks,
106
+ authorization.tools,
107
+ authorization.ascii,
108
+ );
109
+ // Replacement is failure-atomic: retain the current runtime until
110
+ // compatibility ownership has been restored successfully.
111
+ if (compatibility.report) {
112
+ const cleanup = compatibility.dispose();
113
+ if (!cleanup.complete) {
114
+ syncOperational(app.config);
115
+ return;
116
+ }
117
+ }
118
+ if (app.runtime.current) app.sessionShutdown();
119
+ cwd = ctx.cwd ?? process.cwd();
120
+ tuiSession = ctx.mode === "tui";
121
+ app.setProjectTrusted(ctx.isProjectTrusted());
122
+ source.setSession(cwd, ctx.isProjectTrusted());
123
+ active = false;
124
+ await app.reload();
125
+ productGate = app.productPolicy.corePatchGate;
126
+ active = true;
127
+ compatibility.install(app.config, ctx.mode === "tui", productGate);
128
+ // Session-scoped render configuration for the boxed tool/message surfaces.
129
+ // Populated once per session (never inside render).
130
+ setToolsRenderConfig(app.config.tools);
131
+ if (ctx.ui?.theme) setSpecialBlockTheme(ctx.ui.theme as never);
132
+ const toolDetails = collectToolDetails(pi.getActiveTools?.(), pi.getAllTools?.());
133
+ app.sessionStart(
134
+ {
135
+ mode: ctx.mode,
136
+ hasUI: ctx.hasUI,
137
+ ...(ctx.ui ? { ui: ctx.ui } : {}),
138
+ ...(ctx.cwd ? { cwd: ctx.cwd } : {}),
139
+ ...(ctx.model
140
+ ? {
141
+ model: {
142
+ id: ctx.model.id,
143
+ name: ctx.model.name,
144
+ provider: ctx.model.provider,
145
+ reasoning: ctx.model.reasoning,
146
+ },
147
+ }
148
+ : {}),
149
+ ...(ctx.thinkingLevel ? { thinkingLevel: ctx.thinkingLevel } : {}),
150
+ getContextUsage: ctx.getContextUsage,
151
+ projectTrusted: ctx.isProjectTrusted(),
152
+ gitRunner,
153
+ },
154
+ event.reason as "startup" | "reload" | "new" | "resume" | "fork",
155
+ {
156
+ ...(typeof ctx.getSystemPrompt === "function" ? { systemPrompt: ctx.getSystemPrompt() } : {}),
157
+ ...(toolDetails ? { toolDetails } : {}),
158
+ ...(ctx.scopedModels && ctx.scopedModels.length > 0 ? { models: ctx.scopedModels.length } : {}),
159
+ },
160
+ );
161
+ terminalInputUnsubscribe?.();
162
+ terminalInputUnsubscribe = undefined;
163
+ // Consume Pi's default thinking-cycle key and re-issue it through the public API so
164
+ // the footer shows the level without Pi's transient "Thinking level: X" status toast.
165
+ const cycleKey = hooks.thinkingCycleKey ?? "shift+tab";
166
+ if (ctx.mode === "tui" && ctx.ui?.onTerminalInput) {
167
+ terminalInputUnsubscribe = ctx.ui.onTerminalInput((data) => {
168
+ if (!matchesKey(data, cycleKey as KeyId)) return undefined;
169
+ const current = pi.getThinkingLevel?.();
170
+ const index = Math.max(0, THINKING_CYCLE.indexOf(current as (typeof THINKING_CYCLE)[number]));
171
+ const next = THINKING_CYCLE[(index + 1) % THINKING_CYCLE.length];
172
+ pi.setThinkingLevel?.(next as never);
173
+ return { consume: true };
174
+ });
175
+ }
176
+ syncOperational(app.config);
177
+ },
178
+ shutdown(): void {
179
+ active = false;
180
+ tuiSession = false;
181
+ terminalInputUnsubscribe?.();
182
+ terminalInputUnsubscribe = undefined;
183
+ app.sessionShutdown();
184
+ compatibility.dispose();
185
+ },
186
+ };
187
+ }
@@ -0,0 +1,62 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import type { UsageSnapshot } from "../domain/status.js";
3
+
4
+ /**
5
+ * Minimal structural view of Pi's session manager: only the entry iteration
6
+ * needed to aggregate cumulative usage. `ExtensionContext.sessionManager`
7
+ * satisfies this shape.
8
+ */
9
+ export interface SessionUsageSource {
10
+ getEntries(): readonly SessionEntry[];
11
+ }
12
+
13
+ interface UsageTotals {
14
+ input: number;
15
+ output: number;
16
+ cacheRead: number;
17
+ cacheWrite: number;
18
+ cost: number;
19
+ }
20
+
21
+ /**
22
+ * Aggregate cumulative token/cost usage from all session entries, mirroring
23
+ * Pi's native footer: assistant messages always carry usage, tool results and
24
+ * compaction/branch summaries carry it when available.
25
+ */
26
+ export function usageFromSession(session: SessionUsageSource): UsageSnapshot | undefined {
27
+ const totals: UsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
28
+ let sawUsage = false;
29
+ for (const entry of session.getEntries()) {
30
+ if (entry.type === "message") {
31
+ if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") continue;
32
+ const usage = "usage" in entry.message ? entry.message.usage : undefined;
33
+ if (!usage) continue;
34
+ addUsage(totals, usage);
35
+ sawUsage = true;
36
+ } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
37
+ addUsage(totals, entry.usage);
38
+ sawUsage = true;
39
+ }
40
+ }
41
+ if (!sawUsage) return undefined;
42
+ return {
43
+ inputTokens: totals.input,
44
+ outputTokens: totals.output,
45
+ cacheReadTokens: totals.cacheRead,
46
+ cacheWriteTokens: totals.cacheWrite,
47
+ cost: totals.cost,
48
+ subscriptionMode: "unknown",
49
+ streaming: false,
50
+ };
51
+ }
52
+
53
+ function addUsage(
54
+ totals: UsageTotals,
55
+ usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: { total: number } },
56
+ ): void {
57
+ totals.input += usage.input;
58
+ totals.output += usage.output;
59
+ totals.cacheRead += usage.cacheRead;
60
+ totals.cacheWrite += usage.cacheWrite;
61
+ totals.cost += usage.cost.total;
62
+ }
@@ -0,0 +1,70 @@
1
+ import type { SourceInfo, ToolInfo } from "@earendil-works/pi-coding-agent";
2
+
3
+ /**
4
+ * Startup resource collection for the pi/ adapter layer. Runs once at session
5
+ * start, outside any render path, and produces the bounded tool details that
6
+ * the startup surface renders.
7
+ */
8
+
9
+ const CORE_TOOL_SOURCE_LABEL = "core";
10
+
11
+ function stripKnownExtension(name: string): string {
12
+ return name.replace(/\.(?:mjs|cjs|js|jsx|ts|tsx)$/i, "");
13
+ }
14
+
15
+ function compactSourcePathLabel(path: string): string {
16
+ const trimmed = path.trim();
17
+ if (!trimmed) return "";
18
+ const synthetic = /^<([^:>]+)(?::[^>]*)?>$/.exec(trimmed);
19
+ if (synthetic?.[1]) return synthetic[1];
20
+ const segments = trimmed
21
+ .replace(/\\/g, "/")
22
+ .split("/")
23
+ .filter((segment) => segment.length > 0 && segment !== "." && segment !== "~");
24
+ const last = segments.at(-1) ?? trimmed;
25
+ if (/^index\.(?:mjs|cjs|js|jsx|ts|tsx)$/i.test(last) && segments.length > 1) {
26
+ return segments[segments.length - 2] ?? last;
27
+ }
28
+ return stripKnownExtension(last);
29
+ }
30
+
31
+ function compactPackageSourceLabel(source: string): string {
32
+ if (source.startsWith("npm:")) return source.slice("npm:".length) || source;
33
+ if (source.startsWith("git:")) return compactSourcePathLabel(source.replace(/\.git(?:#.*)?$/i, "")) || source;
34
+ return source;
35
+ }
36
+
37
+ /** Compact display label for a tool's source. */
38
+ export function toolSourceLabel(sourceInfo: SourceInfo | undefined): string {
39
+ if (!sourceInfo || typeof sourceInfo !== "object") return CORE_TOOL_SOURCE_LABEL;
40
+ const source = typeof sourceInfo.source === "string" ? sourceInfo.source : "";
41
+ if (source === "builtin") return CORE_TOOL_SOURCE_LABEL;
42
+ if (source === "sdk") return "sdk";
43
+ if (source.startsWith("npm:") || source.startsWith("git:")) return compactPackageSourceLabel(source);
44
+ const baseDir = typeof sourceInfo.baseDir === "string" ? sourceInfo.baseDir : "";
45
+ if (baseDir) return compactSourcePathLabel(baseDir) || source || "extension";
46
+ const path = typeof sourceInfo.path === "string" ? sourceInfo.path : "";
47
+ if (path) return compactSourcePathLabel(path) || source || "extension";
48
+ return source || "extension";
49
+ }
50
+
51
+ /**
52
+ * Map registered tools to the bounded `{ source, name }` shape used by the
53
+ * startup tools panel. When Pi reports active tool names, only those are kept;
54
+ * an empty/absent active list keeps all registered tools so the panel is not
55
+ * silently empty. Returns undefined when there is nothing to show.
56
+ */
57
+ export function collectToolDetails(
58
+ activeNames: readonly string[] | undefined,
59
+ tools: readonly ToolInfo[] | undefined,
60
+ ): { source: string; name: string }[] | undefined {
61
+ if (!tools || tools.length === 0) return undefined;
62
+ const activeSet = activeNames && activeNames.length > 0 ? new Set(activeNames) : undefined;
63
+ const details: { source: string; name: string }[] = [];
64
+ for (const tool of tools) {
65
+ if (typeof tool?.name !== "string" || tool.name.trim().length === 0) continue;
66
+ if (activeSet && !activeSet.has(tool.name)) continue;
67
+ details.push({ source: toolSourceLabel(tool.sourceInfo), name: tool.name.trim() });
68
+ }
69
+ return details.length > 0 ? details : undefined;
70
+ }
File without changes