shariq-pi-extensions 0.1.0
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 +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ANSI helpers for Context Usage.
|
|
2
|
+
// This directory has no index.ts on purpose: pi's extension discovery skips it.
|
|
3
|
+
|
|
4
|
+
/** Matches one OSC sequence (hyperlink/title). The payload can never contain ESC — ESC
|
|
5
|
+
* starts the ST terminator — so excluding it keeps the match from greedily swallowing
|
|
6
|
+
* the visible text between an ST-terminated OSC 8 open and its close. */
|
|
7
|
+
export const OSC_SEQUENCE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
8
|
+
|
|
9
|
+
/** Strip CSI (colors/cursor) and OSC (hyperlink/title) escape sequences. */
|
|
10
|
+
export function stripAnsi(text: string): string {
|
|
11
|
+
return text
|
|
12
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
13
|
+
.replace(OSC_SEQUENCE, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Index of the last char of the escape sequence starting at `i`, or undefined. */
|
|
17
|
+
export function ansiEndIndex(line: string, i: number): number | undefined {
|
|
18
|
+
if (line[i] !== "\x1b") return undefined;
|
|
19
|
+
if (line[i + 1] === "[") {
|
|
20
|
+
const end = line.slice(i).search(/[A-Za-z~]/);
|
|
21
|
+
return end >= 0 ? i + end : undefined;
|
|
22
|
+
}
|
|
23
|
+
if (line[i + 1] === "]") {
|
|
24
|
+
const bel = line.indexOf("\x07", i + 2);
|
|
25
|
+
const st = line.indexOf("\x1b\\", i + 2);
|
|
26
|
+
const end = bel === -1 ? st : st === -1 ? bel : Math.min(bel, st);
|
|
27
|
+
return end >= 0 ? end + (line[end] === "\x1b" ? 1 : 0) : undefined;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Raw string index of the visible character at visible index `target` (escape-aware). */
|
|
33
|
+
export function rawIndexAtVisibleIndex(line: string, target: number): number {
|
|
34
|
+
let visible = 0;
|
|
35
|
+
for (let i = 0; i < line.length; i++) {
|
|
36
|
+
const ansiEnd = ansiEndIndex(line, i);
|
|
37
|
+
if (ansiEnd !== undefined) {
|
|
38
|
+
i = ansiEnd;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (visible === target) return i;
|
|
42
|
+
visible++;
|
|
43
|
+
}
|
|
44
|
+
return line.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Like rawIndexAtVisibleIndex, but lands *before* any escapes preceding the char. */
|
|
48
|
+
export function rawIndexBeforeVisibleIndex(line: string, target: number): number {
|
|
49
|
+
let visible = 0;
|
|
50
|
+
for (let i = 0; i < line.length; i++) {
|
|
51
|
+
if (visible === target) return i;
|
|
52
|
+
const ansiEnd = ansiEndIndex(line, i);
|
|
53
|
+
if (ansiEnd !== undefined) {
|
|
54
|
+
i = ansiEnd;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
visible++;
|
|
58
|
+
}
|
|
59
|
+
return line.length;
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Boundary helpers for values that enter Context Usage from JSON, Pi internals,
|
|
2
|
+
// provider payloads, or other untyped seams. Keep generic uncertainty here or in
|
|
3
|
+
// domain-named parsers, not in rendering/core logic.
|
|
4
|
+
|
|
5
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
6
|
+
export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
|
|
7
|
+
export type JsonObject = { [key: string]: JsonValue };
|
|
8
|
+
|
|
9
|
+
export function isJsonObject(value: unknown): value is JsonObject {
|
|
10
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function jsonObjectOrEmpty(value: unknown): JsonObject {
|
|
14
|
+
return isJsonObject(value) ? value : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function stringValue(value: unknown): string | undefined {
|
|
18
|
+
return typeof value === "string" ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function positiveNumberValue(value: unknown): number | undefined {
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function booleanValue(value: unknown): boolean | undefined {
|
|
26
|
+
return typeof value === "boolean" ? value : undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Shared structural detection of pi's chat tree. All detection is duck-typed
|
|
2
|
+
// against extension-visible component shapes and contract-tested against the
|
|
3
|
+
// installed pi (tests/contract/pi-internals.test.ts), never imported from
|
|
4
|
+
// pi internals — that is what lets the family survive `pi update`.
|
|
5
|
+
import { stripAnsi } from "./ansi.ts";
|
|
6
|
+
|
|
7
|
+
export interface ToolArgsLike {
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
path?: unknown;
|
|
10
|
+
file_path?: unknown;
|
|
11
|
+
content?: unknown;
|
|
12
|
+
command?: unknown;
|
|
13
|
+
offset?: unknown;
|
|
14
|
+
limit?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ToolResultDetailsLike {
|
|
18
|
+
diff?: unknown;
|
|
19
|
+
patch?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ToolResultLike {
|
|
23
|
+
isError?: unknown;
|
|
24
|
+
content?: unknown;
|
|
25
|
+
details?: ToolResultDetailsLike;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ToolCallPreviewLike {
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
error?: unknown;
|
|
31
|
+
diff?: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ToolCallRendererLike {
|
|
35
|
+
render?: (width: number) => unknown;
|
|
36
|
+
preview?: ToolCallPreviewLike;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ToolRowDataLike {
|
|
40
|
+
toolName?: unknown;
|
|
41
|
+
args?: ToolArgsLike;
|
|
42
|
+
result?: ToolResultLike;
|
|
43
|
+
isPartial?: unknown;
|
|
44
|
+
/** pi's per-row expansion flag: written by setExpanded(), read for the zoom ladder (design language §9.12). */
|
|
45
|
+
expanded?: unknown;
|
|
46
|
+
cwd?: unknown;
|
|
47
|
+
callRendererComponent?: ToolCallRendererLike;
|
|
48
|
+
__tracelineWriteSnapshot?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ToolRowLike extends ToolRowDataLike {
|
|
52
|
+
render: (width: number) => unknown;
|
|
53
|
+
setExpanded: (...args: unknown[]) => unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AssistantMessageLike {
|
|
57
|
+
content?: unknown;
|
|
58
|
+
role?: unknown;
|
|
59
|
+
timestamp?: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AssistantRowDataLike {
|
|
63
|
+
hideThinkingBlock?: unknown;
|
|
64
|
+
lastMessage?: AssistantMessageLike;
|
|
65
|
+
hiddenThinkingLabel?: unknown;
|
|
66
|
+
render?: (width: number) => unknown;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AssistantRowLike extends AssistantRowDataLike {
|
|
70
|
+
hideThinkingBlock: boolean;
|
|
71
|
+
setHideThinkingBlock: (...args: unknown[]) => unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ToolRowPrototypeLike extends Partial<ToolRowLike> {
|
|
75
|
+
__tracelineWriteSnapshotPatchVersion?: number;
|
|
76
|
+
__tracelineOriginalSetArgsComplete?: (...args: unknown[]) => unknown;
|
|
77
|
+
setArgsComplete?: (...args: unknown[]) => unknown;
|
|
78
|
+
__tracelineOriginalMarkExecutionStarted?: (...args: unknown[]) => unknown;
|
|
79
|
+
markExecutionStarted?: (...args: unknown[]) => unknown;
|
|
80
|
+
__tracelineOriginalRender?: (width: number) => unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AssistantRowPrototypeLike extends Partial<AssistantRowLike> {
|
|
84
|
+
__tracelineAssistantPatchVersion?: number;
|
|
85
|
+
__tracelineOriginalAssistantRender?: (width: number) => unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface TracelineTuiLike {
|
|
89
|
+
requestRender: (force?: boolean) => unknown;
|
|
90
|
+
__tracelineRRWrapVersion?: number;
|
|
91
|
+
__tracelineOriginalRequestRender?: (force?: boolean) => unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A tool execution row: has setExpanded() and a toolName property. */
|
|
95
|
+
export function isToolRow(component: unknown): component is ToolRowLike {
|
|
96
|
+
if (!component || typeof component !== "object") return false;
|
|
97
|
+
const c = component as { constructor?: { name?: string }; render?: unknown; setExpanded?: unknown };
|
|
98
|
+
if (c.constructor?.name === "ToolExecutionComponent") return true;
|
|
99
|
+
return typeof c.render === "function" && typeof c.setExpanded === "function" && "toolName" in c;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** An assistant message row: carries the hideThinkingBlock toggle. */
|
|
103
|
+
export function isAssistantRow(component: unknown): component is AssistantRowLike {
|
|
104
|
+
const c = component as { setHideThinkingBlock?: unknown; hideThinkingBlock?: unknown };
|
|
105
|
+
return !!c && typeof c.setHideThinkingBlock === "function" && typeof c.hideThinkingBlock === "boolean";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ContainerLike {
|
|
109
|
+
children: unknown[];
|
|
110
|
+
addChild?: (child: unknown) => void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Depth-first search for the container whose direct children satisfy `predicate`. */
|
|
114
|
+
export function findContainerBy(
|
|
115
|
+
node: unknown,
|
|
116
|
+
predicate: (children: unknown[]) => boolean,
|
|
117
|
+
seen = new Set<unknown>(),
|
|
118
|
+
): ContainerLike | undefined {
|
|
119
|
+
if (!node || typeof node !== "object" || seen.has(node)) return undefined;
|
|
120
|
+
seen.add(node);
|
|
121
|
+
const children = (node as { children?: unknown[] }).children;
|
|
122
|
+
if (Array.isArray(children)) {
|
|
123
|
+
if (predicate(children)) return node as unknown as ContainerLike;
|
|
124
|
+
for (const child of children) {
|
|
125
|
+
const found = findContainerBy(child, predicate, seen);
|
|
126
|
+
if (found) return found;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The startup resource listing pi renders at session start: leaf ExpandableText
|
|
133
|
+
// sections whose first line is a [Section] header. Since pi 0.80 it lives in a
|
|
134
|
+
// loadedResourcesContainer rendered immediately before the chat container, making it
|
|
135
|
+
// the one reliable pre-rows landmark for finding that container in a fresh session
|
|
136
|
+
// (the welcome/changelog block is gated on updates). Same seam Context Usage has
|
|
137
|
+
// anchored on since v1.
|
|
138
|
+
export const RESOURCE_HEADER_RE = /^\s*\[(Context|Skills|Prompts|Extensions|Themes)\]/m;
|
|
139
|
+
|
|
140
|
+
/** A single startup resource section: a leaf component whose render carries a [Section] header. */
|
|
141
|
+
export function isResourceRow(component: unknown): boolean {
|
|
142
|
+
const c = component as { render?: (width: number) => string[]; children?: unknown };
|
|
143
|
+
if (!c || typeof c.render !== "function" || Array.isArray(c.children)) return false;
|
|
144
|
+
try {
|
|
145
|
+
return RESOURCE_HEADER_RE.test(stripAnsi(c.render(200).join("\n")));
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* In pi 0.80.x, loaded resources moved out of chatContainer into a sibling
|
|
153
|
+
* loadedResourcesContainer that is rendered immediately before chatContainer. Use that
|
|
154
|
+
* ordering as the fresh-session fallback when there are no assistant/tool rows yet.
|
|
155
|
+
*/
|
|
156
|
+
function findChatContainerAfterResourceContainer(node: unknown, seen = new Set<unknown>()): ContainerLike | undefined {
|
|
157
|
+
if (!node || typeof node !== "object" || seen.has(node)) return undefined;
|
|
158
|
+
seen.add(node);
|
|
159
|
+
|
|
160
|
+
const children = (node as { children?: unknown[] }).children;
|
|
161
|
+
if (!Array.isArray(children)) return undefined;
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < children.length - 1; i++) {
|
|
164
|
+
const child = children[i];
|
|
165
|
+
const next = children[i + 1];
|
|
166
|
+
if (child && typeof child === "object" && next && typeof next === "object") {
|
|
167
|
+
const childChildren = (child as { children?: unknown[] }).children;
|
|
168
|
+
const nextChildren = (next as { children?: unknown[] }).children;
|
|
169
|
+
if (Array.isArray(childChildren) && Array.isArray(nextChildren) && childChildren.some((c) => isResourceRow(c))) {
|
|
170
|
+
return next as ContainerLike;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const child of children) {
|
|
176
|
+
const found = findChatContainerAfterResourceContainer(child, seen);
|
|
177
|
+
if (found) return found;
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** pi's chat container: the one whose direct children include the chat rows — or, in a
|
|
183
|
+
* fresh session with no rows yet, the empty sibling immediately after the startup
|
|
184
|
+
* resource listing (pi 0.80+). */
|
|
185
|
+
export function findChatContainer(node: unknown): ContainerLike | undefined {
|
|
186
|
+
return (
|
|
187
|
+
findContainerBy(node, (children) => children.some((c) => isToolRow(c) || isAssistantRow(c))) ??
|
|
188
|
+
findChatContainerAfterResourceContainer(node)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Shared config-file convention for Context Usage:
|
|
2
|
+
// user-level agent config, then project-level Pi config.
|
|
3
|
+
|
|
4
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export function expandHomePath(filePath: string): string {
|
|
10
|
+
if (filePath === "~") return homedir();
|
|
11
|
+
if (filePath.startsWith("~/")) return join(homedir(), filePath.slice(2));
|
|
12
|
+
return filePath;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read JSON config through a domain parser; undefined on absence or any parse error. */
|
|
16
|
+
export function readJsonConfig<T>(filePath: string, parse: (value: unknown) => T): T | undefined {
|
|
17
|
+
try {
|
|
18
|
+
const expanded = expandHomePath(filePath);
|
|
19
|
+
if (!existsSync(expanded)) return undefined;
|
|
20
|
+
const parsed: unknown = JSON.parse(readFileSync(expanded, "utf8"));
|
|
21
|
+
return parse(parsed);
|
|
22
|
+
} catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Standard lookup order: user file first, project file second (later overrides). */
|
|
28
|
+
export function configPaths(name: string, cwd: string): string[] {
|
|
29
|
+
return [join(getAgentDir(), `${name}.json`), join(cwd, CONFIG_DIR_NAME, `${name}.json`)];
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Shared number formatting for Context Usage.
|
|
2
|
+
// (docs/design-language.md §4). One unit, everywhere: counts render in fixed
|
|
3
|
+
// one-decimal k-units (0.1k, 1.2k, 52.3k) so magnitudes compare down a column —
|
|
4
|
+
// raw integers never appear. M is the only step-up, at ≥1M, where k becomes
|
|
5
|
+
// unreadable (9.1M, not 9100.0k). Char counts read `0.4k ch`.
|
|
6
|
+
|
|
7
|
+
export function compactCount(value: number): string {
|
|
8
|
+
if (!Number.isFinite(value)) return "?";
|
|
9
|
+
const rounded = Math.round(value);
|
|
10
|
+
if (Math.abs(rounded) >= 999_950) return `${(rounded / 1_000_000).toFixed(1)}M`;
|
|
11
|
+
return `${(rounded / 1000).toFixed(1)}k`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatChars(value: number): string {
|
|
15
|
+
return `${compactCount(value)} ch`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Token counts wear the estimate marker unless provider-reported: the ~/no-~
|
|
20
|
+
* distinction is semantic, never stylistic (design language §4).
|
|
21
|
+
*/
|
|
22
|
+
export function formatTokens(value: number, options: { exact?: boolean } = {}): string {
|
|
23
|
+
return `${options.exact ? "" : "~"}${compactCount(value)} tokens`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Money: two decimals; three below $0.10, where the third digit is significant. */
|
|
27
|
+
export function formatUsd(value: number): string {
|
|
28
|
+
return value >= 0.10 ? `$${value.toFixed(2)}` : `$${value.toFixed(3)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Latency durations (design language §4): below 10s the first decimal is significant
|
|
32
|
+
* at first-token scale (`1.9s`, `9.6s`); from 10s the compact mixed-unit grammar
|
|
33
|
+
* resumes. */
|
|
34
|
+
export function formatLatency(ms: number): string {
|
|
35
|
+
const seconds = Math.max(0, ms) / 1000;
|
|
36
|
+
if (seconds < 9.95) return `${seconds.toFixed(1)}s`;
|
|
37
|
+
return formatDuration(ms);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Rates (design language §4): integer tok/s; `~` when estimated from streamed chars,
|
|
41
|
+
* none when derived from provider usage. The ~/no-~ distinction is semantic. */
|
|
42
|
+
export function formatRate(tokensPerSecond: number, options: { exact?: boolean } = {}): string {
|
|
43
|
+
if (!Number.isFinite(tokensPerSecond) || tokensPerSecond < 0) return "?";
|
|
44
|
+
return `${options.exact ? "" : "~"}${Math.round(tokensPerSecond)} tok/s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Durations: compact mixed units, no spaces — 14s, 2m41s, 9h50m. */
|
|
48
|
+
export function formatDuration(ms: number): string {
|
|
49
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
50
|
+
if (seconds < 60) return `${seconds}s`;
|
|
51
|
+
const minutes = Math.floor(seconds / 60);
|
|
52
|
+
if (minutes < 60) {
|
|
53
|
+
const rest = seconds % 60;
|
|
54
|
+
return rest > 0 ? `${minutes}m${rest.toString().padStart(2, "0")}s` : `${minutes}m`;
|
|
55
|
+
}
|
|
56
|
+
const hours = Math.floor(minutes / 60);
|
|
57
|
+
const restMinutes = minutes % 60;
|
|
58
|
+
return restMinutes > 0 ? `${hours}h${restMinutes}m` : `${hours}h`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Context Usage terminal styling.
|
|
2
|
+
// Identity lives in glyphs and layout, not colour: all ink is theme-derived through
|
|
3
|
+
// ink(), with raw-ANSI fallbacks only for surfaces rendered before a Theme handle
|
|
4
|
+
// exists and for the one tone pi's theme has no faithful role for ("running").
|
|
5
|
+
|
|
6
|
+
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { OSC_SEQUENCE, rawIndexAtVisibleIndex, stripAnsi } from "./ansi.ts";
|
|
10
|
+
|
|
11
|
+
/** Line-kind glyphs: one per line, gutter position (design language §1). */
|
|
12
|
+
export const GLYPH = {
|
|
13
|
+
/** a tool action (one trace row) — traceline */
|
|
14
|
+
tool: "\u203a", // ›
|
|
15
|
+
/** a loop-economics fact (clock, notice, ledger line) — cachemire */
|
|
16
|
+
econ: "\u25cd", // ◍
|
|
17
|
+
/** an expandable/summarizable section header — Context Usage */
|
|
18
|
+
section: "\u25b8", // ▸
|
|
19
|
+
/** tool-block rail — the dim left edge that fuses a run of trace rows into one block */
|
|
20
|
+
rail: "\u258f", // ▏
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/** Status scale — family-shared vocabulary (design language §1). */
|
|
24
|
+
export const SCALE = {
|
|
25
|
+
cold: "\u25cb", // ○ cold / empty
|
|
26
|
+
hit: "\u25cf", // ● hit / full
|
|
27
|
+
partial: "\u25d1", // ◑ partial
|
|
28
|
+
miss: "\u25cc", // ◌ miss / broken
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/** The only inline fact separator (design language §5). */
|
|
32
|
+
export const SEP = " \u00b7 "; // ·
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ink tones (design language §2): the neutral ramp (text/muted/dim), the status
|
|
36
|
+
* tones (success/warning/error/running), and the family accent.
|
|
37
|
+
*/
|
|
38
|
+
export type Tone =
|
|
39
|
+
| "text"
|
|
40
|
+
| "muted"
|
|
41
|
+
| "dim"
|
|
42
|
+
| "success"
|
|
43
|
+
| "warning"
|
|
44
|
+
| "error"
|
|
45
|
+
| "running"
|
|
46
|
+
| "accent";
|
|
47
|
+
|
|
48
|
+
const THEME_ROLE: Partial<Record<Tone, ThemeColor>> = {
|
|
49
|
+
text: "text",
|
|
50
|
+
muted: "muted",
|
|
51
|
+
dim: "dim",
|
|
52
|
+
success: "success",
|
|
53
|
+
warning: "warning",
|
|
54
|
+
error: "error",
|
|
55
|
+
accent: "accent",
|
|
56
|
+
// "running" has no faithful theme role (accent would collide with brand/total
|
|
57
|
+
// highlights; warning overloads "fading") — resolved to the ANSI-blue raw tone
|
|
58
|
+
// (design language §2).
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const RESET = "\x1b[0m";
|
|
62
|
+
|
|
63
|
+
// Raw fallbacks: basic ANSI only (widest terminal support), used when no Theme is
|
|
64
|
+
// available yet. "text" and "accent" fall back to uncoloured text — plain is honest
|
|
65
|
+
// before the theme exists.
|
|
66
|
+
const RAW: Record<Tone, string> = {
|
|
67
|
+
text: "",
|
|
68
|
+
muted: "\x1b[90m",
|
|
69
|
+
dim: "\x1b[90m",
|
|
70
|
+
success: "\x1b[32m",
|
|
71
|
+
warning: "\x1b[33m",
|
|
72
|
+
error: "\x1b[31m",
|
|
73
|
+
running: "\x1b[34m",
|
|
74
|
+
accent: "",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** All family ink flows through here (design language §3). */
|
|
78
|
+
export function ink(theme: Theme | undefined, tone: Tone, text: string): string {
|
|
79
|
+
if (text.length === 0) return text;
|
|
80
|
+
const role = THEME_ROLE[tone];
|
|
81
|
+
if (theme && role) {
|
|
82
|
+
try {
|
|
83
|
+
return theme.fg(role, text);
|
|
84
|
+
} catch {
|
|
85
|
+
// fall through to the raw tone — never let styling break a render
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const open = RAW[tone];
|
|
89
|
+
return open ? `${open}${text}${RESET}` : text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- panel headers (design language §8) -------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** Mode pips for a panel header: active mode accent-bold, others dim, `→` dim. */
|
|
95
|
+
export function panelPips(theme: Theme | undefined, modes: readonly string[], active: string): string {
|
|
96
|
+
return modes
|
|
97
|
+
.map((mode) => (mode === active ? ink(theme, "accent", bold(theme, mode)) : ink(theme, "dim", mode)))
|
|
98
|
+
.join(ink(theme, "dim", " → "));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The family panel-header form (design language §8): `[Name]` in bold accent, optional
|
|
103
|
+
* mode pips, then one dim hint line carrying keybinding, scope, and the panel's
|
|
104
|
+
* methodology — stated here once, never on data rows.
|
|
105
|
+
*/
|
|
106
|
+
export function panelHeader(
|
|
107
|
+
theme: Theme | undefined,
|
|
108
|
+
name: string,
|
|
109
|
+
options: { modes?: readonly string[]; active?: string; hint?: string } = {},
|
|
110
|
+
): string[] {
|
|
111
|
+
const brand = ink(theme, "accent", bold(theme, `[${name}]`));
|
|
112
|
+
const pips = options.modes && options.active ? ` ${panelPips(theme, options.modes, options.active)}` : "";
|
|
113
|
+
const lines = ["", `${brand}${pips}`];
|
|
114
|
+
if (options.hint) lines.push(` ${ink(theme, "dim", options.hint)}`);
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function bold(theme: Theme | undefined, text: string): string {
|
|
119
|
+
try {
|
|
120
|
+
return theme?.bold ? theme.bold(text) : text;
|
|
121
|
+
} catch {
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- layout helpers (design language §5) -----------------------------------------------
|
|
127
|
+
|
|
128
|
+
export const ELLIPSIS = "\u2026"; // …
|
|
129
|
+
|
|
130
|
+
const MIN_HEAD_COLS = 6;
|
|
131
|
+
const TAIL_RATIO = 0.55;
|
|
132
|
+
|
|
133
|
+
// Replace an absolute home-directory prefix with ~ so boilerplate path heads stop eating
|
|
134
|
+
// width (e.g. bash `cd /Users/me/... && ...` -> `cd ~/... && ...`). OSC sequences are
|
|
135
|
+
// skipped: a read row's OSC 8 hyperlink carries a file:// URL containing the home path,
|
|
136
|
+
// and tildifying *that* would corrupt the click target. The pattern is built once —
|
|
137
|
+
// tildify runs for every visible row on every frame.
|
|
138
|
+
const TILDIFY_PATTERN = (() => {
|
|
139
|
+
const home = homedir();
|
|
140
|
+
if (!home) return undefined;
|
|
141
|
+
return new RegExp(`${OSC_SEQUENCE.source}|${home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
|
|
142
|
+
})();
|
|
143
|
+
|
|
144
|
+
export function tildify(text: string): string {
|
|
145
|
+
if (!TILDIFY_PATTERN) return text;
|
|
146
|
+
return text.replace(TILDIFY_PATTERN, (match) => (match.startsWith("\x1b") ? match : "~"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ANSI-aware middle truncation that protects the *tail* of the line — the basename +
|
|
150
|
+
// :line-range for a path, or the operative end of a command — because that is where the
|
|
151
|
+
// information distinguishing one row from the next lives. The cut snaps to a nearby "/"
|
|
152
|
+
// or space boundary, and the ellipsis is dimmed so it reads as a UI marker rather than as
|
|
153
|
+
// part of the path/command. Falls back to tail truncation only when the width is too
|
|
154
|
+
// small to keep both ends.
|
|
155
|
+
// Ink continuity across the cut (design language §5): the tail's opening SGR may
|
|
156
|
+
// sit in the removed middle, which would leave the tail at the terminal default until
|
|
157
|
+
// the next styled span opened. Replay the *net* SGR state active at the cut point —
|
|
158
|
+
// one sequence per attribute, offs and full resets clearing their slots — so the tail
|
|
159
|
+
// keeps its ink without echoing the whole styling history.
|
|
160
|
+
function activeSgrAt(line: string, rawIndex: number): string {
|
|
161
|
+
const state = new Map<string, string>();
|
|
162
|
+
const sgr = /\x1b\[([0-9;]*)m/g;
|
|
163
|
+
let match: RegExpExecArray | null;
|
|
164
|
+
while ((match = sgr.exec(line)) && match.index < rawIndex) {
|
|
165
|
+
const params = match[1] ?? "";
|
|
166
|
+
if (params === "" || params === "0") {
|
|
167
|
+
state.clear();
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const key =
|
|
171
|
+
params.startsWith("38") || params === "39" || /^(3[0-7]|9[0-7])$/.test(params)
|
|
172
|
+
? "fg"
|
|
173
|
+
: params.startsWith("48") || params === "49" || /^(4[0-7]|10[0-7])$/.test(params)
|
|
174
|
+
? "bg"
|
|
175
|
+
: params === "1" || params === "2" || params === "22"
|
|
176
|
+
? "weight"
|
|
177
|
+
: params === "3" || params === "23"
|
|
178
|
+
? "italic"
|
|
179
|
+
: params === "4" || params === "24"
|
|
180
|
+
? "underline"
|
|
181
|
+
: params; // unknown/compound: replay verbatim, keyed by itself
|
|
182
|
+
const off = params === "39" || params === "49" || params === "22" || params === "23" || params === "24";
|
|
183
|
+
if (off) state.delete(key);
|
|
184
|
+
else state.set(key, match[0]);
|
|
185
|
+
}
|
|
186
|
+
return [...state.values()].join("");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function middleTruncate(line: string, width: number, theme?: Theme): string {
|
|
190
|
+
const maxWidth = Math.max(1, width);
|
|
191
|
+
if (visibleWidth(line) <= maxWidth) return line;
|
|
192
|
+
|
|
193
|
+
const vis = stripAnsi(line);
|
|
194
|
+
const visLen = vis.length;
|
|
195
|
+
const ellipsisWidth = visibleWidth(ELLIPSIS);
|
|
196
|
+
const budget = Math.max(1, maxWidth - ellipsisWidth); // reserve columns for the ellipsis
|
|
197
|
+
|
|
198
|
+
const maxTail = Math.min(budget - MIN_HEAD_COLS, Math.max(12, Math.floor(budget * TAIL_RATIO)));
|
|
199
|
+
if (maxTail < 1) return truncateToWidth(line, maxWidth, ELLIPSIS);
|
|
200
|
+
|
|
201
|
+
// The cut is a column (design language §9.8): the tail is exactly `maxTail` wide and
|
|
202
|
+
// the head exactly fills the rest, so every line truncated to the same budget cuts at
|
|
203
|
+
// identical columns and fills the budget exactly. No content-dependent snapping — a
|
|
204
|
+
// mid-token cut beside a dim ellipsis is legible; a wandering ellipsis column is not.
|
|
205
|
+
const tailStart = visLen - maxTail;
|
|
206
|
+
const dimEllipsis = ink(theme, "dim", ELLIPSIS);
|
|
207
|
+
const tailRawStart = rawIndexAtVisibleIndex(line, tailStart);
|
|
208
|
+
const tailRaw = `${activeSgrAt(line, tailRawStart)}${line.slice(tailRawStart)}`;
|
|
209
|
+
|
|
210
|
+
const headEnd = budget - maxTail;
|
|
211
|
+
if (headEnd <= 0) return `${dimEllipsis}${tailRaw}`;
|
|
212
|
+
|
|
213
|
+
const headRaw = line.slice(0, rawIndexAtVisibleIndex(line, headEnd));
|
|
214
|
+
return `${headRaw}${RESET}${dimEllipsis}${tailRaw}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Quantities right (design language §5): body left, right-aligned suffix, ≥2-space gap
|
|
218
|
+
* (§9.1 — the tail must not crowd the facts); the body middle-truncates and the suffix
|
|
219
|
+
* wins when the width is starved. `reserve` (design language §9.8) is a minimum
|
|
220
|
+
* right-side reservation — gap included — so callers can hold every body in a block to
|
|
221
|
+
* the same budget regardless of each row's own suffix. */
|
|
222
|
+
export function rightAlignSuffix(
|
|
223
|
+
body: string,
|
|
224
|
+
suffix: string,
|
|
225
|
+
width: number,
|
|
226
|
+
theme?: Theme,
|
|
227
|
+
reserve = 0,
|
|
228
|
+
): string {
|
|
229
|
+
const maxWidth = Math.max(1, width);
|
|
230
|
+
const bodyText = body.trimEnd();
|
|
231
|
+
if (!suffix) {
|
|
232
|
+
const budget = maxWidth - reserve >= MIN_HEAD_COLS ? maxWidth - reserve : maxWidth;
|
|
233
|
+
return middleTruncate(bodyText, budget, theme);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const suffixWidth = visibleWidth(suffix);
|
|
237
|
+
if (suffixWidth >= maxWidth) return truncateToWidth(suffix, maxWidth, ELLIPSIS);
|
|
238
|
+
|
|
239
|
+
const bodyWidth = Math.max(0, maxWidth - Math.max(suffixWidth + 2, reserve));
|
|
240
|
+
const fittedBody = bodyWidth > 0 ? middleTruncate(bodyText, bodyWidth, theme) : "";
|
|
241
|
+
const gapWidth = Math.max(1, maxWidth - visibleWidth(fittedBody) - suffixWidth);
|
|
242
|
+
return `${fittedBody}${" ".repeat(gapWidth)}${suffix}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- severity (design language §6) -------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/** Severity thresholds for tool result sizes, in chars (design language §6). */
|
|
248
|
+
export type SizeThresholds = { warning: number; error: number };
|
|
249
|
+
|
|
250
|
+
export const SIZE_THRESHOLDS: SizeThresholds = { warning: 10_000, error: 50_000 };
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Tone for a result-size quantity: dim below the warning threshold so healthy rows
|
|
254
|
+
* stay quiet; warning/error above so ballooned outputs jump out of the column.
|
|
255
|
+
*/
|
|
256
|
+
export function sizeTone(chars: number, thresholds: SizeThresholds = SIZE_THRESHOLDS): Tone {
|
|
257
|
+
if (chars >= thresholds.error) return "error";
|
|
258
|
+
if (chars >= thresholds.warning) return "warning";
|
|
259
|
+
return "dim";
|
|
260
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copy All
|
|
2
|
+
|
|
3
|
+
Copies a readable transcript of the active Pi conversation branch to the system clipboard.
|
|
4
|
+
|
|
5
|
+
## Command
|
|
6
|
+
|
|
7
|
+
`/copy-all` waits for the current agent run to settle, then copies visible user and assistant messages from the active branch. Messages are labeled `USER` and `ASSISTANT` and separated with a horizontal divider.
|
|
8
|
+
|
|
9
|
+
The exported transcript intentionally excludes system messages, tool calls, tool results, and hidden reasoning. Image blocks are represented as `[image]`. Empty branches produce an informational notification instead of changing the clipboard.
|
|
10
|
+
|
|
11
|
+
Clipboard access uses Pi's cross-platform clipboard helper. A clipboard failure is shown in the interface and does not alter the session.
|
|
12
|
+
|
|
13
|
+
## Validation
|
|
14
|
+
|
|
15
|
+
From the repository root, run `npm run validate`.
|