@diegopetrucci/pi-annotate-last-message 0.1.7 → 0.1.9

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.
@@ -1 +1 @@
1
- 0.83.0
1
+ 0.84.4
package/index.ts CHANGED
@@ -21,12 +21,15 @@ function appendPrompt(ctx: ExtensionCommandContext, prompt: string): void {
21
21
 
22
22
  export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
23
23
  let activeWindow: QuietGlimpseWindow | null = null;
24
+ let annotationOpening = false;
25
+ let annotationAttempt = 0;
24
26
  const suppressedWindows = new WeakSet<QuietGlimpseWindow>();
25
27
 
26
28
  function closeActiveWindow(options: { suppressResults?: boolean } = {}): void {
27
- if (activeWindow == null) return;
29
+ annotationAttempt += 1;
28
30
  const windowToClose = activeWindow;
29
31
  activeWindow = null;
32
+ if (windowToClose == null) return;
30
33
  if (options.suppressResults) {
31
34
  suppressedWindows.add(windowToClose);
32
35
  }
@@ -42,7 +45,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
42
45
  ctx.ui.notify("annotate-last-message requires interactive mode.", "error");
43
46
  return;
44
47
  }
45
- if (activeWindow != null) {
48
+ if (annotationOpening || activeWindow != null) {
46
49
  ctx.ui.notify("A last-message annotation window is already open.", "warning");
47
50
  return;
48
51
  }
@@ -54,6 +57,8 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
54
57
  }
55
58
 
56
59
  const messageData = messageResult.data;
60
+ annotationOpening = true;
61
+ const attempt = ++annotationAttempt;
57
62
 
58
63
  try {
59
64
  const html = buildAnnotateLastMessageHtml(messageData);
@@ -62,13 +67,28 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
62
67
  height: 980,
63
68
  title: "annotate last message",
64
69
  });
70
+ if (attempt !== annotationAttempt) {
71
+ suppressedWindows.add(window);
72
+ window.close();
73
+ return;
74
+ }
65
75
  activeWindow = window;
76
+ if (window.failure != null) throw window.failure;
77
+ if (window.closed) throw new Error("Glimpse closed while the annotation window was starting.");
66
78
 
67
79
  const terminalMessagePromise = new Promise<AnnotateLastMessageSubmitPayload | AnnotateLastMessageCancelPayload | null>(
68
80
  (resolve, reject) => {
69
81
  let settled = false;
70
82
  let closeTimer: ReturnType<typeof setTimeout> | null = null;
71
83
 
84
+ const requestWindowClose = (): void => {
85
+ try {
86
+ window.close();
87
+ } catch {
88
+ // Ignore races when the native process has already exited.
89
+ }
90
+ };
91
+
72
92
  const cleanup = (): void => {
73
93
  if (closeTimer != null) {
74
94
  clearTimeout(closeTimer);
@@ -91,6 +111,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
91
111
 
92
112
  const onMessage = (data: unknown): void => {
93
113
  if (isSubmitPayload(data) || isCancelPayload(data)) {
114
+ requestWindowClose();
94
115
  settle(data);
95
116
  }
96
117
  };
@@ -106,6 +127,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
106
127
  const onError = (error: Error): void => {
107
128
  if (settled) return;
108
129
  settled = true;
130
+ requestWindowClose();
109
131
  cleanup();
110
132
  reject(error);
111
133
  };
@@ -113,6 +135,8 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
113
135
  window.on("message", onMessage);
114
136
  window.on("closed", onClosed);
115
137
  window.on("error", onError);
138
+ if (window.failure != null) onError(window.failure);
139
+ else if (window.closed) onClosed();
116
140
  },
117
141
  );
118
142
 
@@ -145,6 +169,8 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
145
169
  closeActiveWindow({ suppressResults: true });
146
170
  const message = error instanceof Error ? error.message : String(error);
147
171
  ctx.ui.notify(`Annotation failed: ${message}`, "error");
172
+ } finally {
173
+ annotationOpening = false;
148
174
  }
149
175
  }
150
176
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-annotate-last-message",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",
package/quiet-glimpse.ts CHANGED
@@ -23,6 +23,8 @@ interface GlimpseProtocolMessage {
23
23
  }
24
24
 
25
25
  export interface QuietGlimpseWindow {
26
+ readonly closed: boolean;
27
+ readonly failure: Error | null;
26
28
  on(event: "message", listener: (data: unknown) => void): this;
27
29
  on(event: "closed", listener: () => void): this;
28
30
  on(event: "error", listener: (error: Error) => void): this;
@@ -38,11 +40,15 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
38
40
  #closed = false;
39
41
  #pendingHTML: string | null;
40
42
  #stderr = "";
43
+ #failure: Error | null = null;
41
44
 
42
45
  constructor(proc: ChildProcessWithoutNullStreams, initialHTML: string) {
43
46
  super();
44
47
  this.#proc = proc;
45
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", () => {});
46
52
 
47
53
  proc.stdin.on("error", () => {});
48
54
  proc.stderr.on("data", (chunk) => {
@@ -55,7 +61,7 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
55
61
  try {
56
62
  message = JSON.parse(line) as GlimpseProtocolMessage;
57
63
  } catch {
58
- this.emit("error", new Error(`Malformed glimpse protocol line: ${line}`));
64
+ this.#reportError(new Error(`Malformed glimpse protocol line: ${line}`));
59
65
  return;
60
66
  }
61
67
 
@@ -77,16 +83,18 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
77
83
  }
78
84
  });
79
85
 
80
- proc.on("error", (error) => this.emit("error", error));
86
+ proc.on("error", (error) => this.#reportError(error));
81
87
  proc.on("exit", (code, signal) => {
82
88
  const stderr = this.#stderr.trim();
83
89
  const failed = signal != null || (code != null && code !== 0);
84
90
  if (!this.#closed && failed) {
85
91
  const message = stderr || `Glimpse process exited abnormally (code: ${code}, signal: ${signal}).`;
86
- this.emit("error", new Error(message));
92
+ this.#reportError(new Error(message));
87
93
  }
88
94
  this.#markClosed();
89
95
  });
96
+ // Spawn failures can emit `error` and then `close` without `exit`.
97
+ proc.on("close", () => this.#markClosed());
90
98
  }
91
99
 
92
100
  send(js: string): void {
@@ -97,6 +105,19 @@ class QuietGlimpseWindowImpl extends EventEmitter implements QuietGlimpseWindow
97
105
  this.#write({ type: "close" });
98
106
  }
99
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
+
100
121
  #markClosed(): void {
101
122
  if (this.#closed) return;
102
123
  this.#closed = true;