critique 0.1.118 → 0.1.119

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.
Files changed (222) hide show
  1. package/dist/agentation-api.d.ts +21 -0
  2. package/dist/agentation-api.d.ts.map +1 -0
  3. package/dist/agentation-api.js +152 -0
  4. package/dist/agentation-api.preview.e2e.test.d.ts +2 -0
  5. package/dist/agentation-api.preview.e2e.test.d.ts.map +1 -0
  6. package/dist/agentation-api.preview.e2e.test.js +293 -0
  7. package/dist/agentation-widget.d.ts +2 -0
  8. package/dist/agentation-widget.d.ts.map +1 -0
  9. package/dist/agentation-widget.js +67 -0
  10. package/dist/ansi-html.d.ts +42 -0
  11. package/dist/ansi-html.d.ts.map +1 -0
  12. package/dist/ansi-html.js +327 -0
  13. package/dist/ansi-output.d.ts +22 -0
  14. package/dist/ansi-output.d.ts.map +1 -0
  15. package/dist/ansi-output.js +154 -0
  16. package/dist/ansi-output.test.d.ts +2 -0
  17. package/dist/ansi-output.test.d.ts.map +1 -0
  18. package/dist/ansi-output.test.js +189 -0
  19. package/dist/balance-delimiters.d.ts +21 -0
  20. package/dist/balance-delimiters.d.ts.map +1 -0
  21. package/dist/balance-delimiters.js +126 -0
  22. package/dist/balance-delimiters.test.d.ts +2 -0
  23. package/dist/balance-delimiters.test.d.ts.map +1 -0
  24. package/dist/balance-delimiters.test.js +468 -0
  25. package/dist/cli-scroll.test.d.ts +2 -0
  26. package/dist/cli-scroll.test.d.ts.map +1 -0
  27. package/dist/cli-scroll.test.js +55 -0
  28. package/dist/cli.d.ts +9 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +2103 -0
  31. package/dist/components/diff-view.d.ts +11 -0
  32. package/dist/components/diff-view.d.ts.map +1 -0
  33. package/dist/components/diff-view.js +77 -0
  34. package/dist/components/diff-view.test.d.ts +2 -0
  35. package/dist/components/diff-view.test.d.ts.map +1 -0
  36. package/dist/components/diff-view.test.js +162 -0
  37. package/dist/components/directory-tree-view.d.ts +16 -0
  38. package/dist/components/directory-tree-view.d.ts.map +1 -0
  39. package/dist/components/directory-tree-view.js +63 -0
  40. package/dist/components/index.d.ts +3 -0
  41. package/dist/components/index.d.ts.map +1 -0
  42. package/dist/components/index.js +4 -0
  43. package/dist/diff-utils.d.ts +176 -0
  44. package/dist/diff-utils.d.ts.map +1 -0
  45. package/dist/diff-utils.js +510 -0
  46. package/dist/diff-utils.test.d.ts +2 -0
  47. package/dist/diff-utils.test.d.ts.map +1 -0
  48. package/dist/diff-utils.test.js +617 -0
  49. package/dist/directory-tree.d.ts +43 -0
  50. package/dist/directory-tree.d.ts.map +1 -0
  51. package/dist/directory-tree.js +118 -0
  52. package/dist/directory-tree.test.d.ts +2 -0
  53. package/dist/directory-tree.test.d.ts.map +1 -0
  54. package/dist/directory-tree.test.js +289 -0
  55. package/dist/dropdown.d.ts +26 -0
  56. package/dist/dropdown.d.ts.map +1 -0
  57. package/dist/dropdown.js +172 -0
  58. package/dist/dropdown.test.d.ts +2 -0
  59. package/dist/dropdown.test.d.ts.map +1 -0
  60. package/dist/dropdown.test.js +106 -0
  61. package/dist/filter-submodule.e2e.test.d.ts +2 -0
  62. package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
  63. package/dist/filter-submodule.e2e.test.js +109 -0
  64. package/dist/hooks/use-copy-selection.d.ts +29 -0
  65. package/dist/hooks/use-copy-selection.d.ts.map +1 -0
  66. package/dist/hooks/use-copy-selection.js +114 -0
  67. package/dist/image.d.ts +133 -0
  68. package/dist/image.d.ts.map +1 -0
  69. package/dist/image.js +186 -0
  70. package/dist/image.test.d.ts +2 -0
  71. package/dist/image.test.d.ts.map +1 -0
  72. package/dist/image.test.js +316 -0
  73. package/dist/license.d.ts +14 -0
  74. package/dist/license.d.ts.map +1 -0
  75. package/dist/license.js +63 -0
  76. package/dist/logger.d.ts +9 -0
  77. package/dist/logger.d.ts.map +1 -0
  78. package/dist/logger.js +78 -0
  79. package/dist/monochrome.d.ts +34 -0
  80. package/dist/monochrome.d.ts.map +1 -0
  81. package/dist/monochrome.js +613 -0
  82. package/dist/monotone.d.ts +22 -0
  83. package/dist/monotone.d.ts.map +1 -0
  84. package/dist/monotone.js +185 -0
  85. package/dist/og-layout.test.d.ts +2 -0
  86. package/dist/og-layout.test.d.ts.map +1 -0
  87. package/dist/og-layout.test.js +315 -0
  88. package/dist/opentui-image.d.ts +119 -0
  89. package/dist/opentui-image.d.ts.map +1 -0
  90. package/dist/opentui-image.js +288 -0
  91. package/dist/opentui-pdf.d.ts +140 -0
  92. package/dist/opentui-pdf.d.ts.map +1 -0
  93. package/dist/opentui-pdf.js +376 -0
  94. package/dist/parsers-config.d.ts +19 -0
  95. package/dist/parsers-config.d.ts.map +1 -0
  96. package/dist/parsers-config.js +271 -0
  97. package/dist/patch-terminal-dimensions.d.ts +2 -0
  98. package/dist/patch-terminal-dimensions.d.ts.map +1 -0
  99. package/dist/patch-terminal-dimensions.js +45 -0
  100. package/dist/review/acp-client.d.ts +83 -0
  101. package/dist/review/acp-client.d.ts.map +1 -0
  102. package/dist/review/acp-client.js +626 -0
  103. package/dist/review/acp-stream-display.d.ts +54 -0
  104. package/dist/review/acp-stream-display.d.ts.map +1 -0
  105. package/dist/review/acp-stream-display.js +159 -0
  106. package/dist/review/acp-stream-display.test.d.ts +2 -0
  107. package/dist/review/acp-stream-display.test.d.ts.map +1 -0
  108. package/dist/review/acp-stream-display.test.js +206 -0
  109. package/dist/review/diagram-parser.d.ts +34 -0
  110. package/dist/review/diagram-parser.d.ts.map +1 -0
  111. package/dist/review/diagram-parser.js +214 -0
  112. package/dist/review/diagram-parser.test.d.ts +2 -0
  113. package/dist/review/diagram-parser.test.d.ts.map +1 -0
  114. package/dist/review/diagram-parser.test.js +328 -0
  115. package/dist/review/fixtures/simple-response.json +98 -0
  116. package/dist/review/fixtures/tool-call-response.json +307 -0
  117. package/dist/review/hunk-parser.d.ts +121 -0
  118. package/dist/review/hunk-parser.d.ts.map +1 -0
  119. package/dist/review/hunk-parser.js +570 -0
  120. package/dist/review/hunk-parser.test.d.ts +2 -0
  121. package/dist/review/hunk-parser.test.d.ts.map +1 -0
  122. package/dist/review/hunk-parser.test.js +1073 -0
  123. package/dist/review/index.d.ts +10 -0
  124. package/dist/review/index.d.ts.map +1 -0
  125. package/dist/review/index.js +10 -0
  126. package/dist/review/review-app.d.ts +40 -0
  127. package/dist/review/review-app.d.ts.map +1 -0
  128. package/dist/review/review-app.js +471 -0
  129. package/dist/review/review-app.test.d.ts +2 -0
  130. package/dist/review/review-app.test.d.ts.map +1 -0
  131. package/dist/review/review-app.test.js +1402 -0
  132. package/dist/review/session-context.d.ts +11 -0
  133. package/dist/review/session-context.d.ts.map +1 -0
  134. package/dist/review/session-context.js +136 -0
  135. package/dist/review/session-context.test.d.ts +2 -0
  136. package/dist/review/session-context.test.d.ts.map +1 -0
  137. package/dist/review/session-context.test.js +147 -0
  138. package/dist/review/storage.d.ts +62 -0
  139. package/dist/review/storage.d.ts.map +1 -0
  140. package/dist/review/storage.js +173 -0
  141. package/dist/review/stream-display.d.ts +20 -0
  142. package/dist/review/stream-display.d.ts.map +1 -0
  143. package/dist/review/stream-display.js +90 -0
  144. package/dist/review/stream-display.test.d.ts +2 -0
  145. package/dist/review/stream-display.test.d.ts.map +1 -0
  146. package/dist/review/stream-display.test.js +176 -0
  147. package/dist/review/types.d.ts +96 -0
  148. package/dist/review/types.d.ts.map +1 -0
  149. package/dist/review/types.js +3 -0
  150. package/dist/review/yaml-watcher.d.ts +16 -0
  151. package/dist/review/yaml-watcher.d.ts.map +1 -0
  152. package/dist/review/yaml-watcher.js +200 -0
  153. package/dist/routes/annotations-context.d.ts +21 -0
  154. package/dist/routes/annotations-context.d.ts.map +1 -0
  155. package/dist/routes/annotations-context.js +60 -0
  156. package/dist/stdin-pager.test.d.ts +2 -0
  157. package/dist/stdin-pager.test.d.ts.map +1 -0
  158. package/dist/stdin-pager.test.js +400 -0
  159. package/dist/store.d.ts +12 -0
  160. package/dist/store.d.ts.map +1 -0
  161. package/dist/store.js +42 -0
  162. package/dist/themes/github.json +247 -0
  163. package/dist/themes.d.ts +59 -0
  164. package/dist/themes.d.ts.map +1 -0
  165. package/dist/themes.js +241 -0
  166. package/dist/utils.d.ts +2 -0
  167. package/dist/utils.d.ts.map +1 -0
  168. package/dist/utils.js +13 -0
  169. package/dist/web-utils.d.ts +118 -0
  170. package/dist/web-utils.d.ts.map +1 -0
  171. package/dist/web-utils.js +623 -0
  172. package/dist/web-utils.test.d.ts +2 -0
  173. package/dist/web-utils.test.d.ts.map +1 -0
  174. package/dist/web-utils.test.js +205 -0
  175. package/dist/worker.d.ts +28 -0
  176. package/dist/worker.d.ts.map +1 -0
  177. package/dist/worker.js +656 -0
  178. package/package.json +15 -7
  179. package/public/agentation-widget.js +8815 -0
  180. package/src/agentation-api.preview.e2e.test.ts +368 -0
  181. package/src/agentation-api.ts +188 -0
  182. package/src/agentation-widget.tsx +84 -0
  183. package/src/ansi-html.ts +105 -52
  184. package/src/ansi-output.test.ts +1 -1
  185. package/src/balance-delimiters.test.ts +1 -1
  186. package/src/cli-scroll.test.tsx +2 -2
  187. package/src/cli.tsx +47 -47
  188. package/src/components/diff-view.test.tsx +2 -2
  189. package/src/components/diff-view.tsx +3 -3
  190. package/src/components/directory-tree-view.tsx +3 -3
  191. package/src/components/index.ts +2 -2
  192. package/src/diff-utils.test.ts +4 -4
  193. package/src/directory-tree.test.tsx +2 -2
  194. package/src/dropdown.test.tsx +2 -2
  195. package/src/dropdown.tsx +1 -1
  196. package/src/image.test.ts +10 -10
  197. package/src/image.ts +5 -5
  198. package/src/og-layout.test.ts +1 -1
  199. package/src/parsers-config.ts +7 -6
  200. package/src/review/acp-client.ts +2 -2
  201. package/src/review/acp-stream-display.test.ts +1 -1
  202. package/src/review/diagram-parser.test.ts +1 -1
  203. package/src/review/hunk-parser.test.ts +6 -6
  204. package/src/review/hunk-parser.ts +2 -2
  205. package/src/review/index.ts +9 -9
  206. package/src/review/review-app.test.tsx +3 -3
  207. package/src/review/review-app.tsx +14 -14
  208. package/src/review/session-context.test.ts +2 -2
  209. package/src/review/session-context.ts +1 -1
  210. package/src/review/storage.ts +1 -1
  211. package/src/review/stream-display.test.tsx +1 -1
  212. package/src/review/stream-display.tsx +3 -3
  213. package/src/review/yaml-watcher.ts +2 -2
  214. package/src/routes/annotations-context.tsx +157 -0
  215. package/src/store.ts +1 -1
  216. package/src/themes.ts +4 -5
  217. package/src/web-utils.test.ts +5 -5
  218. package/src/web-utils.tsx +14 -14
  219. package/src/worker.tsx +65 -3
  220. package/LICENSE +0 -21
  221. package/README.md +0 -344
  222. package/dist/critique +0 -0
@@ -0,0 +1,21 @@
1
+ import { Hono } from "hono";
2
+ type Bindings = {
3
+ CommentRoom: {
4
+ idFromName(name: string): {
5
+ toString(): string;
6
+ };
7
+ get(id: {
8
+ toString(): string;
9
+ }): {
10
+ fetch(request: Request): Promise<Response>;
11
+ };
12
+ };
13
+ };
14
+ declare const api: Hono<{
15
+ Bindings: Bindings;
16
+ Variables: {
17
+ userId: string;
18
+ };
19
+ }, import("hono/types").BlankSchema, "/">;
20
+ export { api as agentationApi };
21
+ //# sourceMappingURL=agentation-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentation-api.d.ts","sourceRoot":"","sources":["../src/agentation-api.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAE3B,KAAK,QAAQ,GAAG;IACd,WAAW,EAAE;QACX,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,CAAA;QAChD,GAAG,CAAC,EAAE,EAAE;YAAE,QAAQ,IAAI,MAAM,CAAA;SAAE,GAAG;YAC/B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;SAC3C,CAAA;KACF,CAAA;CACF,CAAA;AAED,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE;yCAAK,CAAA;AAuK7E,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,CAAA"}
@@ -0,0 +1,152 @@
1
+ // Hono sub-app mounting the Agentation HTTP API.
2
+ // Proxies all annotation/session/SSE requests to the CommentRoom Durable Object.
3
+ // Mounted at /a in the main worker, so Agentation client calls /a/sessions/:id/annotations etc.
4
+ //
5
+ // The agents SDK (built on partyserver) requires an x-partykit-room header
6
+ // on every request to a DO stub so it can initialize its name. Without this
7
+ // header, the DO throws "Missing namespace or room headers". We inject it
8
+ // via proxyToDO() on every proxied request.
9
+ import { Hono } from "hono";
10
+ const api = new Hono();
11
+ // Helper: proxy a request to the CommentRoom DO for a given session ID.
12
+ // Sets x-partykit-room so the agents SDK can initialize the DO name.
13
+ function proxyToDO(env, sessionId, url, init) {
14
+ const doId = env.CommentRoom.idFromName(sessionId);
15
+ const stub = env.CommentRoom.get(doId);
16
+ const headers = new Headers(init?.headers);
17
+ headers.set("x-partykit-room", sessionId);
18
+ return stub.fetch(new Request(url, { ...init, headers }));
19
+ }
20
+ // Helper: extract session ID from an annotation ID ({sessionId}_{uuid})
21
+ function parseAnnotationId(annotationId) {
22
+ const idx = annotationId.indexOf("_");
23
+ if (idx === -1)
24
+ return null;
25
+ return {
26
+ sessionId: annotationId.slice(0, idx),
27
+ rawId: annotationId,
28
+ };
29
+ }
30
+ // Health check (Agentation polls this)
31
+ api.get("/health", (c) => c.json({ status: "ok" }));
32
+ // Create session (Agentation calls POST /sessions with { url })
33
+ api.post("/sessions", async (c) => {
34
+ const body = await c.req.json();
35
+ const url = body.url;
36
+ const match = url?.match(/\/v\/([a-f0-9]{16,32})/);
37
+ const sessionId = match?.[1] || crypto.randomUUID();
38
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/session");
39
+ const session = await resp.json();
40
+ return c.json({ id: sessionId, ...session });
41
+ });
42
+ // List sessions — return empty for v1
43
+ api.get("/sessions", (c) => {
44
+ return c.json({ sessions: [] });
45
+ });
46
+ // Get session with all annotations
47
+ api.get("/sessions/:id", async (c) => {
48
+ const sessionId = c.req.param("id");
49
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/session");
50
+ return new Response(resp.body, resp);
51
+ });
52
+ // Create annotation within a session
53
+ api.post("/sessions/:id/annotations", async (c) => {
54
+ const sessionId = c.req.param("id");
55
+ const body = await c.req.json();
56
+ // Prefix annotation ID with session ID for routing
57
+ if (typeof body.id !== "string" || !body.id.startsWith(`${sessionId}_`)) {
58
+ body.id = `${sessionId}_${crypto.randomUUID()}`;
59
+ }
60
+ // Support legacy createdBy field from old clients
61
+ const legacyCreatedBy = typeof body.createdBy === "string" ? body.createdBy : undefined;
62
+ body.authorId = body.authorId || legacyCreatedBy || c.get("userId");
63
+ delete body.createdBy;
64
+ body.sessionId = sessionId;
65
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/annotations", {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify(body),
69
+ });
70
+ return new Response(resp.body, resp);
71
+ });
72
+ // Submit action request for a session
73
+ api.post("/sessions/:id/action", async (c) => {
74
+ const sessionId = c.req.param("id");
75
+ const body = await c.req.text();
76
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/action", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body,
80
+ });
81
+ return new Response(resp.body, resp);
82
+ });
83
+ // Get annotation by ID (annotation IDs encode session: {sessionId}_{uuid})
84
+ api.get("/annotations/:id", async (c) => {
85
+ const parsed = parseAnnotationId(c.req.param("id"));
86
+ if (!parsed) {
87
+ return c.json({ error: "Invalid annotation ID format" }, 400);
88
+ }
89
+ const resp = await proxyToDO(c.env, parsed.sessionId, `https://internal/api/annotations/${parsed.rawId}`);
90
+ return new Response(resp.body, resp);
91
+ });
92
+ // Update annotation
93
+ api.patch("/annotations/:id", async (c) => {
94
+ const parsed = parseAnnotationId(c.req.param("id"));
95
+ if (!parsed) {
96
+ return c.json({ error: "Invalid annotation ID format" }, 400);
97
+ }
98
+ const body = await c.req.text();
99
+ const resp = await proxyToDO(c.env, parsed.sessionId, `https://internal/api/annotations/${parsed.rawId}`, {
100
+ method: "PATCH",
101
+ headers: { "Content-Type": "application/json" },
102
+ body,
103
+ });
104
+ return new Response(resp.body, resp);
105
+ });
106
+ // Delete annotation
107
+ api.delete("/annotations/:id", async (c) => {
108
+ const parsed = parseAnnotationId(c.req.param("id"));
109
+ if (!parsed) {
110
+ return c.json({ error: "Invalid annotation ID format" }, 400);
111
+ }
112
+ const resp = await proxyToDO(c.env, parsed.sessionId, `https://internal/api/annotations/${parsed.rawId}`, {
113
+ method: "DELETE",
114
+ });
115
+ return new Response(resp.body, resp);
116
+ });
117
+ // Thread messages
118
+ api.post("/annotations/:id/thread", async (c) => {
119
+ const parsed = parseAnnotationId(c.req.param("id"));
120
+ if (!parsed) {
121
+ return c.json({ error: "Invalid annotation ID format" }, 400);
122
+ }
123
+ const body = await c.req.text();
124
+ const resp = await proxyToDO(c.env, parsed.sessionId, `https://internal/api/annotations/${parsed.rawId}/thread`, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json" },
127
+ body,
128
+ });
129
+ return new Response(resp.body, resp);
130
+ });
131
+ // Pending annotations for a session
132
+ api.get("/sessions/:id/pending", async (c) => {
133
+ const sessionId = c.req.param("id");
134
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/pending");
135
+ return new Response(resp.body, resp);
136
+ });
137
+ // SSE events — proxy to DO's SSE endpoint
138
+ api.get("/sessions/:id/events", async (c) => {
139
+ const sessionId = c.req.param("id");
140
+ const resp = await proxyToDO(c.env, sessionId, "https://internal/api/events");
141
+ const headers = new Headers(resp.headers);
142
+ headers.set("Content-Type", "text/event-stream");
143
+ headers.set("Cache-Control", "no-cache");
144
+ headers.set("Connection", "keep-alive");
145
+ headers.set("Access-Control-Allow-Origin", "*");
146
+ return new Response(resp.body, {
147
+ status: resp.status,
148
+ statusText: resp.statusText,
149
+ headers,
150
+ });
151
+ });
152
+ export { api as agentationApi };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agentation-api.preview.e2e.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentation-api.preview.e2e.test.d.ts","sourceRoot":"","sources":["../src/agentation-api.preview.e2e.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,293 @@
1
+ // End-to-end preview worker test for Agentation API compatibility.
2
+ // Uses real fetch requests against preview.critique.work and validates
3
+ // session lifecycle, annotation CRUD, multi-user cookies, and SSE events.
4
+ import { describe, expect, test } from "bun:test";
5
+ import crypto from "crypto";
6
+ const BASE_URL = process.env.AGENTATION_BASE_URL || "https://preview.critique.work/a";
7
+ const SSE_TIMEOUT_MS = 30_000;
8
+ function randomHex(bytes) {
9
+ return crypto.randomBytes(bytes).toString("hex");
10
+ }
11
+ function asObject(value, label) {
12
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
13
+ throw new Error(`${label} must be an object`);
14
+ }
15
+ return value;
16
+ }
17
+ function getString(record, key, label) {
18
+ const value = record[key];
19
+ if (typeof value !== "string") {
20
+ throw new Error(`${label}.${key} must be a string`);
21
+ }
22
+ return value;
23
+ }
24
+ function getNumber(record, key, label) {
25
+ const value = record[key];
26
+ if (typeof value !== "number" || Number.isNaN(value)) {
27
+ throw new Error(`${label}.${key} must be a number`);
28
+ }
29
+ return value;
30
+ }
31
+ function getArray(record, key, label) {
32
+ const value = record[key];
33
+ if (!Array.isArray(value)) {
34
+ throw new Error(`${label}.${key} must be an array`);
35
+ }
36
+ return value;
37
+ }
38
+ function parseJson(text) {
39
+ if (text.length === 0)
40
+ return null;
41
+ return JSON.parse(text);
42
+ }
43
+ async function requestJson(path, options) {
44
+ const headers = new Headers(options?.headers);
45
+ headers.set("Accept", "application/json");
46
+ if (options?.body !== undefined) {
47
+ headers.set("Content-Type", "application/json");
48
+ }
49
+ if (options?.userId) {
50
+ headers.set("Cookie", `cw_user_id=${options.userId}`);
51
+ }
52
+ const response = await fetch(`${BASE_URL}${path}`, {
53
+ method: options?.method || "GET",
54
+ headers,
55
+ body: options?.body === undefined ? undefined : JSON.stringify(options.body),
56
+ });
57
+ const text = await response.text();
58
+ const json = parseJson(text);
59
+ return { response, json, text };
60
+ }
61
+ function parseSseFrame(frame) {
62
+ const lines = frame.split("\n");
63
+ let eventName = "";
64
+ let id;
65
+ const dataLines = [];
66
+ for (const line of lines) {
67
+ if (line.startsWith("event:")) {
68
+ eventName = line.slice("event:".length).trim();
69
+ continue;
70
+ }
71
+ if (line.startsWith("data:")) {
72
+ dataLines.push(line.slice("data:".length).trimStart());
73
+ continue;
74
+ }
75
+ if (line.startsWith("id:")) {
76
+ const parsed = Number(line.slice("id:".length).trim());
77
+ if (Number.isFinite(parsed)) {
78
+ id = parsed;
79
+ }
80
+ }
81
+ }
82
+ if (!eventName)
83
+ return null;
84
+ const dataText = dataLines.join("\n");
85
+ if (!dataText) {
86
+ return { event: eventName, data: null, id };
87
+ }
88
+ try {
89
+ return { event: eventName, data: JSON.parse(dataText), id };
90
+ }
91
+ catch {
92
+ return { event: eventName, data: dataText, id };
93
+ }
94
+ }
95
+ async function withTimeout(promise, timeoutMs, message) {
96
+ let timeoutId = null;
97
+ const timeoutPromise = new Promise((_, reject) => {
98
+ timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
99
+ });
100
+ try {
101
+ return await Promise.race([promise, timeoutPromise]);
102
+ }
103
+ finally {
104
+ if (timeoutId) {
105
+ clearTimeout(timeoutId);
106
+ }
107
+ }
108
+ }
109
+ async function collectSseEvents(stream, expectedEventNames) {
110
+ const reader = stream.getReader();
111
+ const decoder = new TextDecoder();
112
+ const expectedSet = new Set(expectedEventNames);
113
+ const seen = new Map();
114
+ let buffer = "";
115
+ const start = Date.now();
116
+ try {
117
+ while (seen.size < expectedSet.size) {
118
+ const elapsed = Date.now() - start;
119
+ const remaining = SSE_TIMEOUT_MS - elapsed;
120
+ if (remaining <= 0) {
121
+ throw new Error(`Timed out waiting for SSE events. Seen: ${Array.from(seen.keys()).join(", ") || "none"}`);
122
+ }
123
+ const readResult = await withTimeout(reader.read(), remaining, `Timed out waiting for SSE chunk after ${SSE_TIMEOUT_MS}ms`);
124
+ if (readResult.done) {
125
+ break;
126
+ }
127
+ buffer += decoder.decode(readResult.value, { stream: true });
128
+ buffer = buffer.replaceAll("\r\n", "\n");
129
+ while (true) {
130
+ const delimiterIndex = buffer.indexOf("\n\n");
131
+ if (delimiterIndex === -1)
132
+ break;
133
+ const rawFrame = buffer.slice(0, delimiterIndex);
134
+ buffer = buffer.slice(delimiterIndex + 2);
135
+ const parsed = parseSseFrame(rawFrame);
136
+ if (!parsed)
137
+ continue;
138
+ if (!expectedSet.has(parsed.event))
139
+ continue;
140
+ if (!seen.has(parsed.event)) {
141
+ seen.set(parsed.event, parsed);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ finally {
147
+ await reader.cancel().catch(() => undefined);
148
+ }
149
+ if (seen.size < expectedSet.size) {
150
+ const missing = expectedEventNames.filter((name) => !seen.has(name));
151
+ throw new Error(`Missing SSE events: ${missing.join(", ")}`);
152
+ }
153
+ return expectedEventNames.map((name) => seen.get(name));
154
+ }
155
+ describe("preview worker Agentation API", () => {
156
+ test("supports session lifecycle, multi-user annotation CRUD, and SSE", async () => {
157
+ const userA = `preview-user-a-${randomHex(4)}`;
158
+ const userB = `preview-user-b-${randomHex(4)}`;
159
+ const sessionId = randomHex(16);
160
+ const health = await requestJson("/health");
161
+ expect(health.response.status).toBe(200);
162
+ const healthBody = asObject(health.json, "health");
163
+ expect(getString(healthBody, "status", "health")).toBe("ok");
164
+ const createdSession = await requestJson("/sessions", {
165
+ method: "POST",
166
+ body: { url: `https://preview.critique.work/v/${sessionId}` },
167
+ });
168
+ expect(createdSession.response.status).toBe(200);
169
+ const sessionBody = asObject(createdSession.json, "createdSession");
170
+ expect(getString(sessionBody, "id", "createdSession")).toBe(sessionId);
171
+ const fetchedSession = await requestJson(`/sessions/${sessionId}`);
172
+ expect(fetchedSession.response.status).toBe(200);
173
+ const fetchedSessionBody = asObject(fetchedSession.json, "fetchedSession");
174
+ const initialAnnotations = getArray(fetchedSessionBody, "annotations", "fetchedSession");
175
+ expect(initialAnnotations.length).toBe(0);
176
+ const sseResponse = await fetch(`${BASE_URL}/sessions/${sessionId}/events`, {
177
+ headers: { Accept: "text/event-stream" },
178
+ });
179
+ expect(sseResponse.ok).toBe(true);
180
+ const sseContentType = sseResponse.headers.get("Content-Type") || "";
181
+ expect(sseContentType.includes("text/event-stream")).toBe(true);
182
+ expect(sseResponse.body).toBeTruthy();
183
+ const sseEventsPromise = collectSseEvents(sseResponse.body, [
184
+ "annotation.created",
185
+ "annotation.updated",
186
+ "action.requested",
187
+ "annotation.deleted",
188
+ ]);
189
+ const createdAnnotationResponse = await requestJson(`/sessions/${sessionId}/annotations`, {
190
+ method: "POST",
191
+ userId: userA,
192
+ body: {
193
+ comment: "preview smoke create",
194
+ elementPath: "#content > .line:nth-child(1)",
195
+ element: "div",
196
+ timestamp: Date.now(),
197
+ x: 12,
198
+ y: 32,
199
+ status: "pending",
200
+ },
201
+ });
202
+ expect(createdAnnotationResponse.response.status).toBe(201);
203
+ const createdAnnotation = asObject(createdAnnotationResponse.json, "createdAnnotation");
204
+ const annotationId = getString(createdAnnotation, "id", "createdAnnotation");
205
+ expect(annotationId.startsWith(`${sessionId}_`)).toBe(true);
206
+ expect(getString(createdAnnotation, "authorId", "createdAnnotation")).toBe(userA);
207
+ expect(getString(createdAnnotation, "sessionId", "createdAnnotation")).toBe(sessionId);
208
+ const getCreated = await requestJson(`/annotations/${annotationId}`);
209
+ expect(getCreated.response.status).toBe(200);
210
+ const getCreatedBody = asObject(getCreated.json, "getCreated");
211
+ expect(getString(getCreatedBody, "id", "getCreated")).toBe(annotationId);
212
+ const pendingBeforeResolve = await requestJson(`/sessions/${sessionId}/pending`);
213
+ expect(pendingBeforeResolve.response.status).toBe(200);
214
+ const pendingBeforeResolveBody = asObject(pendingBeforeResolve.json, "pendingBeforeResolve");
215
+ const pendingBeforeResolveList = getArray(pendingBeforeResolveBody, "annotations", "pendingBeforeResolve").map((item) => asObject(item, "pendingBeforeResolve.annotations"));
216
+ expect(pendingBeforeResolveList.some((item) => getString(item, "id", "pending item") === annotationId)).toBe(true);
217
+ const updatedAnnotationResponse = await requestJson(`/annotations/${annotationId}`, {
218
+ method: "PATCH",
219
+ userId: userB,
220
+ body: {
221
+ status: "resolved",
222
+ resolvedBy: "human",
223
+ comment: "preview smoke resolved",
224
+ },
225
+ });
226
+ expect(updatedAnnotationResponse.response.status).toBe(200);
227
+ const updatedAnnotation = asObject(updatedAnnotationResponse.json, "updatedAnnotation");
228
+ expect(getString(updatedAnnotation, "status", "updatedAnnotation")).toBe("resolved");
229
+ expect(getString(updatedAnnotation, "comment", "updatedAnnotation")).toBe("preview smoke resolved");
230
+ const threadResponse = await requestJson(`/annotations/${annotationId}/thread`, {
231
+ method: "POST",
232
+ userId: userB,
233
+ body: {
234
+ role: "human",
235
+ content: "confirmed in preview test",
236
+ },
237
+ });
238
+ expect(threadResponse.response.status).toBe(201);
239
+ const threadBody = asObject(threadResponse.json, "thread");
240
+ expect(getString(threadBody, "role", "thread")).toBe("human");
241
+ const pendingAfterResolve = await requestJson(`/sessions/${sessionId}/pending`);
242
+ expect(pendingAfterResolve.response.status).toBe(200);
243
+ const pendingAfterResolveBody = asObject(pendingAfterResolve.json, "pendingAfterResolve");
244
+ const pendingAfterResolveList = getArray(pendingAfterResolveBody, "annotations", "pendingAfterResolve").map((item) => asObject(item, "pendingAfterResolve.annotations"));
245
+ expect(pendingAfterResolveList.some((item) => getString(item, "id", "pending item") === annotationId)).toBe(false);
246
+ const actionResponse = await requestJson(`/sessions/${sessionId}/action`, {
247
+ method: "POST",
248
+ userId: userB,
249
+ body: {
250
+ output: "please handle this annotation",
251
+ },
252
+ });
253
+ expect(actionResponse.response.status).toBe(200);
254
+ const actionBody = asObject(actionResponse.json, "action");
255
+ expect(actionBody.success).toBe(true);
256
+ expect(getNumber(actionBody, "annotationCount", "action")).toBeGreaterThanOrEqual(1);
257
+ const delivered = asObject(actionBody.delivered, "action.delivered");
258
+ expect(getNumber(delivered, "total", "action.delivered")).toBeGreaterThanOrEqual(0);
259
+ const deleteResponse = await requestJson(`/annotations/${annotationId}`, {
260
+ method: "DELETE",
261
+ userId: userB,
262
+ });
263
+ expect(deleteResponse.response.status).toBe(200);
264
+ const deleteBody = asObject(deleteResponse.json, "delete");
265
+ expect(deleteBody.success).toBe(true);
266
+ const getDeleted = await requestJson(`/annotations/${annotationId}`);
267
+ expect(getDeleted.response.status).toBe(404);
268
+ const sseEvents = await sseEventsPromise;
269
+ const createdEvent = asObject(sseEvents[0].data, "sse.created");
270
+ const updatedEvent = asObject(sseEvents[1].data, "sse.updated");
271
+ const actionEvent = asObject(sseEvents[2].data, "sse.action");
272
+ const deletedEvent = asObject(sseEvents[3].data, "sse.deleted");
273
+ expect(getString(createdEvent, "sessionId", "sse.created")).toBe(sessionId);
274
+ expect(getString(updatedEvent, "sessionId", "sse.updated")).toBe(sessionId);
275
+ expect(getString(actionEvent, "sessionId", "sse.action")).toBe(sessionId);
276
+ expect(getString(deletedEvent, "sessionId", "sse.deleted")).toBe(sessionId);
277
+ const createdSeq = getNumber(createdEvent, "sequence", "sse.created");
278
+ const updatedSeq = getNumber(updatedEvent, "sequence", "sse.updated");
279
+ const actionSeq = getNumber(actionEvent, "sequence", "sse.action");
280
+ const deletedSeq = getNumber(deletedEvent, "sequence", "sse.deleted");
281
+ expect(createdSeq < updatedSeq).toBe(true);
282
+ expect(updatedSeq < actionSeq).toBe(true);
283
+ expect(actionSeq < deletedSeq).toBe(true);
284
+ const createdPayload = asObject(createdEvent.payload, "sse.created.payload");
285
+ const updatedPayload = asObject(updatedEvent.payload, "sse.updated.payload");
286
+ const actionPayload = asObject(actionEvent.payload, "sse.action.payload");
287
+ const deletedPayload = asObject(deletedEvent.payload, "sse.deleted.payload");
288
+ expect(getString(createdPayload, "id", "sse.created.payload")).toBe(annotationId);
289
+ expect(getString(updatedPayload, "id", "sse.updated.payload")).toBe(annotationId);
290
+ expect(getString(actionPayload, "output", "sse.action.payload")).toBe("please handle this annotation");
291
+ expect(getString(deletedPayload, "id", "sse.deleted.payload")).toBe(annotationId);
292
+ }, 120_000);
293
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agentation-widget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentation-widget.d.ts","sourceRoot":"","sources":["../src/agentation-widget.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,67 @@
1
+ // Widget entry point — self-initializing script for static HTML pages.
2
+ // Reads config from window.__CRITIQUE_CONFIG__, creates a React root,
3
+ // and renders the Agentation component for annotation UI.
4
+ //
5
+ // This is used by pages that don't have React (e.g., critique's static diff pages).
6
+ //
7
+ // Usage: inject into HTML before </body>:
8
+ // <script>window.__CRITIQUE_CONFIG__ = { endpoint: "https://critique.work/a", sessionId: "abc123", userId: "user-1" }</script>
9
+ // <script src="/agentation-widget.js"></script>
10
+ //
11
+ // Performance optimization:
12
+ // Critique diff pages can have 185K+ DOM nodes (6,500+ div.line elements each
13
+ // containing many spans). We inject content-visibility: auto on .line elements
14
+ // so the browser only renders visible lines, skipping layout/paint for the
15
+ // ~6,400 off-screen lines. This brings page load from ~16s to <2s on large diffs.
16
+ function injectContentVisibility() {
17
+ if (document.getElementById("critique-cv-styles"))
18
+ return;
19
+ const style = document.createElement("style");
20
+ style.id = "critique-cv-styles";
21
+ style.textContent = `
22
+ #content > .line {
23
+ content-visibility: auto;
24
+ contain-intrinsic-height: auto 20px;
25
+ }
26
+ `;
27
+ document.head.appendChild(style);
28
+ }
29
+ async function init() {
30
+ // Inject content-visibility optimization before anything else
31
+ injectContentVisibility();
32
+ // Seed agentation theme from system preference before it reads localStorage.
33
+ // Agentation reads localStorage["feedback-toolbar-theme"] on mount and
34
+ // defaults to dark if absent. We set it to match the OS color scheme so
35
+ // the widget looks correct on light-mode systems from the first render.
36
+ if (!localStorage.getItem("feedback-toolbar-theme")) {
37
+ const isLight = window.matchMedia("(prefers-color-scheme: light)").matches;
38
+ localStorage.setItem("feedback-toolbar-theme", isLight ? "light" : "dark");
39
+ }
40
+ if (document.getElementById("critique-agentation"))
41
+ return;
42
+ const config = window.__CRITIQUE_CONFIG__;
43
+ if (!config?.endpoint || !config?.sessionId) {
44
+ console.warn("[critique] Missing window.__CRITIQUE_CONFIG__");
45
+ return;
46
+ }
47
+ const container = document.createElement("div");
48
+ container.id = "critique-agentation";
49
+ document.body.appendChild(container);
50
+ // Use preact's render() directly since the build aliases react → preact/compat.
51
+ // preact/compat does not export createRoot from react-dom/client.
52
+ const [{ Agentation }, { render, h }] = await Promise.all([
53
+ import("@critique.work/agentation"),
54
+ import("preact"),
55
+ ]);
56
+ render(h(Agentation, {
57
+ endpoint: config.endpoint,
58
+ sessionId: config.sessionId,
59
+ showFreezeButton: config.showFreezeButton,
60
+ }), container);
61
+ }
62
+ if (document.readyState === "loading") {
63
+ document.addEventListener("DOMContentLoaded", init);
64
+ }
65
+ else {
66
+ init();
67
+ }
@@ -0,0 +1,42 @@
1
+ import type { CapturedFrame, CapturedLine, CapturedSpan } from "@opentuah/core";
2
+ export interface ToHtmlOptions {
3
+ /** Background color for the container */
4
+ backgroundColor?: string;
5
+ /** Text color for the container */
6
+ textColor?: string;
7
+ /** Font family for the output */
8
+ fontFamily?: string;
9
+ /** Trim empty lines from the end */
10
+ trimEmptyLines?: boolean;
11
+ /** Enable auto light/dark mode based on system preference */
12
+ autoTheme?: boolean;
13
+ /** HTML document title */
14
+ title?: string;
15
+ /** OG image URL for social media previews */
16
+ ogImageUrl?: string;
17
+ /** Custom line renderer - wraps or replaces the default <div class="line"> output per line.
18
+ * Generic hook: receives the default HTML, the captured line data, and the 0-based line index.
19
+ * Return a replacement HTML string. If not provided, the default <div class="line"> is used. */
20
+ renderLine?: (defaultHtml: string, line: CapturedLine, lineIndex: number) => string;
21
+ /** Extra CSS injected into the document style block */
22
+ extraCss?: string;
23
+ /** Extra JS injected as a separate script block before </body> */
24
+ extraJs?: string;
25
+ }
26
+ /**
27
+ * Converts captured frame to styled HTML.
28
+ * Renders HTML line by line from the CapturedFrame structure.
29
+ * Returns both the content HTML and a CSS block for deduplicated span styles.
30
+ */
31
+ export declare function frameToHtml(frame: CapturedFrame, options?: ToHtmlOptions): {
32
+ html: string;
33
+ spanCss: string;
34
+ };
35
+ /**
36
+ * Generates a complete HTML document from captured frame.
37
+ * Includes proper styling for terminal output display.
38
+ * Font size automatically adjusts to fit content within viewport.
39
+ */
40
+ export declare function frameToHtmlDocument(frame: CapturedFrame, options?: ToHtmlOptions): string;
41
+ export type { CapturedFrame, CapturedLine, CapturedSpan };
42
+ //# sourceMappingURL=ansi-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-html.d.ts","sourceRoot":"","sources":["../src/ansi-html.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAM/E,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0BAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;qGAEiG;IACjG,UAAU,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IACnF,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAqHD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA4BhH;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAyL7F;AAED,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,CAAA"}