@rol1n/pi-kit 0.0.0-stage → 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.
- package/README.md +73 -2
- package/ask-u-questions/README.md +26 -0
- package/ask-u-questions/dialog-ui.ts +80 -0
- package/ask-u-questions/index.ts +92 -0
- package/ask-u-questions/multi-choice.ts +258 -0
- package/ask-u-questions/single-choice.ts +180 -0
- package/ask-u-questions/tool-renderers.ts +67 -0
- package/ask-u-questions/types.ts +192 -0
- package/continue-signal/README.md +20 -0
- package/continue-signal/index.ts +69 -0
- package/custom-providers/README.md +20 -0
- package/custom-providers/asr/handlers/dashscope.ts +154 -0
- package/custom-providers/asr/index.ts +37 -0
- package/custom-providers/asr/models.ts +18 -0
- package/custom-providers/asr/types.ts +20 -0
- package/custom-providers/chat/index.ts +33 -0
- package/custom-providers/chat/magpie.ts +258 -0
- package/custom-providers/chat/models.ts +111 -0
- package/custom-providers/classifier/index.ts +6 -0
- package/custom-providers/classifier/models.ts +1 -0
- package/custom-providers/image/handlers/dashscope.ts +240 -0
- package/custom-providers/image/handlers/openrouter.ts +185 -0
- package/custom-providers/image/index.ts +96 -0
- package/custom-providers/image/models.ts +71 -0
- package/custom-providers/image/types.ts +22 -0
- package/custom-providers/index.ts +64 -0
- package/custom-providers/types.ts +17 -0
- package/image-generate/README.md +25 -0
- package/image-generate/files.ts +49 -0
- package/image-generate/index.ts +220 -0
- package/image-generate/model-selector.ts +150 -0
- package/memory-log/README.md +54 -0
- package/memory-log/agent/consolidator/prompt.ts +47 -0
- package/memory-log/agent/consolidator/tools.ts +160 -0
- package/memory-log/agent/cost.ts +32 -0
- package/memory-log/agent/index.ts +42 -0
- package/memory-log/agent/observer/prompt.ts +73 -0
- package/memory-log/agent/observer/tool.ts +73 -0
- package/memory-log/commands/compact.ts +35 -0
- package/memory-log/commands/consolidate.ts +47 -0
- package/memory-log/commands/status.ts +50 -0
- package/memory-log/config.ts +253 -0
- package/memory-log/debug-log.ts +76 -0
- package/memory-log/hooks/compaction-hook.ts +161 -0
- package/memory-log/hooks/compaction-trigger.ts +160 -0
- package/memory-log/hooks/consolidator-trigger.ts +143 -0
- package/memory-log/hooks/observer-trigger.ts +176 -0
- package/memory-log/ids.ts +78 -0
- package/memory-log/index.ts +149 -0
- package/memory-log/ledger/fold.ts +77 -0
- package/memory-log/ledger/index.ts +7 -0
- package/memory-log/ledger/pool.ts +38 -0
- package/memory-log/ledger/progress.ts +181 -0
- package/memory-log/ledger/projection.ts +115 -0
- package/memory-log/ledger/render.ts +38 -0
- package/memory-log/ledger/serialize.ts +161 -0
- package/memory-log/ledger/types.ts +201 -0
- package/memory-log/memory/index-render.ts +45 -0
- package/memory-log/memory/paths.ts +120 -0
- package/memory-log/memory/session.ts +64 -0
- package/memory-log/runtime.ts +127 -0
- package/memory-log/spawn/launch.ts +133 -0
- package/memory-log/spawn/runs.ts +83 -0
- package/memory-log/tokens.ts +66 -0
- package/memory-log/ui/status-controller.ts +225 -0
- package/memory-log/ui/timeline.ts +103 -0
- package/model-vision/README.md +22 -0
- package/multi-harness/README.md +63 -0
- package/multi-harness/adapters.mjs +72 -0
- package/multi-harness/harness-done.mjs +14 -0
- package/multi-harness/index.ts +375 -0
- package/multi-harness/ipc.mjs +37 -0
- package/multi-harness/opencode-plugin.mjs +99 -0
- package/multi-harness/process-runner.mjs +115 -0
- package/multi-harness/turn-hook.mjs +29 -0
- package/multi-harness/worker-bridge.ts +52 -0
- package/oai-subquota/README.md +22 -0
- package/oai-subquota/index.ts +286 -0
- package/oai-subquota/snapshot.ts +293 -0
- package/oai-subquota/ui.ts +183 -0
- package/package.json +92 -4
- package/prompt-snipaste/README.md +27 -0
- package/prompt-snipaste/index.ts +143 -0
- package/prompt-snipaste/menu.ts +159 -0
- package/prompt-snipaste/prompts/ask-questions.md +8 -0
- package/prompt-snipaste/prompts/delegate-exploration.md +7 -0
- package/prompt-snipaste/prompts/diagnose-report.md +7 -0
- package/prompt-snipaste/prompts/frontend-tech-preset.md +18 -0
- package/prompt-snipaste/prompts/orchestrator-mode.md +7 -0
- package/prompt-snipaste/prompts/session-kickoff.md +7 -0
- package/prompt-snipaste/prompts/verify-not-assume.md +7 -0
- package/prompt-snipaste/renderers.ts +87 -0
- package/prompt-snipaste/snippets.ts +206 -0
- package/prompt-snipaste/types.ts +18 -0
- package/screenshot/README.md +14 -0
- package/srp-theme/README.md +18 -0
- package/srp-theme/footer.ts +193 -0
- package/srp-theme/index.ts +172 -0
- package/srp-theme/tps-meter.ts +441 -0
- package/subagent/README.md +35 -0
- package/subagent/bridge.ts +171 -0
- package/subagent/index.ts +590 -0
- package/subagent/mux.ts +621 -0
- package/subagent/session.ts +166 -0
- package/subagent/ui.ts +100 -0
- package/tui-asr/README.md +21 -0
- package/tui-asr/dictation.ts +272 -0
- package/tui-asr/index.ts +181 -0
- package/tui-asr/input.ts +87 -0
- package/tui-asr/model-selector.ts +163 -0
- package/tui-asr/status.ts +79 -0
- package/web-reach/README.md +19 -0
- package/web-reach/html.ts +83 -0
- package/web-reach/index.ts +82 -0
- package/web-reach/page.ts +214 -0
- package/web-reach/rsc.ts +161 -0
- package/web-reach/search.ts +85 -0
- package/web-reach/transport.ts +35 -0
- package/windows-wheel-fix/README.md +12 -0
- package/windows-wheel-fix/index.ts +44 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { PromptCardData, Snippet } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const WIDGET_ID = "prompt-snipaste";
|
|
6
|
+
|
|
7
|
+
export function buildPromptCardComponent(
|
|
8
|
+
snippets: PromptCardData["snippets"],
|
|
9
|
+
theme: Theme,
|
|
10
|
+
) {
|
|
11
|
+
const borderCol = (s: string) => theme.fg("borderAccent", s);
|
|
12
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
13
|
+
const bold = (s: string) => theme.bold(s);
|
|
14
|
+
|
|
15
|
+
const titleText = ` Prompt Snippets (${snippets.length}) `;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
render(width: number): string[] {
|
|
19
|
+
const effectiveWidth = Math.max(20, Math.min(width, 100));
|
|
20
|
+
const innerW = effectiveWidth - 4;
|
|
21
|
+
|
|
22
|
+
const titleVisW = visibleWidth(titleText);
|
|
23
|
+
const topDashLen = Math.max(1, innerW - titleVisW);
|
|
24
|
+
const topLine = borderCol("╭─") + bold(titleText) + borderCol("─".repeat(topDashLen) + "╮");
|
|
25
|
+
const botLine = borderCol("╰" + "─".repeat(innerW + 2) + "╯");
|
|
26
|
+
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
lines.push(topLine);
|
|
29
|
+
|
|
30
|
+
snippets.forEach((item, idx) => {
|
|
31
|
+
if (idx > 0) {
|
|
32
|
+
lines.push(borderCol("├" + "─".repeat(innerW + 2) + "┤"));
|
|
33
|
+
}
|
|
34
|
+
const header = `● ${item.name}`;
|
|
35
|
+
const headerVisW = visibleWidth(header);
|
|
36
|
+
const headerPad = Math.max(0, innerW - headerVisW);
|
|
37
|
+
lines.push(borderCol("│ ") + bold(header) + " ".repeat(headerPad) + borderCol(" │"));
|
|
38
|
+
|
|
39
|
+
const bodyLines = item.body.split(/\r?\n/);
|
|
40
|
+
for (const line of bodyLines) {
|
|
41
|
+
if (!line.trim()) continue;
|
|
42
|
+
for (const wrapped of wrapTextWithAnsi(line, innerW)) {
|
|
43
|
+
const pad = Math.max(0, innerW - visibleWidth(wrapped));
|
|
44
|
+
lines.push(borderCol("│ ") + dim(wrapped) + " ".repeat(pad) + borderCol(" │"));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
lines.push(botLine);
|
|
50
|
+
return lines.map((l) => truncateToWidth(l, width));
|
|
51
|
+
},
|
|
52
|
+
invalidate() {},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function updateSnippetWidget(
|
|
57
|
+
ctx: ExtensionContext,
|
|
58
|
+
getSelection: () => { snippets: Snippet[]; enabled: Set<string> },
|
|
59
|
+
) {
|
|
60
|
+
if (getSelection().enabled.size === 0) {
|
|
61
|
+
ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.ui.setWidget(WIDGET_ID, (_tui, theme) => ({
|
|
66
|
+
render(width: number): string[] {
|
|
67
|
+
const { snippets, enabled } = getSelection();
|
|
68
|
+
const active = snippets.filter((s) => enabled.has(s.id));
|
|
69
|
+
if (active.length === 0) return [];
|
|
70
|
+
|
|
71
|
+
const prepends = active.filter((s) => s.placement === "prepend");
|
|
72
|
+
const appends = active.filter((s) => s.placement === "append");
|
|
73
|
+
|
|
74
|
+
const parts: string[] = [];
|
|
75
|
+
if (prepends.length > 0) {
|
|
76
|
+
parts.push(theme.fg("accent", `↑ ${prepends.map((s) => s.name).join(", ")}`));
|
|
77
|
+
}
|
|
78
|
+
if (appends.length > 0) {
|
|
79
|
+
parts.push(theme.fg("warning", `↓ ${appends.map((s) => s.name).join(", ")}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const text = `[Prompts: ${parts.join(" · ")}]`;
|
|
83
|
+
return [truncateToWidth(text, width)];
|
|
84
|
+
},
|
|
85
|
+
invalidate() {},
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const BUNDLED_PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), "prompts");
|
|
7
|
+
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { Snippet } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export function parseSnippet(filename: string, raw: string): Snippet | null {
|
|
11
|
+
const cleanRaw = raw.replace(/^\uFEFF/, "");
|
|
12
|
+
const match = cleanRaw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
13
|
+
|
|
14
|
+
let name = filename.replace(/\.md$/, "");
|
|
15
|
+
let description = "";
|
|
16
|
+
let placement: "prepend" | "append" = "append";
|
|
17
|
+
let order = 100;
|
|
18
|
+
let body = "";
|
|
19
|
+
|
|
20
|
+
if (match) {
|
|
21
|
+
const meta: Record<string, string> = {};
|
|
22
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
23
|
+
const kv = line.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
|
|
24
|
+
if (kv) meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
|
|
25
|
+
}
|
|
26
|
+
if (meta.name) name = meta.name;
|
|
27
|
+
if (meta.description) description = meta.description;
|
|
28
|
+
if (meta.placement === "prepend") placement = "prepend";
|
|
29
|
+
const parsedOrder = Number.parseInt(meta.order ?? "100", 10);
|
|
30
|
+
if (!Number.isNaN(parsedOrder)) order = parsedOrder;
|
|
31
|
+
body = match[2].trim();
|
|
32
|
+
} else {
|
|
33
|
+
body = cleanRaw.trim();
|
|
34
|
+
if (!body) return null;
|
|
35
|
+
const firstLine = body.split(/\r?\n/).find((l) => l.trim().length > 0) ?? "";
|
|
36
|
+
description = firstLine.replace(/^#+\s*/, "").slice(0, 80);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!body) return null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: filename,
|
|
43
|
+
name,
|
|
44
|
+
description,
|
|
45
|
+
placement,
|
|
46
|
+
order,
|
|
47
|
+
body,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveSnippetPath(rawPath: string, cwd: string): string {
|
|
52
|
+
const trimmed = rawPath.trim();
|
|
53
|
+
if (trimmed === "~" || trimmed.startsWith("~/") || (process.platform === "win32" && trimmed.startsWith("~\\"))) {
|
|
54
|
+
return resolve(homedir(), trimmed.replace(/^~[/\\]?/, ""));
|
|
55
|
+
}
|
|
56
|
+
if (isAbsolute(trimmed)) {
|
|
57
|
+
return resolve(trimmed);
|
|
58
|
+
}
|
|
59
|
+
const fromCwd = resolve(cwd, trimmed);
|
|
60
|
+
if (existsSync(fromCwd)) return fromCwd;
|
|
61
|
+
const fromAgent = resolve(getAgentDir(), trimmed);
|
|
62
|
+
if (existsSync(fromAgent)) return fromAgent;
|
|
63
|
+
return fromCwd;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSettingsPrompts(cwd: string, pi?: ExtensionAPI): string[] {
|
|
67
|
+
const results: string[] = [];
|
|
68
|
+
|
|
69
|
+
if (pi) {
|
|
70
|
+
try {
|
|
71
|
+
const settings = pi.getSettings();
|
|
72
|
+
if (Array.isArray(settings?.prompts)) {
|
|
73
|
+
results.push(...settings.prompts);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// pi.getSettings() may throw before runner binds
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const settingsFiles = [
|
|
82
|
+
join(getAgentDir(), "settings.json"),
|
|
83
|
+
join(cwd, CONFIG_DIR_NAME, "settings.json"),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (const sf of settingsFiles) {
|
|
87
|
+
if (!existsSync(sf)) continue;
|
|
88
|
+
try {
|
|
89
|
+
const content = readFileSync(sf, "utf-8");
|
|
90
|
+
const parsed = JSON.parse(content.replace(/^\uFEFF/, ""));
|
|
91
|
+
if (Array.isArray(parsed?.prompts)) {
|
|
92
|
+
for (const p of parsed.prompts) {
|
|
93
|
+
if (typeof p === "string" && !results.includes(p)) {
|
|
94
|
+
results.push(p);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore JSON parse errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function loadSnippets(cwd: string, pi?: ExtensionAPI): Snippet[] {
|
|
107
|
+
const seen = new Map<string, Snippet>();
|
|
108
|
+
const excludedNames = new Set<string>();
|
|
109
|
+
|
|
110
|
+
const loadedPromptPaths = pi?.getCommands()
|
|
111
|
+
.filter((command) => command.source === "prompt")
|
|
112
|
+
.map((command) => command.sourceInfo.path);
|
|
113
|
+
const defaultDirs = loadedPromptPaths ? [] : [
|
|
114
|
+
BUNDLED_PROMPTS_DIR,
|
|
115
|
+
join(getAgentDir(), "prompts"),
|
|
116
|
+
join(cwd, CONFIG_DIR_NAME, "prompts"),
|
|
117
|
+
];
|
|
118
|
+
const settingsPrompts = loadedPromptPaths ?? getSettingsPrompts(cwd, pi);
|
|
119
|
+
|
|
120
|
+
const processDirectory = (dir: string) => {
|
|
121
|
+
if (!existsSync(dir)) return;
|
|
122
|
+
let files: string[];
|
|
123
|
+
try {
|
|
124
|
+
files = readdirSync(dir);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
127
|
+
console.error(`Failed to read prompt directory ${dir}: ${error.message}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
if (!file.endsWith(".md")) continue;
|
|
132
|
+
if (excludedNames.has(file)) continue;
|
|
133
|
+
const fullPath = join(dir, file);
|
|
134
|
+
try {
|
|
135
|
+
const snippet = parseSnippet(file, readFileSync(fullPath, "utf-8"));
|
|
136
|
+
if (snippet) seen.set(file, snippet);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
139
|
+
console.error(`Failed to read prompt ${fullPath}: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const processFile = (filePath: string) => {
|
|
145
|
+
if (!existsSync(filePath) || !filePath.endsWith(".md")) return;
|
|
146
|
+
const filename = basename(filePath);
|
|
147
|
+
if (excludedNames.has(filename)) return;
|
|
148
|
+
try {
|
|
149
|
+
const snippet = parseSnippet(filename, readFileSync(filePath, "utf-8"));
|
|
150
|
+
if (snippet) seen.set(filename, snippet);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
153
|
+
console.error(`Failed to read prompt file ${filePath}: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Collect exclusions first
|
|
158
|
+
for (const raw of settingsPrompts) {
|
|
159
|
+
if (typeof raw !== "string") continue;
|
|
160
|
+
const trimmed = raw.trim();
|
|
161
|
+
if (trimmed.startsWith("-") || trimmed.startsWith("!")) {
|
|
162
|
+
const clean = trimmed.slice(1).trim();
|
|
163
|
+
excludedNames.add(basename(clean));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 1. Load default prompt directories
|
|
168
|
+
for (const dir of defaultDirs) {
|
|
169
|
+
processDirectory(dir);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 2. Load Pi's resolved prompts, or explicit settings paths without a runtime.
|
|
173
|
+
for (const raw of settingsPrompts) {
|
|
174
|
+
if (typeof raw !== "string") continue;
|
|
175
|
+
let trimmed = raw.trim();
|
|
176
|
+
if (trimmed.startsWith("-") || trimmed.startsWith("!")) continue;
|
|
177
|
+
if (trimmed.startsWith("+")) trimmed = trimmed.slice(1).trim();
|
|
178
|
+
if (!trimmed) continue;
|
|
179
|
+
|
|
180
|
+
const resolved = resolveSnippetPath(trimmed, cwd);
|
|
181
|
+
if (!existsSync(resolved)) continue;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const stat = statSync(resolved);
|
|
185
|
+
if (stat.isDirectory()) {
|
|
186
|
+
processDirectory(resolved);
|
|
187
|
+
} else if (stat.isFile() && resolved.endsWith(".md")) {
|
|
188
|
+
processFile(resolved);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (!(error instanceof Error) || !("code" in error)) throw error;
|
|
192
|
+
console.error(`Failed to inspect prompt path ${resolved}: ${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Remove any excluded items
|
|
197
|
+
for (const ex of excludedNames) {
|
|
198
|
+
seen.delete(ex);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return Array.from(seen.values()).sort((a, b) => {
|
|
202
|
+
if (a.placement !== b.placement) return a.placement === "prepend" ? -1 : 1;
|
|
203
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
204
|
+
return a.name.localeCompare(b.name);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Snippet {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
placement: "prepend" | "append";
|
|
6
|
+
order: number;
|
|
7
|
+
body: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PromptCardData {
|
|
11
|
+
snippets: {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
placement: "prepend" | "append";
|
|
15
|
+
order: number;
|
|
16
|
+
body: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# screenshot
|
|
2
|
+
|
|
3
|
+
Insert the file path of the most recently modified Windows/WSL system screenshot into Pi's editor via `/screenshot`.
|
|
4
|
+
|
|
5
|
+
Searches standard and localized `Pictures/Screenshots` and OneDrive screenshot directories under `USERPROFILE` on Windows or `/mnt/c/Users` on WSL.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pi install npm:@rol1n/screenshot
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Available as a standalone package (not enabled in `@rol1n/pi-kit` by default).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# srp-theme
|
|
2
|
+
|
|
3
|
+
Incremental Pi terminal presentation enhancement: adds a `belowEditor` last-prompt preview widget and a real-time streaming TPS (tokens per second) status meter while keeping Pi's native header and footer intact.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pi install npm:@rol1n/srp-theme
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Also included in `@rol1n/pi-kit`.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
/srp-theme on
|
|
17
|
+
/srp-theme off
|
|
18
|
+
```
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
// ============================ Prompt Preview Widget ============================
|
|
5
|
+
|
|
6
|
+
const POWERLINE_SEP_ANSI = "\x1b[38;5;244m";
|
|
7
|
+
const ANSI_RESET = "\x1b[0m";
|
|
8
|
+
|
|
9
|
+
/** Compact a multiline prompt into a single line, matching powerline's last-prompt behavior. */
|
|
10
|
+
export function compactPrompt(prompt: string | null | undefined): string {
|
|
11
|
+
// Custom-message runs (extension `sendMessage`, auto-continue, retries) have no user prompt.
|
|
12
|
+
if (typeof prompt !== "string") return "";
|
|
13
|
+
return prompt.replace(/\s+/g, " ").trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render last prompt line using powerline's sep color (ANSI 244), displaying '↳ prompt'.
|
|
18
|
+
*/
|
|
19
|
+
export function renderLastPromptLine(
|
|
20
|
+
lastUserPrompt: string | null | undefined,
|
|
21
|
+
width: number,
|
|
22
|
+
): string[] {
|
|
23
|
+
const compact = compactPrompt(lastUserPrompt);
|
|
24
|
+
const prefix = ` ${POWERLINE_SEP_ANSI}↳${ANSI_RESET} `;
|
|
25
|
+
const availableWidth = width - visibleWidth(prefix);
|
|
26
|
+
|
|
27
|
+
if (!compact || availableWidth < 10) return [];
|
|
28
|
+
|
|
29
|
+
return [
|
|
30
|
+
truncateToWidth(
|
|
31
|
+
`${prefix}${POWERLINE_SEP_ANSI}${truncateToWidth(compact, availableWidth, "…")}${ANSI_RESET}`,
|
|
32
|
+
width,
|
|
33
|
+
"…",
|
|
34
|
+
),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// ============================ Incremental Time / Finished Widget ============================
|
|
41
|
+
|
|
42
|
+
// const WEEK_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
43
|
+
// type ClockVariant = "full" | "month_day" | "time_only";
|
|
44
|
+
type FinishedVariant = "full" | "compact" | "duration_only";
|
|
45
|
+
|
|
46
|
+
function formatDuration(ms: number): string {
|
|
47
|
+
if (ms < 0) return "0.0s";
|
|
48
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
|
|
49
|
+
if (ms < 3600_000) {
|
|
50
|
+
const totalSecs = Math.round(ms / 1000);
|
|
51
|
+
const mins = Math.floor(totalSecs / 60);
|
|
52
|
+
const secs = totalSecs % 60;
|
|
53
|
+
return `${mins}m${secs < 10 ? "0" : ""}${secs}s`;
|
|
54
|
+
}
|
|
55
|
+
const totalMins = Math.round(ms / 60000);
|
|
56
|
+
const hours = Math.floor(totalMins / 60);
|
|
57
|
+
const mins = totalMins % 60;
|
|
58
|
+
return `${hours}h${mins < 10 ? "0" : ""}${mins}m`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
// 时间胶囊格式化已注释停用
|
|
63
|
+
function formatFooterClockVariant(date: Date, variant: ClockVariant): string {
|
|
64
|
+
const pad = (value: number) => value.toString().padStart(2, "0");
|
|
65
|
+
const hours = pad(date.getHours());
|
|
66
|
+
const minutes = pad(date.getMinutes());
|
|
67
|
+
|
|
68
|
+
if (variant === "time_only") return `[${hours}:${minutes}]`;
|
|
69
|
+
|
|
70
|
+
const month = pad(date.getMonth() + 1);
|
|
71
|
+
const dayOfMonth = pad(date.getDate());
|
|
72
|
+
if (variant === "month_day") return `[${month}-${dayOfMonth} ${hours}:${minutes}]`;
|
|
73
|
+
|
|
74
|
+
return `[${date.getFullYear()}-${month}-${dayOfMonth} ${WEEK_DAYS[date.getDay()]} ${hours}:${minutes}]`;
|
|
75
|
+
}
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
function formatLoopEndVariant(
|
|
79
|
+
finished: Date,
|
|
80
|
+
now: Date,
|
|
81
|
+
durationMs: number | null,
|
|
82
|
+
variant: FinishedVariant,
|
|
83
|
+
): string {
|
|
84
|
+
const pad = (value: number) => value.toString().padStart(2, "0");
|
|
85
|
+
const hasDuration = typeof durationMs === "number" && durationMs >= 0;
|
|
86
|
+
|
|
87
|
+
if (variant === "duration_only") {
|
|
88
|
+
return hasDuration ? `X ${formatDuration(durationMs)}` : "X";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isCrossYear = finished.getFullYear() !== now.getFullYear();
|
|
92
|
+
const isCrossDay =
|
|
93
|
+
finished.getMonth() !== now.getMonth() || finished.getDate() !== now.getDate();
|
|
94
|
+
const dateOrTime = isCrossYear
|
|
95
|
+
? `${finished.getFullYear()}`
|
|
96
|
+
: isCrossDay
|
|
97
|
+
? `${pad(finished.getMonth() + 1)}-${pad(finished.getDate())}`
|
|
98
|
+
: `${pad(finished.getHours())}:${pad(finished.getMinutes())}`;
|
|
99
|
+
|
|
100
|
+
const details = hasDuration ? `${formatDuration(durationMs)} ${dateOrTime}` : dateOrTime;
|
|
101
|
+
return `X ${details}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getLastLoopInfoFromSession(ctx: ExtensionContext): {
|
|
105
|
+
endTime: Date;
|
|
106
|
+
durationMs: number | null;
|
|
107
|
+
} | null {
|
|
108
|
+
try {
|
|
109
|
+
const entries = ctx.sessionManager?.getEntries?.() ?? [];
|
|
110
|
+
let assistantIndex = -1;
|
|
111
|
+
let endTime: Date | null = null;
|
|
112
|
+
|
|
113
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
114
|
+
const entry = entries[i] as any;
|
|
115
|
+
if (entry?.type !== "message" || entry?.message?.role !== "assistant" || !entry?.timestamp) continue;
|
|
116
|
+
const timestamp = new Date(entry.timestamp);
|
|
117
|
+
if (!Number.isNaN(timestamp.getTime())) {
|
|
118
|
+
assistantIndex = i;
|
|
119
|
+
endTime = timestamp;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!endTime || assistantIndex < 0) return null;
|
|
125
|
+
|
|
126
|
+
let durationMs: number | null = null;
|
|
127
|
+
for (let i = assistantIndex - 1; i >= 0; i--) {
|
|
128
|
+
const entry = entries[i] as any;
|
|
129
|
+
if (entry?.type !== "message" || entry?.message?.role !== "user" || !entry?.timestamp) continue;
|
|
130
|
+
const startTime = new Date(entry.timestamp);
|
|
131
|
+
if (!Number.isNaN(startTime.getTime())) {
|
|
132
|
+
const duration = endTime.getTime() - startTime.getTime();
|
|
133
|
+
durationMs = duration >= 0 ? duration : null;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { endTime, durationMs };
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function renderTimeAndFinishedLine(
|
|
145
|
+
lastLoopEndTime: Date | null,
|
|
146
|
+
lastLoopDurationMs: number | null,
|
|
147
|
+
width: number,
|
|
148
|
+
theme: Theme,
|
|
149
|
+
): string[] {
|
|
150
|
+
if (!lastLoopEndTime) return [];
|
|
151
|
+
|
|
152
|
+
const now = new Date();
|
|
153
|
+
const candidates: string[] = [];
|
|
154
|
+
|
|
155
|
+
const fullFinished = formatLoopEndVariant(lastLoopEndTime, now, lastLoopDurationMs, "full");
|
|
156
|
+
const compactFinished = formatLoopEndVariant(lastLoopEndTime, now, lastLoopDurationMs, "compact");
|
|
157
|
+
const durationFinished = formatLoopEndVariant(lastLoopEndTime, now, lastLoopDurationMs, "duration_only");
|
|
158
|
+
|
|
159
|
+
candidates.push(fullFinished);
|
|
160
|
+
if (compactFinished !== fullFinished) {
|
|
161
|
+
candidates.push(compactFinished);
|
|
162
|
+
}
|
|
163
|
+
if (durationFinished && durationFinished !== compactFinished && durationFinished !== fullFinished) {
|
|
164
|
+
candidates.push(durationFinished);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
// 时间胶囊(已禁用/注释,仅保留 agent-loop 完成时间和用时):
|
|
169
|
+
if (lastLoopEndTime) {
|
|
170
|
+
candidates.push(
|
|
171
|
+
`${fullFinished} ${formatFooterClockVariant(now, "full")}`,
|
|
172
|
+
`${fullFinished} ${formatFooterClockVariant(now, "month_day")}`,
|
|
173
|
+
`${fullFinished} ${formatFooterClockVariant(now, "time_only")}`,
|
|
174
|
+
);
|
|
175
|
+
if (compactFinished !== fullFinished) {
|
|
176
|
+
candidates.push(`${compactFinished} ${formatFooterClockVariant(now, "time_only")}`);
|
|
177
|
+
}
|
|
178
|
+
if (durationFinished && durationFinished !== compactFinished && durationFinished !== fullFinished) {
|
|
179
|
+
candidates.push(`${durationFinished} ${formatFooterClockVariant(now, "time_only")}`);
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
candidates.push(
|
|
183
|
+
formatFooterClockVariant(now, "full"),
|
|
184
|
+
formatFooterClockVariant(now, "month_day"),
|
|
185
|
+
formatFooterClockVariant(now, "time_only"),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
const content = candidates.find((candidate) => visibleWidth(candidate) <= width) ?? candidates.at(-1)!;
|
|
191
|
+
const line = truncateToWidth(content, width, "…");
|
|
192
|
+
return [theme.fg("dim", line)];
|
|
193
|
+
}
|