@diegopetrucci/pi-extensions 0.1.72 → 0.1.73
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 +2 -2
- package/extensions/agent-workflow-audit/index.ts +41 -8
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +7 -3
- package/extensions/annotate-git-diff/git.ts +3 -2
- package/extensions/annotate-git-diff/index.ts +366 -81
- package/extensions/annotate-git-diff/package.json +2 -2
- package/extensions/annotate-git-diff/quiet-glimpse.ts +4 -2
- package/extensions/annotate-git-diff/types.ts +6 -0
- package/extensions/annotate-git-diff/web/app.js +54 -4
- package/extensions/annotate-git-diff/web/index.html +2 -2
- package/extensions/annotate-last-message/README.md +9 -1
- package/extensions/annotate-last-message/index.ts +53 -13
- package/extensions/annotate-last-message/package.json +2 -1
- package/extensions/annotate-last-message/quiet-glimpse.ts +4 -2
- package/extensions/annotate-last-message/theme.ts +247 -0
- package/extensions/annotate-last-message/ui.ts +28 -2
- package/extensions/annotate-last-message/web/app.js +351 -162
- package/extensions/annotate-last-message/web/index.html +153 -24
- package/extensions/annotate-last-message/web/md-renderer.js +458 -0
- package/extensions/code-reviewer/index.ts +12 -1
- package/extensions/code-reviewer/package.json +2 -1
- package/extensions/context-cap/README.md +33 -4
- package/extensions/context-cap/index.ts +271 -68
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/README.md +2 -0
- package/extensions/context-inspector/index.ts +90 -9
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +21 -15
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +17 -5
- package/extensions/dynamic-context-pruning/index.ts +17 -4
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +24 -3
- package/extensions/fast/index.ts +2 -2
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -0
- package/extensions/git-footer/package.json +1 -1
- package/extensions/librarian/index.ts +7 -0
- package/extensions/librarian/package.json +2 -1
- package/extensions/minimal-footer/README.md +16 -4
- package/extensions/minimal-footer/index.ts +263 -14
- package/extensions/minimal-footer/openai-usage.ts +62 -8
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/index.ts +19 -18
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +6 -0
- package/extensions/permission-gate/index.ts +37 -16
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/quiet-tools/README.md +1 -1
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/triage-comments/package.json +2 -1
- package/package.json +4 -4
|
@@ -846,6 +846,53 @@ function clearWorkingTreeCommitState() {
|
|
|
846
846
|
}
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
function clearCommitScopedErrors(sha) {
|
|
850
|
+
for (const key of Object.keys(state.fileErrors)) {
|
|
851
|
+
if (key.startsWith(`commits:${sha}:`)) delete state.fileErrors[key];
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function reconcileCommitState(nextCommits) {
|
|
856
|
+
const retainedImmutableShas = new Set(
|
|
857
|
+
nextCommits.filter((commit) => commit.kind === "commit").map((commit) => commit.sha),
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
const knownShas = new Set([
|
|
861
|
+
...Object.keys(state.commitFilesBySha),
|
|
862
|
+
...Object.keys(state.commitErrors),
|
|
863
|
+
...Object.keys(state.commitRequestIds),
|
|
864
|
+
]);
|
|
865
|
+
for (const key of [
|
|
866
|
+
...Object.keys(state.fileContents),
|
|
867
|
+
...Object.keys(state.fileErrors),
|
|
868
|
+
...Object.keys(state.pendingRequestIds),
|
|
869
|
+
]) {
|
|
870
|
+
if (!key.startsWith("commits:")) continue;
|
|
871
|
+
const rest = key.slice("commits:".length);
|
|
872
|
+
const separator = rest.indexOf(":");
|
|
873
|
+
if (separator > 0) knownShas.add(rest.slice(0, separator));
|
|
874
|
+
}
|
|
875
|
+
for (const sha of knownShas) {
|
|
876
|
+
if (retainedImmutableShas.has(sha)) continue;
|
|
877
|
+
const previousFiles = state.commitFilesBySha[sha] ?? [];
|
|
878
|
+
for (const file of previousFiles) {
|
|
879
|
+
delete state.reviewedFiles[file.id];
|
|
880
|
+
delete state.scrollPositions[scrollKey("commits", file.id, sha)];
|
|
881
|
+
}
|
|
882
|
+
clearCommitScopedState(sha);
|
|
883
|
+
delete state.commitFilesBySha[sha];
|
|
884
|
+
delete state.commitErrors[sha];
|
|
885
|
+
delete state.commitRequestIds[sha];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
for (const sha of retainedImmutableShas) {
|
|
889
|
+
// A failed immutable request is safe to retry after a successful refresh;
|
|
890
|
+
// successful immutable contents and in-flight requests remain untouched.
|
|
891
|
+
delete state.commitErrors[sha];
|
|
892
|
+
clearCommitScopedErrors(sha);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
849
896
|
function requestLatestReviewData() {
|
|
850
897
|
if (!window.glimpse?.send) return;
|
|
851
898
|
if (state.reviewDataRequestId != null) return;
|
|
@@ -1779,7 +1826,7 @@ function submitDraftOnClose() {
|
|
|
1779
1826
|
const payload = buildSubmitPayload();
|
|
1780
1827
|
if (!hasSubmitPayloadContent(payload)) return;
|
|
1781
1828
|
suppressAutoSubmitOnClose = true;
|
|
1782
|
-
window.glimpse?.send?.(payload);
|
|
1829
|
+
window.glimpse?.send?.({ ...payload, draft: true });
|
|
1783
1830
|
}
|
|
1784
1831
|
|
|
1785
1832
|
function updateCommentsUI() {
|
|
@@ -1924,8 +1971,11 @@ window.__reviewReceive = (message) => {
|
|
|
1924
1971
|
if (state.reviewDataRequestId !== message.requestId) return;
|
|
1925
1972
|
clearRefreshableFileState();
|
|
1926
1973
|
clearWorkingTreeCommitState();
|
|
1927
|
-
|
|
1928
|
-
|
|
1974
|
+
const nextFiles = Array.isArray(message.files) ? message.files : [];
|
|
1975
|
+
const nextCommits = Array.isArray(message.commits) ? message.commits : [];
|
|
1976
|
+
reconcileCommitState(nextCommits);
|
|
1977
|
+
reviewData.files = nextFiles;
|
|
1978
|
+
reviewData.commits = nextCommits;
|
|
1929
1979
|
reviewData.branchBaseRef = message.branchBaseRef ?? null;
|
|
1930
1980
|
reviewData.branchMergeBaseSha = message.branchMergeBaseSha ?? null;
|
|
1931
1981
|
reviewData.repositoryHasHead = message.repositoryHasHead === true;
|
|
@@ -2265,7 +2315,7 @@ window.addEventListener("pagehide", submitDraftOnClose);
|
|
|
2265
2315
|
submitButton.addEventListener("click", () => {
|
|
2266
2316
|
const payload = buildSubmitPayload();
|
|
2267
2317
|
suppressAutoSubmitOnClose = true;
|
|
2268
|
-
window.glimpse.send(payload);
|
|
2318
|
+
window.glimpse.send({ ...payload, draft: false });
|
|
2269
2319
|
window.glimpse.close();
|
|
2270
2320
|
});
|
|
2271
2321
|
|
|
@@ -641,9 +641,9 @@ __INLINE_TAILWIND_JS__
|
|
|
641
641
|
<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"><path d="M10.5 2.5l3 3-8 8H2.5V10.5z"/><path d="M9 4l3 3"/></svg>
|
|
642
642
|
</button>
|
|
643
643
|
<button type="button" id="cancel-button" class="cursor-pointer rounded-md border border-review-border bg-review-panel px-3 py-1.5 text-xs font-medium text-review-text hover:bg-red-500/10 hover:text-red-400 hover:border-red-500/30">Cancel</button>
|
|
644
|
-
<button type="button" id="submit-button" class="gh-primary-btn">
|
|
644
|
+
<button type="button" id="submit-button" class="gh-primary-btn" title="Send review feedback to the agent" aria-label="Send review feedback to the agent">
|
|
645
645
|
<svg aria-hidden="true" focusable="false" viewBox="0 0 16 16" width="12" height="12" fill="currentColor"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>
|
|
646
|
-
Submit
|
|
646
|
+
Submit to agent
|
|
647
647
|
</button>
|
|
648
648
|
</div>
|
|
649
649
|
</div>
|
|
@@ -22,7 +22,15 @@ Run `/annotate-last-message` from an interactive pi session. The annotation wind
|
|
|
22
22
|
- section comments for larger chunks of the message, and
|
|
23
23
|
- inline notes tied to individual lines.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The message preview renders headings, lists, blockquotes, emphasis, inline code, strikethrough, links, and fenced code while preserving the original one-based source-line coordinates. Raw HTML is displayed as text. Link destinations are shown as inert text beside the label; the window never navigates to an assistant-provided URL.
|
|
26
|
+
|
|
27
|
+
The window follows the active Pi theme through `ctx.ui.theme`, including Markdown colors and dark/light appearance, with readable local fallbacks when a theme token is unavailable. Opening the window does not change Pi's host theme or settings.
|
|
28
|
+
|
|
29
|
+
When you submit, the extension sends a structured planning-oriented feedback prompt directly to the agent as a follow-up message. Your existing editor text is left untouched. It does not auto-apply changes or rewrite the previous assistant message in place.
|
|
30
|
+
|
|
31
|
+
## Rollback
|
|
32
|
+
|
|
33
|
+
There is no runtime setting or toggle for submit behavior. To roll back the rendering/theme change, reinstall the previous package version or revert this source change and rebuild/reinstall the package; no Pi theme or host setting needs to be restored.
|
|
26
34
|
|
|
27
35
|
## Requirements
|
|
28
36
|
|
|
@@ -3,22 +3,61 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
|
|
|
3
3
|
import { openQuietGlimpse, type QuietGlimpseWindow } from "./quiet-glimpse.js";
|
|
4
4
|
import { composeAnnotateLastMessagePrompt, hasAnnotateLastMessageFeedback } from "./prompt.js";
|
|
5
5
|
import { findLastAssistantMessage } from "./session.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AnnotateLastMessageCancelPayload,
|
|
8
|
+
AnnotateLastMessageInlineComment,
|
|
9
|
+
AnnotateLastMessageSectionComment,
|
|
10
|
+
AnnotateLastMessageSubmitPayload,
|
|
11
|
+
AnnotateLastMessageWindowMessage,
|
|
12
|
+
LastAssistantMessageData,
|
|
13
|
+
} from "./types.js";
|
|
7
14
|
import { buildAnnotateLastMessageHtml } from "./ui.js";
|
|
8
15
|
|
|
16
|
+
function isInlineComment(value: unknown): value is AnnotateLastMessageInlineComment {
|
|
17
|
+
return (
|
|
18
|
+
typeof value === "object" &&
|
|
19
|
+
value != null &&
|
|
20
|
+
"line" in value &&
|
|
21
|
+
typeof value.line === "number" &&
|
|
22
|
+
Number.isInteger(value.line) &&
|
|
23
|
+
value.line > 0 &&
|
|
24
|
+
"body" in value &&
|
|
25
|
+
typeof value.body === "string"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isSectionComment(value: unknown): value is AnnotateLastMessageSectionComment {
|
|
30
|
+
return (
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
value != null &&
|
|
33
|
+
"sectionId" in value &&
|
|
34
|
+
typeof value.sectionId === "string" &&
|
|
35
|
+
"body" in value &&
|
|
36
|
+
typeof value.body === "string"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
9
40
|
function isSubmitPayload(value: unknown): value is AnnotateLastMessageSubmitPayload {
|
|
10
|
-
return
|
|
41
|
+
return (
|
|
42
|
+
typeof value === "object" &&
|
|
43
|
+
value != null &&
|
|
44
|
+
"type" in value &&
|
|
45
|
+
value.type === "submit" &&
|
|
46
|
+
"overallComment" in value &&
|
|
47
|
+
typeof value.overallComment === "string" &&
|
|
48
|
+
"inlineComments" in value &&
|
|
49
|
+
Array.isArray(value.inlineComments) &&
|
|
50
|
+
value.inlineComments.every(isInlineComment) &&
|
|
51
|
+
"sectionComments" in value &&
|
|
52
|
+
Array.isArray(value.sectionComments) &&
|
|
53
|
+
value.sectionComments.every(isSectionComment)
|
|
54
|
+
);
|
|
11
55
|
}
|
|
12
56
|
|
|
13
57
|
function isCancelPayload(value: unknown): value is AnnotateLastMessageCancelPayload {
|
|
14
58
|
return typeof value === "object" && value != null && "type" in value && value.type === "cancel";
|
|
15
59
|
}
|
|
16
60
|
|
|
17
|
-
function appendPrompt(ctx: ExtensionCommandContext, prompt: string): void {
|
|
18
|
-
const prefix = ctx.ui.getEditorText().trim().length > 0 ? "\n\n" : "";
|
|
19
|
-
ctx.ui.pasteToEditor(`${prefix}${prompt}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
61
|
export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
23
62
|
let activeWindow: QuietGlimpseWindow | null = null;
|
|
24
63
|
let annotationOpening = false;
|
|
@@ -61,7 +100,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
61
100
|
const attempt = ++annotationAttempt;
|
|
62
101
|
|
|
63
102
|
try {
|
|
64
|
-
const html = buildAnnotateLastMessageHtml(messageData);
|
|
103
|
+
const html = buildAnnotateLastMessageHtml(messageData, ctx.ui.theme);
|
|
65
104
|
const window = await openQuietGlimpse(html, {
|
|
66
105
|
width: 1440,
|
|
67
106
|
height: 980,
|
|
@@ -111,8 +150,8 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
111
150
|
|
|
112
151
|
const onMessage = (data: unknown): void => {
|
|
113
152
|
if (isSubmitPayload(data) || isCancelPayload(data)) {
|
|
114
|
-
requestWindowClose();
|
|
115
153
|
settle(data);
|
|
154
|
+
requestWindowClose();
|
|
116
155
|
}
|
|
117
156
|
};
|
|
118
157
|
|
|
@@ -143,7 +182,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
143
182
|
void (async (windowMessageSource: QuietGlimpseWindow, sourceData: LastAssistantMessageData) => {
|
|
144
183
|
try {
|
|
145
184
|
const result = await terminalMessagePromise;
|
|
146
|
-
if (suppressedWindows.has(windowMessageSource)) return;
|
|
185
|
+
if (suppressedWindows.has(windowMessageSource) || attempt !== annotationAttempt) return;
|
|
147
186
|
if (result == null) return;
|
|
148
187
|
if (result.type === "cancel") {
|
|
149
188
|
ctx.ui.notify("Annotation cancelled.", "info");
|
|
@@ -155,10 +194,10 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
const prompt = composeAnnotateLastMessagePrompt(sourceData, result);
|
|
158
|
-
|
|
159
|
-
ctx.ui.notify("
|
|
197
|
+
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
198
|
+
ctx.ui.notify("Annotation feedback sent to the agent.", "info");
|
|
160
199
|
} catch (error) {
|
|
161
|
-
if (suppressedWindows.has(windowMessageSource)) return;
|
|
200
|
+
if (suppressedWindows.has(windowMessageSource) || attempt !== annotationAttempt) return;
|
|
162
201
|
const message = error instanceof Error ? error.message : String(error);
|
|
163
202
|
ctx.ui.notify(`Annotation failed: ${message}`, "error");
|
|
164
203
|
}
|
|
@@ -166,6 +205,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
166
205
|
|
|
167
206
|
ctx.ui.notify("Opened native annotation window.", "info");
|
|
168
207
|
} catch (error) {
|
|
208
|
+
if (attempt !== annotationAttempt) return;
|
|
169
209
|
closeActiveWindow({ suppressResults: true });
|
|
170
210
|
const message = error instanceof Error ? error.message : String(error);
|
|
171
211
|
ctx.ui.notify(`Annotation failed: ${message}`, "error");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-last-message",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "A standalone pi extension that adds /annotate-last-message, a native Glimpse UI for annotating the latest assistant reply.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"prompt.ts",
|
|
21
21
|
"quiet-glimpse.ts",
|
|
22
22
|
"session.ts",
|
|
23
|
+
"theme.ts",
|
|
23
24
|
"types.ts",
|
|
24
25
|
"ui.ts",
|
|
25
26
|
"web",
|
|
@@ -57,13 +57,15 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
|
|
|
57
57
|
|
|
58
58
|
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
59
59
|
rl.on("line", (line) => {
|
|
60
|
-
let
|
|
60
|
+
let parsed: unknown;
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
parsed = JSON.parse(line);
|
|
63
63
|
} catch {
|
|
64
64
|
this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
68
|
+
const message = parsed as GlimpseProtocolMessage;
|
|
67
69
|
|
|
68
70
|
switch (message.type) {
|
|
69
71
|
case "ready":
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Theme,
|
|
3
|
+
ThemeBg,
|
|
4
|
+
ThemeColor,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The small public part of Pi's active theme used by the native annotation UI.
|
|
9
|
+
* The command passes ctx.ui.theme directly; no Pi theme singleton is imported.
|
|
10
|
+
*/
|
|
11
|
+
export type AnnotateTheme = Pick<Theme, "appearance" | "colors" | "fg" | "bg">;
|
|
12
|
+
|
|
13
|
+
const DARK_FALLBACK: Readonly<Record<string, string>> = {
|
|
14
|
+
"--color-scheme": "dark",
|
|
15
|
+
"--bg": "#0d1117",
|
|
16
|
+
"--panel": "#151b23",
|
|
17
|
+
"--panel-hover": "#1f242c",
|
|
18
|
+
"--inset": "#0b1118",
|
|
19
|
+
"--border": "#30363d",
|
|
20
|
+
"--border-muted": "#21262d",
|
|
21
|
+
"--text": "#f0f6fc",
|
|
22
|
+
"--muted": "#8a8a8a",
|
|
23
|
+
"--dim": "#585858",
|
|
24
|
+
"--accent": "#f4c95d",
|
|
25
|
+
"--success": "#7ee787",
|
|
26
|
+
"--error": "#ffb3b3",
|
|
27
|
+
"--warning": "#f4c95d",
|
|
28
|
+
"--mdHeading": "#f4c95d",
|
|
29
|
+
"--mdLink": "#7dd3fc",
|
|
30
|
+
"--mdLinkUrl": "#8a8a8a",
|
|
31
|
+
"--mdCode": "#9b7bff",
|
|
32
|
+
"--mdCodeBlock": "inherit",
|
|
33
|
+
"--mdCodeBlockBorder": "#6f42c1",
|
|
34
|
+
"--mdQuote": "#8a8a8a",
|
|
35
|
+
"--mdQuoteBorder": "#6f42c1",
|
|
36
|
+
"--mdHr": "#585858",
|
|
37
|
+
"--mdListBullet": "#f4c95d",
|
|
38
|
+
"--mdBold": "inherit",
|
|
39
|
+
"--mdItalic": "inherit",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const LIGHT_FALLBACK: Readonly<Record<string, string>> = {
|
|
43
|
+
"--color-scheme": "light",
|
|
44
|
+
"--bg": "#f6f8fa",
|
|
45
|
+
"--panel": "#ffffff",
|
|
46
|
+
"--panel-hover": "#f3f4f6",
|
|
47
|
+
"--inset": "#f6f8fa",
|
|
48
|
+
"--border": "#d0d7de",
|
|
49
|
+
"--border-muted": "#afb8c1",
|
|
50
|
+
"--text": "#1f2328",
|
|
51
|
+
"--muted": "#57606a",
|
|
52
|
+
"--dim": "#6e7781",
|
|
53
|
+
"--accent": "#8250df",
|
|
54
|
+
"--success": "#1a7f37",
|
|
55
|
+
"--error": "#cf222e",
|
|
56
|
+
"--warning": "#9a6700",
|
|
57
|
+
"--mdHeading": "#9a6700",
|
|
58
|
+
"--mdLink": "#0969da",
|
|
59
|
+
"--mdLinkUrl": "#57606a",
|
|
60
|
+
"--mdCode": "#8250df",
|
|
61
|
+
"--mdCodeBlock": "#1a7f37",
|
|
62
|
+
"--mdCodeBlockBorder": "#6e7781",
|
|
63
|
+
"--mdQuote": "#57606a",
|
|
64
|
+
"--mdQuoteBorder": "#6e7781",
|
|
65
|
+
"--mdHr": "#8c959f",
|
|
66
|
+
"--mdListBullet": "#8250df",
|
|
67
|
+
"--mdBold": "inherit",
|
|
68
|
+
"--mdItalic": "inherit",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const BASIC_ANSI_COLORS: Readonly<Record<number, string>> = {
|
|
72
|
+
0: "#000000",
|
|
73
|
+
1: "#800000",
|
|
74
|
+
2: "#008000",
|
|
75
|
+
3: "#808000",
|
|
76
|
+
4: "#000080",
|
|
77
|
+
5: "#800080",
|
|
78
|
+
6: "#008080",
|
|
79
|
+
7: "#c0c0c0",
|
|
80
|
+
8: "#808080",
|
|
81
|
+
9: "#ff0000",
|
|
82
|
+
10: "#00ff00",
|
|
83
|
+
11: "#ffff00",
|
|
84
|
+
12: "#0000ff",
|
|
85
|
+
13: "#ff00ff",
|
|
86
|
+
14: "#00ffff",
|
|
87
|
+
15: "#ffffff",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const CUBE_LEVELS = [0, 95, 135, 175, 215, 255] as const;
|
|
91
|
+
const SENTINEL = "X";
|
|
92
|
+
|
|
93
|
+
const FOREGROUND_THEME_VARS: ReadonlyArray<{ token: ThemeColor; cssVar: string }> = [
|
|
94
|
+
{ token: "text", cssVar: "--text" },
|
|
95
|
+
{ token: "muted", cssVar: "--muted" },
|
|
96
|
+
{ token: "dim", cssVar: "--dim" },
|
|
97
|
+
{ token: "accent", cssVar: "--accent" },
|
|
98
|
+
{ token: "success", cssVar: "--success" },
|
|
99
|
+
{ token: "error", cssVar: "--error" },
|
|
100
|
+
{ token: "warning", cssVar: "--warning" },
|
|
101
|
+
{ token: "mdHeading", cssVar: "--mdHeading" },
|
|
102
|
+
{ token: "mdLink", cssVar: "--mdLink" },
|
|
103
|
+
{ token: "mdLinkUrl", cssVar: "--mdLinkUrl" },
|
|
104
|
+
{ token: "mdCode", cssVar: "--mdCode" },
|
|
105
|
+
{ token: "mdCodeBlock", cssVar: "--mdCodeBlock" },
|
|
106
|
+
{ token: "mdCodeBlockBorder", cssVar: "--mdCodeBlockBorder" },
|
|
107
|
+
{ token: "mdQuote", cssVar: "--mdQuote" },
|
|
108
|
+
{ token: "mdQuoteBorder", cssVar: "--mdQuoteBorder" },
|
|
109
|
+
{ token: "mdHr", cssVar: "--mdHr" },
|
|
110
|
+
{ token: "mdListBullet", cssVar: "--mdListBullet" },
|
|
111
|
+
{ token: "border", cssVar: "--border" },
|
|
112
|
+
{ token: "borderMuted", cssVar: "--border-muted" },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const BACKGROUND_THEME_VARS: ReadonlyArray<{ token: ThemeBg; cssVar: string }> = [
|
|
116
|
+
{ token: "customMessageBg", cssVar: "--panel" },
|
|
117
|
+
{ token: "selectedBg", cssVar: "--panel-hover" },
|
|
118
|
+
{ token: "toolPendingBg", cssVar: "--inset" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
function toHex(value: number): string {
|
|
122
|
+
const clamped = Math.min(255, Math.max(0, Math.round(value)));
|
|
123
|
+
return clamped.toString(16).padStart(2, "0");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function xterm256ToHex(index: number): string | null {
|
|
127
|
+
if (!Number.isInteger(index) || index < 0 || index > 255) return null;
|
|
128
|
+
if (index < 16) return BASIC_ANSI_COLORS[index] ?? null;
|
|
129
|
+
if (index >= 232) {
|
|
130
|
+
const value = 8 + (index - 232) * 10;
|
|
131
|
+
const hex = toHex(value);
|
|
132
|
+
return `#${hex}${hex}${hex}`;
|
|
133
|
+
}
|
|
134
|
+
const cubeIndex = index - 16;
|
|
135
|
+
const blue = cubeIndex % 6;
|
|
136
|
+
const green = Math.floor(cubeIndex / 6) % 6;
|
|
137
|
+
const red = Math.floor(cubeIndex / 36) % 6;
|
|
138
|
+
return `#${toHex(CUBE_LEVELS[red])}${toHex(CUBE_LEVELS[green])}${toHex(CUBE_LEVELS[blue])}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function colorValueToHex(value: unknown): string | null {
|
|
142
|
+
if (typeof value !== "object" || value == null) return null;
|
|
143
|
+
const color = value as { kind?: unknown; r?: unknown; g?: unknown; b?: unknown; index?: unknown };
|
|
144
|
+
if (color.kind === "rgb") {
|
|
145
|
+
const channels = [color.r, color.g, color.b];
|
|
146
|
+
if (!channels.every((channel) => typeof channel === "number" && Number.isFinite(channel) && channel >= 0 && channel <= 255)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return `#${toHex(color.r as number)}${toHex(color.g as number)}${toHex(color.b as number)}`;
|
|
150
|
+
}
|
|
151
|
+
if (color.kind === "indexed" && typeof color.index === "number") {
|
|
152
|
+
return xterm256ToHex(color.index);
|
|
153
|
+
}
|
|
154
|
+
// OKLCH values are intentionally handled through theme.fg/theme.bg below.
|
|
155
|
+
// Those public helpers account for Pi's configured terminal color mode.
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ansiColorToHex(value: unknown, background: boolean): string | null {
|
|
160
|
+
if (typeof value !== "string") return null;
|
|
161
|
+
const match = new RegExp(`^\\u001b\\[([0-9;]+)m`).exec(value);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
const params = match[1].split(";").map(Number);
|
|
164
|
+
const [mode, kind, first, second, third] = params;
|
|
165
|
+
const expectedMode = background ? 48 : 38;
|
|
166
|
+
if (mode !== expectedMode) {
|
|
167
|
+
if (!background && ((mode >= 30 && mode <= 37) || (mode >= 90 && mode <= 97))) {
|
|
168
|
+
return xterm256ToHex(mode >= 90 ? mode - 82 : mode - 30);
|
|
169
|
+
}
|
|
170
|
+
if (background && ((mode >= 40 && mode <= 47) || (mode >= 100 && mode <= 107))) {
|
|
171
|
+
return xterm256ToHex(mode >= 100 ? mode - 92 : mode - 40);
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
if (kind === 2 && params.length >= 5) {
|
|
176
|
+
const channels = [first, second, third];
|
|
177
|
+
if (!channels.every((channel) => Number.isInteger(channel) && channel >= 0 && channel <= 255)) return null;
|
|
178
|
+
return `#${toHex(first)}${toHex(second)}${toHex(third)}`;
|
|
179
|
+
}
|
|
180
|
+
if (kind === 5 && params.length >= 3) return xterm256ToHex(first);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function readThemeColor(theme: AnnotateTheme, token: ThemeColor | ThemeBg, background: boolean): string | null {
|
|
185
|
+
let colors: Readonly<Record<string, unknown>> | undefined;
|
|
186
|
+
try {
|
|
187
|
+
colors = theme.colors as Readonly<Record<string, unknown>>;
|
|
188
|
+
} catch {
|
|
189
|
+
colors = undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let colorValue: unknown;
|
|
193
|
+
try {
|
|
194
|
+
colorValue = colors?.[token];
|
|
195
|
+
} catch {
|
|
196
|
+
colorValue = undefined;
|
|
197
|
+
}
|
|
198
|
+
const fromColors = colorValueToHex(colorValue);
|
|
199
|
+
if (fromColors != null) return fromColors;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const styled = background
|
|
203
|
+
? theme.bg(token as ThemeBg, SENTINEL)
|
|
204
|
+
: theme.fg(token as ThemeColor, SENTINEL);
|
|
205
|
+
return ansiColorToHex(styled, background);
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readAppearance(theme: AnnotateTheme | null | undefined): "dark" | "light" {
|
|
212
|
+
try {
|
|
213
|
+
return theme?.appearance === "light" ? "light" : "dark";
|
|
214
|
+
} catch {
|
|
215
|
+
return "dark";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build safe CSS custom properties from the active Pi theme.
|
|
221
|
+
*
|
|
222
|
+
* Every value is either a local fallback or a validated hex color. Theme
|
|
223
|
+
* getters are isolated per token so one broken custom theme cannot make the
|
|
224
|
+
* native annotation window unreadable or prevent it from opening.
|
|
225
|
+
*/
|
|
226
|
+
export function getThemeCssVars(theme?: AnnotateTheme | null): Record<string, string> {
|
|
227
|
+
const vars: Record<string, string> = {
|
|
228
|
+
...(readAppearance(theme) === "light" ? LIGHT_FALLBACK : DARK_FALLBACK),
|
|
229
|
+
};
|
|
230
|
+
if (theme == null) return vars;
|
|
231
|
+
|
|
232
|
+
for (const { token, cssVar } of FOREGROUND_THEME_VARS) {
|
|
233
|
+
const color = readThemeColor(theme, token, false);
|
|
234
|
+
if (color != null) vars[cssVar] = color;
|
|
235
|
+
}
|
|
236
|
+
for (const { token, cssVar } of BACKGROUND_THEME_VARS) {
|
|
237
|
+
const color = readThemeColor(theme, token, true);
|
|
238
|
+
if (color != null) vars[cssVar] = color;
|
|
239
|
+
}
|
|
240
|
+
return vars;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const __testing = {
|
|
244
|
+
ansiColorToHex,
|
|
245
|
+
colorValueToHex,
|
|
246
|
+
xterm256ToHex,
|
|
247
|
+
};
|
|
@@ -2,6 +2,8 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
import type { AnnotateTheme } from "./theme.js";
|
|
6
|
+
import { getThemeCssVars } from "./theme.js";
|
|
5
7
|
import type { LastAssistantMessageData } from "./types.js";
|
|
6
8
|
|
|
7
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -15,9 +17,33 @@ function escapeInlineScriptSource(value: string): string {
|
|
|
15
17
|
return value.replace(/<\/(script)/gi, "<\\/$1");
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
function buildThemeCssBlock(vars: Record<string, string>): string {
|
|
21
|
+
const declarations = Object.entries(vars)
|
|
22
|
+
.map(([key, value]) => ` ${key}: ${value};`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
return `:root {\n${declarations}\n}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildAnnotateLastMessageHtml(
|
|
28
|
+
data: LastAssistantMessageData,
|
|
29
|
+
theme?: AnnotateTheme,
|
|
30
|
+
): string {
|
|
19
31
|
const templateHtml = readFileSync(join(webDir, "index.html"), "utf8");
|
|
32
|
+
const mdRendererJs = escapeInlineScriptSource(readFileSync(join(webDir, "md-renderer.js"), "utf8"));
|
|
20
33
|
const appJs = escapeInlineScriptSource(readFileSync(join(webDir, "app.js"), "utf8"));
|
|
21
34
|
const payload = escapeForInlineScript(JSON.stringify(data));
|
|
22
|
-
|
|
35
|
+
const themeBlock = buildThemeCssBlock(getThemeCssVars(theme));
|
|
36
|
+
const replacements = new Map([
|
|
37
|
+
['"__INLINE_DATA__"', payload],
|
|
38
|
+
["__INLINE_MD_RENDERER_JS__", mdRendererJs],
|
|
39
|
+
["__INLINE_JS__", appJs],
|
|
40
|
+
["__INLINE_THEME__", themeBlock],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
// Replace the original template in one pass so marker-like message data,
|
|
44
|
+
// theme values, or runtime source is never scanned as another placeholder.
|
|
45
|
+
return templateHtml.replace(
|
|
46
|
+
/"__INLINE_DATA__"|__INLINE_MD_RENDERER_JS__|__INLINE_JS__|__INLINE_THEME__/g,
|
|
47
|
+
(marker) => replacements.get(marker) ?? marker,
|
|
48
|
+
);
|
|
23
49
|
}
|