@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/web/app.js CHANGED
@@ -1,211 +1,397 @@
1
1
  /* global window, document */
2
2
 
3
- const messageData = JSON.parse(document.getElementById("annotate-last-message-data").textContent || "{}");
3
+ // Markdown parsing helpers are provided by the md-renderer.js script that
4
+ // ui.ts inlines before this file. Access them via globalThis.__tlhMdRenderer.
4
5
 
6
+ const messageData = JSON.parse(
7
+ document.getElementById("annotate-last-message-data").textContent || "{}",
8
+ );
5
9
  if (!Array.isArray(messageData.lines)) messageData.lines = [];
6
10
  if (!Array.isArray(messageData.sections)) messageData.sections = [];
7
11
 
8
12
  const state = {
9
- overallComment: "",
10
- inlineComments: new Map(),
11
- sectionComments: new Map(),
13
+ overallComment: "",
14
+ inlineComments: new Map(),
15
+ sectionComments: new Map(),
12
16
  };
13
17
 
14
18
  const elements = {
15
- messageLines: document.getElementById("message-lines"),
16
- overallComment: document.getElementById("overall-comment"),
17
- sectionComments: document.getElementById("section-comments"),
18
- status: document.getElementById("status"),
19
- submitButton: document.getElementById("submit-button"),
20
- cancelButton: document.getElementById("cancel-button"),
19
+ messageLines: document.getElementById("message-lines"),
20
+ overallComment: document.getElementById("overall-comment"),
21
+ sectionComments: document.getElementById("section-comments"),
22
+ status: document.getElementById("status"),
23
+ submitButton: document.getElementById("submit-button"),
24
+ cancelButton: document.getElementById("cancel-button"),
21
25
  };
22
26
 
27
+ // Compute fence context once over the complete source message. Section cards
28
+ // reuse this source-line metadata so a section that starts after a blank line
29
+ // inside a fence remains a code preview instead of reopening markdown parsing.
30
+ const fullMessageFenceState = globalThis.__tlhMdRenderer.applyFenceState(
31
+ messageData.lines.map((line) => line.text),
32
+ );
33
+
23
34
  function feedbackCount() {
24
- let count = state.overallComment.trim().length > 0 ? 1 : 0;
25
- for (const value of state.inlineComments.values()) {
26
- if (value.trim().length > 0) count += 1;
27
- }
28
- for (const value of state.sectionComments.values()) {
29
- if (value.trim().length > 0) count += 1;
30
- }
31
- return count;
35
+ let count = state.overallComment.trim().length > 0 ? 1 : 0;
36
+ for (const value of state.inlineComments.values()) {
37
+ if (value.trim().length > 0) count += 1;
38
+ }
39
+ for (const value of state.sectionComments.values()) {
40
+ if (value.trim().length > 0) count += 1;
41
+ }
42
+ return count;
32
43
  }
33
44
 
34
45
  function setStatus(message, status = "idle") {
35
- elements.status.textContent = message;
36
- elements.status.dataset.state = status;
46
+ elements.status.textContent = message;
47
+ elements.status.dataset.state = status;
37
48
  }
38
49
 
39
50
  function updateSubmitState() {
40
- const count = feedbackCount();
41
- elements.submitButton.disabled = count === 0;
42
- if (count === 0) {
43
- setStatus("Add any feedback you want to send back to the editor.");
44
- return;
45
- }
46
- const noun = count === 1 ? "item" : "items";
47
- setStatus(`Ready to submit ${count} feedback ${noun}.`, "ready");
51
+ const count = feedbackCount();
52
+ elements.submitButton.disabled = count === 0;
53
+ if (count === 0) {
54
+ setStatus("Add any feedback you want to send to the agent.");
55
+ return;
56
+ }
57
+ const noun = count === 1 ? "item" : "items";
58
+ setStatus(`Ready to submit ${count} feedback ${noun}.`, "ready");
48
59
  }
49
60
 
50
61
  function setInlineComment(lineNumber, value) {
51
- state.inlineComments.set(lineNumber, value);
52
- updateSubmitState();
62
+ state.inlineComments.set(lineNumber, value);
63
+ updateSubmitState();
53
64
  }
54
65
 
55
66
  function setSectionComment(sectionId, value) {
56
- state.sectionComments.set(sectionId, value);
57
- updateSubmitState();
67
+ state.sectionComments.set(sectionId, value);
68
+ updateSubmitState();
58
69
  }
59
70
 
60
71
  function createInlineEditor(line) {
61
- const container = document.createElement("div");
62
- container.className = "inline-editor";
63
- container.hidden = true;
64
-
65
- const meta = document.createElement("p");
66
- meta.className = "line-meta";
67
- meta.textContent = `Inline note for line ${line.number}`;
68
- container.append(meta);
69
-
70
- const textarea = document.createElement("textarea");
71
- textarea.placeholder = "Explain what should change here, what is unclear, or what planning detail is missing.";
72
- textarea.value = state.inlineComments.get(line.number) || "";
73
- container.append(textarea);
74
-
75
- return { container, textarea };
76
- }
77
-
78
- function createLineRow(line) {
79
- const wrapper = document.createElement("div");
80
- wrapper.className = "message-line";
81
-
82
- const row = document.createElement("div");
83
- row.className = "message-line-row";
84
-
85
- const lineNumber = document.createElement("div");
86
- lineNumber.className = "line-number";
87
- lineNumber.textContent = String(line.number);
88
- row.append(lineNumber);
89
-
90
- const lineText = document.createElement("pre");
91
- lineText.className = "line-text";
92
- lineText.textContent = line.text.length > 0 ? line.text : " ";
93
- row.append(lineText);
94
-
95
- const toggle = document.createElement("button");
96
- toggle.className = "inline-toggle";
97
- toggle.type = "button";
98
- toggle.textContent = "Add inline note";
99
- row.append(toggle);
100
-
101
- const editor = createInlineEditor(line);
102
- const syncToggle = () => {
103
- const hasValue = (state.inlineComments.get(line.number) || "").trim().length > 0;
104
- toggle.dataset.active = hasValue || !editor.container.hidden ? "true" : "false";
105
- toggle.textContent = hasValue ? "Edit inline note" : "Add inline note";
106
- };
107
-
108
- editor.textarea.addEventListener("input", () => {
109
- setInlineComment(line.number, editor.textarea.value);
110
- syncToggle();
111
- });
112
-
113
- toggle.addEventListener("click", () => {
114
- editor.container.hidden = !editor.container.hidden;
115
- syncToggle();
116
- if (!editor.container.hidden) {
117
- editor.textarea.focus();
118
- }
119
- });
120
-
121
- wrapper.append(row);
122
- wrapper.append(editor.container);
123
- syncToggle();
124
- return wrapper;
72
+ const container = document.createElement("div");
73
+ container.className = "inline-editor";
74
+ container.hidden = true;
75
+
76
+ const meta = document.createElement("p");
77
+ meta.className = "line-meta";
78
+ meta.textContent = `Inline note for line ${line.number}`;
79
+ container.append(meta);
80
+
81
+ const textarea = document.createElement("textarea");
82
+ textarea.placeholder =
83
+ "Explain what should change here, what is unclear, or what planning detail is missing.";
84
+ textarea.value = state.inlineComments.get(line.number) || "";
85
+ container.append(textarea);
86
+
87
+ return { container, textarea };
125
88
  }
126
89
 
127
- function createSectionCard(section) {
128
- const card = document.createElement("div");
129
- card.className = "section-card";
130
-
131
- const title = document.createElement("h3");
132
- const lineRange = section.startLine === section.endLine
133
- ? `line ${section.startLine}`
134
- : `lines ${section.startLine}-${section.endLine}`;
135
- title.textContent = `Section ${section.index}`;
136
- card.append(title);
90
+ // ---------------------------------------------------------------------------
91
+ // Markdown DOM rendering helpers
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Render an array of inline tokens into a parent element using
96
+ * createElement/textContent only (no innerHTML).
97
+ */
98
+ function renderInlineTokens(parent, tokens) {
99
+ for (const token of tokens) {
100
+ switch (token.type) {
101
+ case "text":
102
+ parent.append(document.createTextNode(token.text));
103
+ break;
104
+ case "code": {
105
+ const code = document.createElement("code");
106
+ code.textContent = token.text;
107
+ parent.append(code);
108
+ break;
109
+ }
110
+ case "bold": {
111
+ const strong = document.createElement("strong");
112
+ renderInlineTokens(strong, token.children);
113
+ parent.append(strong);
114
+ break;
115
+ }
116
+ case "italic": {
117
+ const em = document.createElement("em");
118
+ renderInlineTokens(em, token.children);
119
+ parent.append(em);
120
+ break;
121
+ }
122
+ case "boldItalic": {
123
+ const strong = document.createElement("strong");
124
+ const em = document.createElement("em");
125
+ renderInlineTokens(em, token.children);
126
+ strong.append(em);
127
+ parent.append(strong);
128
+ break;
129
+ }
130
+ case "strikethrough": {
131
+ const s = document.createElement("s");
132
+ renderInlineTokens(s, token.children);
133
+ parent.append(s);
134
+ break;
135
+ }
136
+ case "link": {
137
+ // Render the link label text inside an <a> element.
138
+ // href is intentionally not set to avoid javascript: URL XSS;
139
+ // the URL is shown as muted text beside the link.
140
+ const a = document.createElement("a");
141
+ renderInlineTokens(a, token.labelTokens);
142
+ const urlSpan = document.createElement("span");
143
+ urlSpan.className = "md-link-url";
144
+ urlSpan.textContent = ` (${token.url})`;
145
+ parent.append(a);
146
+ parent.append(urlSpan);
147
+ break;
148
+ }
149
+ default:
150
+ if (typeof token.text === "string") {
151
+ parent.append(document.createTextNode(token.text));
152
+ }
153
+ }
154
+ }
155
+ }
137
156
 
138
- const meta = document.createElement("p");
139
- meta.className = "section-meta";
140
- meta.textContent = lineRange;
141
- card.append(meta);
157
+ /**
158
+ * Populate a container element with DOM nodes for a classified (non-fence) line.
159
+ * Leading indentation is preserved via text nodes where relevant.
160
+ */
161
+ function renderClassifiedLine(container, classification) {
162
+ switch (classification.type) {
163
+ case "blank":
164
+ container.textContent = " ";
165
+ break;
166
+ case "heading": {
167
+ const h = document.createElement(`h${classification.level}`);
168
+ renderInlineTokens(h, globalThis.__tlhMdRenderer.tokenizeLine(classification.text));
169
+ container.append(h);
170
+ break;
171
+ }
172
+ case "hr":
173
+ container.append(document.createElement("hr"));
174
+ break;
175
+ case "blockquote": {
176
+ const bq = document.createElement("blockquote");
177
+ renderInlineTokens(bq, globalThis.__tlhMdRenderer.tokenizeLine(classification.text));
178
+ container.append(bq);
179
+ break;
180
+ }
181
+ case "ul":
182
+ case "ol": {
183
+ // Preserve leading indentation as a text node.
184
+ if (classification.indent) {
185
+ container.append(document.createTextNode(classification.indent));
186
+ }
187
+ const bullet = document.createElement("span");
188
+ bullet.style.cssText = "color: var(--mdListBullet)";
189
+ bullet.textContent = classification.bullet;
190
+ container.append(bullet);
191
+ container.append(document.createTextNode(" "));
192
+ renderInlineTokens(container, globalThis.__tlhMdRenderer.tokenizeLine(classification.text));
193
+ break;
194
+ }
195
+ default: {
196
+ // 'plain' and any unrecognised types
197
+ const lineText = typeof classification.text === "string" ? classification.text : "";
198
+ renderInlineTokens(container, globalThis.__tlhMdRenderer.tokenizeLine(lineText));
199
+ break;
200
+ }
201
+ }
202
+ }
142
203
 
143
- const preview = document.createElement("div");
144
- preview.className = "section-preview";
145
- preview.textContent = section.text;
146
- card.append(preview);
204
+ /**
205
+ * Build the content of a markdown line-text container element.
206
+ * lineType is one of: 'plain' | 'fence-open' | 'fence-body' | 'fence-close'
207
+ */
208
+ function renderLineContent(container, rawText, lineType) {
209
+ if (lineType === "fence-body") {
210
+ // Code-block body: raw text, no inline markdown, code-block styling.
211
+ container.style.cssText = "border-left: 3px solid var(--mdCodeBlockBorder); padding-left: 8px;";
212
+ const code = document.createElement("code");
213
+ code.style.cssText = "display: block; color: var(--mdCodeBlock, var(--text))";
214
+ code.textContent = rawText.length > 0 ? rawText : " ";
215
+ container.append(code);
216
+ return;
217
+ }
218
+
219
+ if (lineType === "fence-open" || lineType === "fence-close") {
220
+ // Fence delimiter: visible, styled as block-edge marker.
221
+ const span = document.createElement("span");
222
+ span.style.cssText = "color: var(--mdCodeBlockBorder)";
223
+ span.textContent = rawText;
224
+ container.append(span);
225
+ return;
226
+ }
227
+
228
+ // Plain line: classify and render inline markdown.
229
+ if (rawText.trim().length === 0) {
230
+ container.textContent = " ";
231
+ return;
232
+ }
233
+
234
+ renderClassifiedLine(container, globalThis.__tlhMdRenderer.classifyLine(rawText));
235
+ }
147
236
 
148
- const textarea = document.createElement("textarea");
149
- textarea.placeholder = "Describe what should change across this section or what larger concern should be addressed.";
150
- textarea.value = state.sectionComments.get(section.id) || "";
151
- textarea.addEventListener("input", () => {
152
- setSectionComment(section.id, textarea.value);
153
- });
154
- card.append(textarea);
237
+ // ---------------------------------------------------------------------------
238
+ // Line row and section card builders
239
+ // ---------------------------------------------------------------------------
240
+
241
+ function createLineRow(line, lineType) {
242
+ const wrapper = document.createElement("div");
243
+ wrapper.className = "message-line";
244
+ const row = document.createElement("div");
245
+ row.className = "message-line-row";
246
+
247
+ const lineNumber = document.createElement("div");
248
+ lineNumber.className = "line-number";
249
+ lineNumber.textContent = String(line.number);
250
+ row.append(lineNumber);
251
+
252
+ // Use a <div> with markdown-content class so heading/blockquote/hr/etc.
253
+ // CSS rules inside .markdown-content apply correctly.
254
+ const lineText = document.createElement("div");
255
+ lineText.className = "line-text markdown-content";
256
+ renderLineContent(lineText, line.text, lineType);
257
+ row.append(lineText);
258
+ wrapper.append(row);
259
+
260
+ const toggle = document.createElement("button");
261
+ toggle.className = "inline-toggle";
262
+ toggle.type = "button";
263
+ toggle.textContent = "Add inline note";
264
+ row.append(toggle);
265
+
266
+ const editor = createInlineEditor(line);
267
+ const syncToggle = () => {
268
+ const hasValue = (state.inlineComments.get(line.number) || "").trim().length > 0;
269
+ toggle.dataset.active = hasValue || !editor.container.hidden ? "true" : "false";
270
+ toggle.textContent = hasValue ? "Edit inline note" : "Add inline note";
271
+ };
272
+
273
+ editor.textarea.addEventListener("input", () => {
274
+ setInlineComment(line.number, editor.textarea.value);
275
+ syncToggle();
276
+ });
277
+
278
+ toggle.addEventListener("click", () => {
279
+ editor.container.hidden = !editor.container.hidden;
280
+ syncToggle();
281
+ if (!editor.container.hidden) {
282
+ editor.textarea.focus();
283
+ }
284
+ });
285
+
286
+ wrapper.append(editor.container);
287
+ syncToggle();
288
+ return wrapper;
289
+ }
155
290
 
156
- return card;
291
+ function createSectionCard(section) {
292
+ const card = document.createElement("div");
293
+ card.className = "section-card";
294
+
295
+ const title = document.createElement("h3");
296
+ const lineRange =
297
+ section.startLine === section.endLine
298
+ ? `line ${section.startLine}`
299
+ : `lines ${section.startLine}-${section.endLine}`;
300
+ title.textContent = `Section ${section.index}`;
301
+ card.append(title);
302
+
303
+ const meta = document.createElement("p");
304
+ meta.className = "section-meta";
305
+ meta.textContent = lineRange;
306
+ card.append(meta);
307
+
308
+ const preview = document.createElement("div");
309
+ preview.className = "section-preview markdown-content";
310
+
311
+ const startIndex = Number.isInteger(section.startLine) ? section.startLine - 1 : -1;
312
+ const endIndex = Number.isInteger(section.endLine) ? section.endLine : -1;
313
+ const sourceLines =
314
+ startIndex >= 0 && endIndex >= startIndex && endIndex <= messageData.lines.length
315
+ ? messageData.lines.slice(startIndex, endIndex).map((line) => line.text)
316
+ : [];
317
+ for (let idx = 0; idx < sourceLines.length; idx++) {
318
+ const lineDiv = document.createElement("div");
319
+ const sourceIndex = startIndex >= 0 ? startIndex + idx : -1;
320
+ const lineType = fullMessageFenceState[sourceIndex]?.lineType ?? "plain";
321
+ renderLineContent(lineDiv, sourceLines[idx], lineType);
322
+ preview.append(lineDiv);
323
+ }
324
+ card.append(preview);
325
+ const textarea = document.createElement("textarea");
326
+ textarea.placeholder =
327
+ "Describe what should change across this section or what larger concern should be addressed.";
328
+ textarea.value = state.sectionComments.get(section.id) || "";
329
+ textarea.addEventListener("input", () => {
330
+ setSectionComment(section.id, textarea.value);
331
+ });
332
+ card.append(textarea);
333
+
334
+ return card;
157
335
  }
158
336
 
159
337
  function renderMessageLines() {
160
- elements.messageLines.replaceChildren();
161
- for (const line of messageData.lines) {
162
- elements.messageLines.append(createLineRow(line));
163
- }
338
+ elements.messageLines.replaceChildren();
339
+ for (let i = 0; i < messageData.lines.length; i++) {
340
+ elements.messageLines.append(
341
+ createLineRow(messageData.lines[i], fullMessageFenceState[i].lineType),
342
+ );
343
+ }
164
344
  }
165
345
 
166
346
  function renderSectionComments() {
167
- elements.sectionComments.replaceChildren();
168
- if (messageData.sections.length === 0) {
169
- const empty = document.createElement("p");
170
- empty.className = "empty-hint";
171
- empty.textContent = "This message does not have any non-empty sections to annotate.";
172
- elements.sectionComments.append(empty);
173
- return;
174
- }
175
- for (const section of messageData.sections) {
176
- elements.sectionComments.append(createSectionCard(section));
177
- }
347
+ elements.sectionComments.replaceChildren();
348
+ if (messageData.sections.length === 0) {
349
+ const empty = document.createElement("p");
350
+ empty.className = "empty-hint";
351
+ empty.textContent = "This message does not have any non-empty sections to annotate.";
352
+ elements.sectionComments.append(empty);
353
+ return;
354
+ }
355
+ for (const section of messageData.sections) {
356
+ elements.sectionComments.append(createSectionCard(section));
357
+ }
178
358
  }
179
359
 
180
360
  function collectPayload() {
181
- return {
182
- type: "submit",
183
- overallComment: state.overallComment,
184
- inlineComments: Array.from(state.inlineComments.entries()).map(([line, body]) => ({ line, body })),
185
- sectionComments: Array.from(state.sectionComments.entries()).map(([sectionId, body]) => ({ sectionId, body })),
186
- };
361
+ return {
362
+ type: "submit",
363
+ overallComment: state.overallComment,
364
+ inlineComments: Array.from(state.inlineComments.entries()).map(([line, body]) => ({
365
+ line,
366
+ body,
367
+ })),
368
+ sectionComments: Array.from(state.sectionComments.entries()).map(([sectionId, body]) => ({
369
+ sectionId,
370
+ body,
371
+ })),
372
+ };
187
373
  }
188
374
 
189
375
  function sendPayload(payload) {
190
- window.glimpse?.send?.(payload);
191
- window.glimpse?.close?.();
376
+ window.glimpse?.send?.(payload);
377
+ window.glimpse?.close?.();
192
378
  }
193
379
 
194
380
  function submit() {
195
- if (feedbackCount() === 0) {
196
- setStatus("Add at least one comment before submitting.", "error");
197
- return;
198
- }
199
- sendPayload(collectPayload());
381
+ if (feedbackCount() === 0) {
382
+ setStatus("Add at least one comment before submitting.", "error");
383
+ return;
384
+ }
385
+ sendPayload(collectPayload());
200
386
  }
201
387
 
202
388
  function cancel() {
203
- sendPayload({ type: "cancel" });
389
+ sendPayload({ type: "cancel" });
204
390
  }
205
391
 
206
392
  elements.overallComment.addEventListener("input", () => {
207
- state.overallComment = elements.overallComment.value;
208
- updateSubmitState();
393
+ state.overallComment = elements.overallComment.value;
394
+ updateSubmitState();
209
395
  });
210
396
 
211
397
  elements.submitButton.addEventListener("click", submit);
@@ -213,15 +399,15 @@ elements.submitButton.addEventListener("click", submit);
213
399
  elements.cancelButton.addEventListener("click", cancel);
214
400
 
215
401
  document.addEventListener("keydown", (event) => {
216
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
217
- event.preventDefault();
218
- submit();
219
- return;
220
- }
221
- if (event.key === "Escape") {
222
- event.preventDefault();
223
- cancel();
224
- }
402
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
403
+ event.preventDefault();
404
+ submit();
405
+ return;
406
+ }
407
+ if (event.key === "Escape") {
408
+ event.preventDefault();
409
+ cancel();
410
+ }
225
411
  });
226
412
 
227
413
  renderMessageLines();