@diegopetrucci/pi-annotate-last-message 0.1.9 → 0.1.11
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 +10 -2
- package/glimpseui.d.ts +87 -78
- package/index.ts +218 -174
- package/package.json +2 -1
- package/prompt.ts +87 -66
- package/quiet-glimpse.ts +161 -153
- package/session.ts +94 -89
- package/theme.ts +253 -0
- package/types.ts +23 -23
- package/ui.ts +35 -7
- package/web/app.js +348 -162
- package/web/index.html +153 -24
- package/web/md-renderer.js +458 -0
package/prompt.ts
CHANGED
|
@@ -1,93 +1,114 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AnnotateLastMessageInlineComment,
|
|
3
|
+
AnnotateLastMessageSectionComment,
|
|
4
|
+
AnnotateLastMessageSubmitPayload,
|
|
5
|
+
LastAssistantMessageData,
|
|
6
|
+
LastAssistantMessageLine,
|
|
7
|
+
LastAssistantMessageSection,
|
|
8
|
+
} from "./types.js";
|
|
2
9
|
|
|
3
10
|
const EXCERPT_LIMIT = 120;
|
|
4
11
|
|
|
5
12
|
function truncateExcerpt(value: string, limit = EXCERPT_LIMIT): string {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
14
|
+
if (normalized.length === 0) return "(blank line)";
|
|
15
|
+
if (normalized.length <= limit) return normalized;
|
|
16
|
+
return `${normalized.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
function formatLineLabel(line: number): string {
|
|
13
|
-
|
|
20
|
+
return `line ${line}`;
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
function formatLineComment(
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
function formatLineComment(
|
|
24
|
+
comment: AnnotateLastMessageInlineComment,
|
|
25
|
+
line: LastAssistantMessageLine | undefined,
|
|
26
|
+
): string {
|
|
27
|
+
const excerpt = truncateExcerpt(line?.text ?? "");
|
|
28
|
+
return `${formatLineLabel(comment.line)} — “${excerpt}”`;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
function formatSectionLineRange(section: LastAssistantMessageSection): string {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
return section.startLine === section.endLine
|
|
33
|
+
? `line ${section.startLine}`
|
|
34
|
+
: `lines ${section.startLine}-${section.endLine}`;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
function formatSectionComment(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
function formatSectionComment(
|
|
38
|
+
comment: AnnotateLastMessageSectionComment,
|
|
39
|
+
section: LastAssistantMessageSection | undefined,
|
|
40
|
+
): string {
|
|
41
|
+
if (section == null) {
|
|
42
|
+
return "Unknown section";
|
|
43
|
+
}
|
|
44
|
+
return `Section ${section.index} (${formatSectionLineRange(section)}) — “${truncateExcerpt(section.preview)}”`;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
export function hasAnnotateLastMessageFeedback(payload: AnnotateLastMessageSubmitPayload): boolean {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
if (payload.overallComment.trim().length > 0) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (payload.inlineComments.some((comment) => comment.body.trim().length > 0)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return payload.sectionComments.some((comment) => comment.body.trim().length > 0);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
export function composeAnnotateLastMessagePrompt(
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
message: LastAssistantMessageData,
|
|
59
|
+
payload: AnnotateLastMessageSubmitPayload,
|
|
47
60
|
): string {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const lineMap = new Map(message.lines.map((line) => [line.number, line]));
|
|
62
|
+
const sectionMap = new Map(message.sections.map((section) => [section.id, section]));
|
|
63
|
+
const inlineComments = payload.inlineComments
|
|
64
|
+
.filter((comment) => comment.body.trim().length > 0)
|
|
65
|
+
.sort((left, right) => left.line - right.line);
|
|
66
|
+
const sectionComments = payload.sectionComments
|
|
67
|
+
.filter((comment) => comment.body.trim().length > 0)
|
|
68
|
+
.sort(
|
|
69
|
+
(left, right) =>
|
|
70
|
+
(sectionMap.get(left.sectionId)?.index ?? Number.MAX_SAFE_INTEGER) -
|
|
71
|
+
(sectionMap.get(right.sectionId)?.index ?? Number.MAX_SAFE_INTEGER),
|
|
72
|
+
);
|
|
73
|
+
const lines: string[] = [];
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
lines.push("Please revisit your last assistant message using the annotation feedback below.");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("Treat this as planning-oriented feedback:");
|
|
78
|
+
lines.push("- update your explanation, plan, or proposed approach in chat;");
|
|
79
|
+
lines.push("- do not assume any code or file changes have already been applied;");
|
|
80
|
+
lines.push("- do not auto-apply anything outside the normal response flow.");
|
|
81
|
+
lines.push("");
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
const overallComment = payload.overallComment.trim();
|
|
84
|
+
if (overallComment.length > 0) {
|
|
85
|
+
lines.push("## Overall guidance");
|
|
86
|
+
lines.push(overallComment);
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
72
89
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
if (sectionComments.length > 0) {
|
|
91
|
+
lines.push("## Section comments");
|
|
92
|
+
sectionComments.forEach((comment, index) => {
|
|
93
|
+
lines.push(
|
|
94
|
+
`${index + 1}. ${formatSectionComment(comment, sectionMap.get(comment.sectionId))}`,
|
|
95
|
+
);
|
|
96
|
+
lines.push(` ${comment.body.trim()}`);
|
|
97
|
+
lines.push("");
|
|
98
|
+
});
|
|
99
|
+
}
|
|
81
100
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
if (inlineComments.length > 0) {
|
|
102
|
+
lines.push("## Inline comments");
|
|
103
|
+
inlineComments.forEach((comment, index) => {
|
|
104
|
+
lines.push(`${index + 1}. ${formatLineComment(comment, lineMap.get(comment.line))}`);
|
|
105
|
+
lines.push(` ${comment.body.trim()}`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
});
|
|
108
|
+
}
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
lines.push(
|
|
111
|
+
"Please respond by revising your last message or its plan in chat, incorporating the feedback above.",
|
|
112
|
+
);
|
|
113
|
+
return lines.join("\n").trim();
|
|
93
114
|
}
|
package/quiet-glimpse.ts
CHANGED
|
@@ -7,171 +7,179 @@ import { createInterface } from "node:readline";
|
|
|
7
7
|
import type { GlimpseOpenOptions } from "glimpseui";
|
|
8
8
|
|
|
9
9
|
interface NativeHostInfo {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
path: string;
|
|
11
|
+
extraArgs?: string[];
|
|
12
|
+
buildHint?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
interface GlimpseProtocolMessage {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
type?: string;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
screen?: unknown;
|
|
19
|
+
screens?: unknown;
|
|
20
|
+
appearance?: unknown;
|
|
21
|
+
cursor?: unknown;
|
|
22
|
+
cursorTip?: unknown;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export interface QuietGlimpseWindow {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
readonly closed: boolean;
|
|
27
|
+
readonly failure: Error | null;
|
|
28
|
+
on(event: "message", listener: (data: unknown) => void): this;
|
|
29
|
+
on(event: "closed", listener: () => void): this;
|
|
30
|
+
on(event: "error", listener: (error: Error) => void): this;
|
|
31
|
+
removeListener(event: "message", listener: (data: unknown) => void): this;
|
|
32
|
+
removeListener(event: "closed", listener: () => void): this;
|
|
33
|
+
removeListener(event: "error", listener: (error: Error) => void): this;
|
|
34
|
+
send(js: string): void;
|
|
35
|
+
close(): void;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
39
|
+
#proc: ChildProcessWithoutNullStreams;
|
|
40
|
+
#closed = false;
|
|
41
|
+
#pendingHTML: string | null;
|
|
42
|
+
#stderr = "";
|
|
43
|
+
#failure: Error | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(proc: ChildProcessWithoutNullStreams, initialHTML: string) {
|
|
46
|
+
super();
|
|
47
|
+
this.#proc = proc;
|
|
48
|
+
this.#pendingHTML = initialHTML;
|
|
49
|
+
// EventEmitter treats an unobserved `error` as fatal. Keep a permanent
|
|
50
|
+
// fallback while callers attach and detach their lifecycle listeners.
|
|
51
|
+
this.on("error", () => {});
|
|
52
|
+
|
|
53
|
+
proc.stdin.on("error", () => {});
|
|
54
|
+
proc.stderr.on("data", (chunk) => {
|
|
55
|
+
this.#stderr += chunk.toString();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
59
|
+
rl.on("line", (line) => {
|
|
60
|
+
let parsed: unknown;
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(line);
|
|
63
|
+
} catch {
|
|
64
|
+
this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
|
|
68
|
+
const message = parsed as GlimpseProtocolMessage;
|
|
69
|
+
|
|
70
|
+
switch (message.type) {
|
|
71
|
+
case "ready":
|
|
72
|
+
if (this.#pendingHTML != null) {
|
|
73
|
+
this.setHTML(this.#pendingHTML);
|
|
74
|
+
this.#pendingHTML = null;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case "message":
|
|
78
|
+
this.emit("message", message.data);
|
|
79
|
+
break;
|
|
80
|
+
case "closed":
|
|
81
|
+
this.#markClosed();
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
proc.on("error", (error) => this.#reportError(error));
|
|
89
|
+
proc.on("exit", (code, signal) => {
|
|
90
|
+
const stderr = this.#stderr.trim();
|
|
91
|
+
const failed = signal != null || (code != null && code !== 0);
|
|
92
|
+
if (!this.#closed && failed) {
|
|
93
|
+
const message =
|
|
94
|
+
stderr || `Glimpse process exited abnormally (code: ${code}, signal: ${signal}).`;
|
|
95
|
+
this.#reportError(new Error(message));
|
|
96
|
+
}
|
|
97
|
+
this.#markClosed();
|
|
98
|
+
});
|
|
99
|
+
// Spawn failures can emit `error` and then `close` without `exit`.
|
|
100
|
+
proc.on("close", () => this.#markClosed());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
send(js: string): void {
|
|
104
|
+
this.#write({ type: "eval", js });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
close(): void {
|
|
108
|
+
this.#write({ type: "close" });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get closed(): boolean {
|
|
112
|
+
return this.#closed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get failure(): Error | null {
|
|
116
|
+
return this.#failure;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#reportError(error: Error): void {
|
|
120
|
+
this.#failure ??= error;
|
|
121
|
+
this.emit("error", error);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#markClosed(): void {
|
|
125
|
+
if (this.#closed) return;
|
|
126
|
+
this.#closed = true;
|
|
127
|
+
this.emit("closed");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#write(obj: Record<string, unknown>): void {
|
|
131
|
+
if (this.#closed) return;
|
|
132
|
+
this.#proc.stdin.write(`${JSON.stringify(obj)}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setHTML(html: string): void {
|
|
136
|
+
this.#write({ type: "html", html: Buffer.from(html).toString("base64") });
|
|
137
|
+
}
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
async function getNativeHostInfo(): Promise<NativeHostInfo> {
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
const glimpseModule = (await import("glimpseui")) as unknown as {
|
|
142
|
+
getNativeHostInfo: () => NativeHostInfo;
|
|
143
|
+
};
|
|
144
|
+
return glimpseModule.getNativeHostInfo();
|
|
140
145
|
}
|
|
141
146
|
|
|
142
|
-
export async function openQuietGlimpse(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
147
|
+
export async function openQuietGlimpse(
|
|
148
|
+
html: string,
|
|
149
|
+
options: GlimpseOpenOptions = {},
|
|
150
|
+
): Promise<QuietGlimpseWindow> {
|
|
151
|
+
const host = await getNativeHostInfo();
|
|
152
|
+
if (!existsSync(host.path)) {
|
|
153
|
+
const hint = host.buildHint ? ` ${host.buildHint}` : "";
|
|
154
|
+
throw new Error(`Glimpse host not found at '${host.path}'.${hint}`);
|
|
155
|
+
}
|
|
156
|
+
const args: string[] = [];
|
|
157
|
+
|
|
158
|
+
if (options.width != null) args.push("--width", String(options.width));
|
|
159
|
+
if (options.height != null) args.push("--height", String(options.height));
|
|
160
|
+
if (options.title != null) args.push("--title", options.title);
|
|
161
|
+
if (options.frameless) args.push("--frameless");
|
|
162
|
+
if (options.floating) args.push("--floating");
|
|
163
|
+
if (options.transparent) args.push("--transparent");
|
|
164
|
+
if (options.clickThrough) args.push("--click-through");
|
|
165
|
+
if (options.hidden) args.push("--hidden");
|
|
166
|
+
if (options.autoClose) args.push("--auto-close");
|
|
167
|
+
if (options.x != null) args.push(`--x=${options.x}`);
|
|
168
|
+
if (options.y != null) args.push(`--y=${options.y}`);
|
|
169
|
+
if (options.cursorOffset?.x != null) args.push(`--cursor-offset-x=${options.cursorOffset.x}`);
|
|
170
|
+
if (options.cursorOffset?.y != null) args.push(`--cursor-offset-y=${options.cursorOffset.y}`);
|
|
171
|
+
if (options.cursorAnchor != null) args.push("--cursor-anchor", options.cursorAnchor);
|
|
172
|
+
if (options.followMode != null) args.push("--follow-mode", options.followMode);
|
|
173
|
+
if (options.followCursor) args.push("--follow-cursor");
|
|
174
|
+
|
|
175
|
+
const proc = spawn(host.path, [...(host.extraArgs ?? []), ...args], {
|
|
176
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
177
|
+
windowsHide: process.platform === "win32",
|
|
178
|
+
env: {
|
|
179
|
+
...process.env,
|
|
180
|
+
OS_ACTIVITY_MODE: process.env.OS_ACTIVITY_MODE ?? "disable",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return new QuietGlimpseWindowImpl(proc, html);
|
|
177
185
|
}
|