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