@burdenoff/microfe-planmagnet 2026.1001.8 → 2026.1002.1

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 (49) hide show
  1. package/dist/generated/global-types.d.ts +5 -0
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts +16 -0
  4. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts.map +1 -0
  5. package/dist/planmagnet/comments/CommentAttachmentUpload.js +218 -0
  6. package/dist/planmagnet/comments/CommentAttachmentUpload.js.map +1 -0
  7. package/dist/planmagnet/comments/CommentAttachments.d.ts +15 -0
  8. package/dist/planmagnet/comments/CommentAttachments.d.ts.map +1 -0
  9. package/dist/planmagnet/comments/CommentAttachments.js +74 -0
  10. package/dist/planmagnet/comments/CommentAttachments.js.map +1 -0
  11. package/dist/planmagnet/comments/CommentBody.d.ts +11 -0
  12. package/dist/planmagnet/comments/CommentBody.d.ts.map +1 -0
  13. package/dist/planmagnet/comments/CommentBody.js +130 -0
  14. package/dist/planmagnet/comments/CommentBody.js.map +1 -0
  15. package/dist/planmagnet/comments/CommentThread.d.ts +7 -2
  16. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  17. package/dist/planmagnet/comments/CommentThread.js +288 -233
  18. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  19. package/dist/planmagnet/comments/MentionComposer.d.ts +0 -7
  20. package/dist/planmagnet/comments/MentionComposer.d.ts.map +1 -1
  21. package/dist/planmagnet/comments/MentionComposer.js +225 -86
  22. package/dist/planmagnet/comments/MentionComposer.js.map +1 -1
  23. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  24. package/dist/planmagnet/comments/TargetComments.js +44 -6
  25. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  26. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  27. package/dist/planmagnet/comments/WorkItemComments.js +22 -3
  28. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  29. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts +102 -0
  30. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts.map +1 -0
  31. package/dist/planmagnet/comments/commentAttachmentUploadClient.js +298 -0
  32. package/dist/planmagnet/comments/commentAttachmentUploadClient.js.map +1 -0
  33. package/dist/planmagnet/comments/commentImageTokens.d.ts +5 -0
  34. package/dist/planmagnet/comments/commentImageTokens.d.ts.map +1 -0
  35. package/dist/planmagnet/comments/commentImageTokens.js +14 -0
  36. package/dist/planmagnet/comments/commentImageTokens.js.map +1 -0
  37. package/dist/planmagnet/comments/commentTokens.d.ts +31 -0
  38. package/dist/planmagnet/comments/commentTokens.d.ts.map +1 -0
  39. package/dist/planmagnet/comments/commentTokens.js +142 -0
  40. package/dist/planmagnet/comments/commentTokens.js.map +1 -0
  41. package/dist/planmagnet/comments/fileDisplay.d.ts +22 -0
  42. package/dist/planmagnet/comments/fileDisplay.d.ts.map +1 -0
  43. package/dist/planmagnet/comments/fileDisplay.js +48 -0
  44. package/dist/planmagnet/comments/fileDisplay.js.map +1 -0
  45. package/dist/planmagnet/comments/useCommentFileUrl.d.ts +22 -0
  46. package/dist/planmagnet/comments/useCommentFileUrl.d.ts.map +1 -0
  47. package/dist/planmagnet/comments/useCommentFileUrl.js +54 -0
  48. package/dist/planmagnet/comments/useCommentFileUrl.js.map +1 -0
  49. package/package.json +3 -2
@@ -0,0 +1,298 @@
1
+ import { gql as e } from "@apollo/client";
2
+ var t = 100 * 1024 * 1024, n = 3, r = new Set([
3
+ "QUOTA_EXCEEDED",
4
+ "QUOTA_EXHAUSTED",
5
+ "QUOTA_NOT_PROVISIONED"
6
+ ]), i = e`
7
+ mutation PlanmagnetCommentInitiateUpload($input: InitiateUploadInput!) {
8
+ initiateUpload(input: $input) {
9
+ session {
10
+ id
11
+ startedById
12
+ status
13
+ uploadType
14
+ partSize
15
+ totalParts
16
+ }
17
+ uploadUrl
18
+ }
19
+ }
20
+ `, a = e`
21
+ query PlanmagnetCommentPartUploadUrls($sessionId: ID!, $partNumbers: [Int!]!) {
22
+ getBatchPartUploadUrls(sessionId: $sessionId, partNumbers: $partNumbers) {
23
+ partNumber
24
+ url
25
+ }
26
+ }
27
+ `, o = e`
28
+ mutation PlanmagnetCommentRegisterUploadedPart($input: UploadPartInput!) {
29
+ registerUploadedPart(input: $input) {
30
+ session {
31
+ id
32
+ status
33
+ uploadedParts
34
+ }
35
+ }
36
+ }
37
+ `, s = e`
38
+ mutation PlanmagnetCommentCompleteUpload($input: CompleteUploadInput!) {
39
+ completeUpload(input: $input) {
40
+ session {
41
+ status
42
+ }
43
+ file {
44
+ id
45
+ folderId
46
+ visibility
47
+ uploadedById
48
+ }
49
+ }
50
+ }
51
+ `, c = e`
52
+ mutation PlanmagnetCommentAbortUpload($sessionId: ID!) {
53
+ abortUpload(sessionId: $sessionId)
54
+ }
55
+ `, l = class extends Error {
56
+ constructor(e, t = !1) {
57
+ super(t ? "Your Files plan limit currently prevents finalizing this attachment. It may already have uploaded; retry after freeing Files capacity or changing your plan, or remove it." : "The file may have uploaded. Retry finalizing this attachment, or remove it."), this.session = e, this.quotaLimited = t, this.name = "UploadCompletionUncertainError";
58
+ }
59
+ }, u = class extends Error {
60
+ constructor() {
61
+ super("Upload canceled"), this.name = "UploadCanceledError";
62
+ }
63
+ }, d = class extends Error {
64
+ constructor() {
65
+ super("Files did not return a completed attachment"), this.name = "MissingCompletionResponseError";
66
+ }
67
+ };
68
+ function f(e) {
69
+ if (e.aborted) throw new u();
70
+ }
71
+ function p(e) {
72
+ return e instanceof l || e instanceof u ? e.message : m(e).some((e) => r.has(e)) ? "Your Files plan limit does not allow this upload. Remove it or change your plan before retrying." : "The attachment could not be uploaded. Retry or remove it.";
73
+ }
74
+ function m(e) {
75
+ if (!e || typeof e != "object") return [];
76
+ let t = e;
77
+ return [
78
+ t.extensions?.code,
79
+ ...(t.errors ?? []).map((e) => e.extensions?.code),
80
+ ...(t.graphQLErrors ?? []).map((e) => e.extensions?.code)
81
+ ].filter((e) => typeof e == "string");
82
+ }
83
+ function h(e, t) {
84
+ let n = m(e);
85
+ if (n.some((e) => r.has(e) || [
86
+ "FORBIDDEN",
87
+ "UNAUTHENTICATED",
88
+ "UNAUTHORIZED",
89
+ "BAD_USER_INPUT",
90
+ "GRAPHQL_VALIDATION_FAILED",
91
+ "NOT_FOUND",
92
+ "CONFLICT"
93
+ ].includes(e))) return null;
94
+ let i = n.some((e) => [
95
+ "OPERATION_TIMEOUT",
96
+ "GATEWAY_TIMEOUT",
97
+ "REQUEST_TIMEOUT",
98
+ "TIMEOUT"
99
+ ].includes(e)), a = e && typeof e == "object" ? e : null, o = a?.statusCode ?? a?.status;
100
+ return o && o >= 400 && o < 500 && o !== 408 ? null : i || o && [
101
+ 408,
102
+ 504,
103
+ 524
104
+ ].includes(o) ? [1e4, 2e4][t - 1] ?? null : e instanceof d || n.includes("INTERNAL_SERVER_ERROR") || o && o >= 500 || a?.networkError || e instanceof TypeError ? [400, 800][t - 1] ?? null : typeof a?.message == "string" && /\boperation\s+timed?\s?out\b/i.test(a.message) ? [1e4, 2e4][t - 1] ?? null : null;
105
+ }
106
+ function g(e, t) {
107
+ return new Promise((n, r) => {
108
+ if (t.aborted) {
109
+ r(new u());
110
+ return;
111
+ }
112
+ let i = () => {
113
+ clearTimeout(o), t.removeEventListener("abort", a), n();
114
+ }, a = () => {
115
+ clearTimeout(o), t.removeEventListener("abort", a), r(new u());
116
+ }, o = setTimeout(i, e);
117
+ t.addEventListener("abort", a, { once: !0 });
118
+ });
119
+ }
120
+ function _(e, t, n) {
121
+ if (!Number.isSafeInteger(e) || e <= 0 || !Number.isSafeInteger(t) || t <= 0 || !Number.isSafeInteger(n) || n < 1 || n > Math.ceil(e / t)) throw Error("Invalid attachment part range");
122
+ let r = (n - 1) * t;
123
+ return {
124
+ start: r,
125
+ end: Math.min(r + t, e)
126
+ };
127
+ }
128
+ function v(e) {
129
+ if (typeof e != "string" || !e.trim()) return !1;
130
+ try {
131
+ let t = new URL(e);
132
+ return t.protocol === "https:" || t.protocol === "http:";
133
+ } catch {
134
+ return !1;
135
+ }
136
+ }
137
+ function y(e, t) {
138
+ let n = e?.session;
139
+ if (!n?.id || !n.startedById || n.status !== "IN_PROGRESS") throw Error("Files did not start an upload session");
140
+ let r = t <= 5242880 ? "DIRECT" : "MULTIPART";
141
+ if (n.uploadType !== r) throw Error("Files returned an unsupported upload type");
142
+ if (r === "DIRECT" && !v(e?.uploadUrl)) throw Error("Files did not return a direct upload URL");
143
+ if (r === "MULTIPART" && (!Number.isSafeInteger(n.partSize) || n.partSize < 5242880 || !Number.isSafeInteger(n.totalParts) || n.totalParts !== Math.ceil(t / n.partSize))) throw Error("Files returned an invalid multipart layout");
144
+ return n;
145
+ }
146
+ function b(e, t) {
147
+ if (!Number.isInteger(e.status) || e.status < 200 || e.status >= 300) throw Error(t ? `Attachment part ${t} failed to upload` : "Attachment failed to upload");
148
+ if (t !== void 0 && !e.etag?.trim()) throw Error(`Attachment part ${t} did not return an ETag`);
149
+ return e.etag;
150
+ }
151
+ async function x(e, t, i) {
152
+ if (!e.sessionId || !e.startedById || !Number.isSafeInteger(e.fileSize) || e.fileSize > 104857600 || e.fileSize < 0) throw Error("Invalid attachment upload session");
153
+ for (let a = 1; a <= n; a += 1) {
154
+ if (i.aborted) throw new l(e);
155
+ try {
156
+ let n = await t.complete({
157
+ sessionId: e.sessionId,
158
+ fileSize: String(e.fileSize)
159
+ }, i);
160
+ if (!n?.session || !n.file?.id) throw new d();
161
+ if (n.session.status !== "FINALIZED" || n.file.folderId !== null || n.file.visibility !== "PRIVATE" || n.file.uploadedById !== e.startedById) throw Error("Files did not return the completed private attachment");
162
+ return n.file.id;
163
+ } catch (o) {
164
+ let s = m(o).some((e) => r.has(e));
165
+ if (i.aborted || a >= n) throw new l(e, s);
166
+ let c = h(o, a);
167
+ if (c === null) throw new l(e, s);
168
+ try {
169
+ await (t.wait ?? g)(c, i);
170
+ } catch {
171
+ throw new l(e);
172
+ }
173
+ }
174
+ }
175
+ throw new l(e);
176
+ }
177
+ async function S(e, t, n, r) {
178
+ if (e.size > 104857600) throw Error("Attachment exceeds 100 MiB");
179
+ let i = e.type || "application/octet-stream", a = e.name.includes(".") ? e.name.slice(e.name.lastIndexOf(".") + 1) : "", o = null, s = !1, c = !1;
180
+ try {
181
+ f(n);
182
+ let l = await t.initiate({
183
+ fileName: e.name,
184
+ fileSize: String(e.size),
185
+ mimeType: i,
186
+ extension: a,
187
+ visibility: "PRIVATE"
188
+ }, n);
189
+ o = l?.session?.id || null, s = l?.session?.status === "IN_PROGRESS", f(n);
190
+ let u = y(l, e.size);
191
+ if (u.uploadType === "DIRECT") {
192
+ let r = l?.uploadUrl;
193
+ if (!v(r)) throw Error("Files did not return a direct upload URL");
194
+ b(await t.put(r, e, i, n));
195
+ } else for (let r = 1; r <= u.totalParts; r += 1) {
196
+ f(n);
197
+ let a = await t.partUrls(u.id, [r], n);
198
+ if (a?.length !== 1 || a[0]?.partNumber !== r || !v(a[0].url)) throw Error(`Files did not return the URL for attachment part ${r}`);
199
+ let { start: o, end: s } = _(e.size, u.partSize, r), c = b(await t.put(a[0].url, e.slice(o, s), i, n), r);
200
+ f(n);
201
+ let l = await t.registerPart({
202
+ sessionId: u.id,
203
+ partNumber: r,
204
+ etag: c
205
+ }, n), d = r === u.totalParts ? "COMPLETED" : "IN_PROGRESS";
206
+ if (l?.session?.id !== u.id || l.session.status !== d || l.session.uploadedParts !== r) throw Error(`Files did not register attachment part ${r}`);
207
+ }
208
+ return f(n), r?.(), c = !0, await x({
209
+ sessionId: u.id,
210
+ startedById: u.startedById,
211
+ fileSize: e.size
212
+ }, t, n);
213
+ } catch (e) {
214
+ if (o && s && !c) try {
215
+ await t.abort(o);
216
+ } catch {}
217
+ throw n.aborted && !c ? new u() : e;
218
+ }
219
+ }
220
+ function C(e) {
221
+ return {
222
+ async initiate(t, n) {
223
+ let r = await e.mutate({
224
+ mutation: i,
225
+ variables: { input: t },
226
+ context: { fetchOptions: { signal: n } },
227
+ errorPolicy: "all"
228
+ });
229
+ if (r.error) throw r.error;
230
+ return r.data?.initiateUpload ?? null;
231
+ },
232
+ async partUrls(t, n, r) {
233
+ let i = await e.query({
234
+ query: a,
235
+ variables: {
236
+ sessionId: t,
237
+ partNumbers: n
238
+ },
239
+ context: { fetchOptions: { signal: r } },
240
+ fetchPolicy: "no-cache",
241
+ errorPolicy: "all"
242
+ });
243
+ if (i.error) throw i.error;
244
+ return i.data?.getBatchPartUploadUrls ?? null;
245
+ },
246
+ async put(e, t, n, r) {
247
+ try {
248
+ let i = await fetch(e, {
249
+ method: "PUT",
250
+ headers: { "Content-Type": n },
251
+ body: t,
252
+ signal: r,
253
+ credentials: "omit",
254
+ redirect: "error"
255
+ });
256
+ return {
257
+ status: i.status,
258
+ etag: i.headers.get("ETag")
259
+ };
260
+ } catch {
261
+ throw r.aborted ? new u() : Error("Storage PUT failed");
262
+ }
263
+ },
264
+ async registerPart(t, n) {
265
+ let r = await e.mutate({
266
+ mutation: o,
267
+ variables: { input: t },
268
+ context: { fetchOptions: { signal: n } },
269
+ errorPolicy: "all"
270
+ });
271
+ if (r.error) throw r.error;
272
+ return r.data?.registerUploadedPart ?? null;
273
+ },
274
+ async complete(t, n) {
275
+ let r = await e.mutate({
276
+ mutation: s,
277
+ variables: { input: t },
278
+ context: { fetchOptions: { signal: n } },
279
+ errorPolicy: "all"
280
+ });
281
+ if (r.error) throw r.error;
282
+ return r.data?.completeUpload ?? null;
283
+ },
284
+ async abort(t) {
285
+ let n = await e.mutate({
286
+ mutation: c,
287
+ variables: { sessionId: t },
288
+ errorPolicy: "all"
289
+ });
290
+ if (n.error) throw n.error;
291
+ return n.data?.abortUpload === !0;
292
+ }
293
+ };
294
+ }
295
+ //#endregion
296
+ export { t as MAX_COMMENT_ATTACHMENT_BYTES, l as UploadCompletionUncertainError, p as commentUploadErrorMessage, C as createCommentAttachmentUploadTransport, x as retryCommentAttachmentCompletion, S as uploadCommentAttachment };
297
+
298
+ //# sourceMappingURL=commentAttachmentUploadClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commentAttachmentUploadClient.js","names":[],"sources":["../../../src/planmagnet/comments/commentAttachmentUploadClient.ts"],"sourcesContent":["import { gql, type ApolloClient } from '@apollo/client';\n\n/** The Files service switches from a direct PUT to S3 multipart strictly above 5 MiB. */\nexport const DIRECT_UPLOAD_LIMIT_BYTES = 5 * 1024 * 1024;\nexport const MAX_COMMENT_ATTACHMENT_BYTES = 100 * 1024 * 1024;\nconst MAX_COMPLETION_ATTEMPTS = 3;\nconst FILES_QUOTA_ERROR_CODES = new Set([\n 'QUOTA_EXCEEDED',\n 'QUOTA_EXHAUSTED',\n 'QUOTA_NOT_PROVISIONED',\n]);\n\nconst INITIATE_UPLOAD = gql`\n mutation PlanmagnetCommentInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n session {\n id\n startedById\n status\n uploadType\n partSize\n totalParts\n }\n uploadUrl\n }\n }\n`;\n\nconst PART_UPLOAD_URLS = gql`\n query PlanmagnetCommentPartUploadUrls($sessionId: ID!, $partNumbers: [Int!]!) {\n getBatchPartUploadUrls(sessionId: $sessionId, partNumbers: $partNumbers) {\n partNumber\n url\n }\n }\n`;\n\nconst REGISTER_PART = gql`\n mutation PlanmagnetCommentRegisterUploadedPart($input: UploadPartInput!) {\n registerUploadedPart(input: $input) {\n session {\n id\n status\n uploadedParts\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = gql`\n mutation PlanmagnetCommentCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n session {\n status\n }\n file {\n id\n folderId\n visibility\n uploadedById\n }\n }\n }\n`;\n\nconst ABORT_UPLOAD = gql`\n mutation PlanmagnetCommentAbortUpload($sessionId: ID!) {\n abortUpload(sessionId: $sessionId)\n }\n`;\n\nexport interface UploadSession {\n id: string;\n startedById: string;\n status: string;\n uploadType: string;\n partSize: number;\n totalParts: number;\n}\n\ninterface UploadInitiation {\n session: UploadSession | null;\n uploadUrl: string | null;\n}\n\ninterface PresignedPartUrl {\n partNumber: number;\n url: string;\n}\n\ninterface RegisteredPart {\n session: { id: string; status: string; uploadedParts: number } | null;\n}\n\ninterface CompletedUpload {\n session: { status: string } | null;\n file: { id: string; folderId: string | null; visibility: string; uploadedById: string } | null;\n}\n\ninterface PutResult {\n status: number;\n etag: string | null;\n}\n\n/** Injectable, non-React Files protocol; its unit tests run without an Apollo provider or DOM. */\nexport interface CommentAttachmentUploadTransport {\n initiate(\n input: {\n fileName: string;\n fileSize: string;\n mimeType: string;\n extension: string;\n visibility: 'PRIVATE';\n },\n signal: AbortSignal\n ): Promise<UploadInitiation | null>;\n partUrls(\n sessionId: string,\n partNumbers: number[],\n signal: AbortSignal\n ): Promise<PresignedPartUrl[] | null>;\n put(url: string, bytes: Blob, mimeType: string, signal: AbortSignal): Promise<PutResult>;\n registerPart(\n input: { sessionId: string; partNumber: number; etag: string },\n signal: AbortSignal\n ): Promise<RegisteredPart | null>;\n complete(\n input: { sessionId: string; fileSize: string },\n signal: AbortSignal\n ): Promise<CompletedUpload | null>;\n /** Deliberately does not use the canceled PUT's signal: cleanup still needs to reach Files. */\n abort(sessionId: string): Promise<boolean>;\n /** Injectable delay keeps retry tests deterministic. */\n wait?(ms: number, signal: AbortSignal): Promise<void>;\n}\n\n/** Keep only server IDs, not presigned URLs, for a safe retry after an ambiguous completion. */\nexport interface CompletionSession {\n sessionId: string;\n startedById: string;\n fileSize: number;\n}\n\nexport class UploadCompletionUncertainError extends Error {\n constructor(\n readonly session: CompletionSession,\n readonly quotaLimited = false\n ) {\n super(\n quotaLimited\n ? 'Your Files plan limit currently prevents finalizing this attachment. It may already have uploaded; retry after freeing Files capacity or changing your plan, or remove it.'\n : 'The file may have uploaded. Retry finalizing this attachment, or remove it.'\n );\n this.name = 'UploadCompletionUncertainError';\n }\n}\n\nexport class UploadCanceledError extends Error {\n constructor() {\n super('Upload canceled');\n this.name = 'UploadCanceledError';\n }\n}\n\nclass MissingCompletionResponseError extends Error {\n constructor() {\n super('Files did not return a completed attachment');\n this.name = 'MissingCompletionResponseError';\n }\n}\n\nfunction assertNotCanceled(signal: AbortSignal): void {\n if (signal.aborted) throw new UploadCanceledError();\n}\n\n/** Never reflect an object-store or Apollo exception that might contain a signed URL to the UI. */\nexport function commentUploadErrorMessage(error: unknown): string {\n if (error instanceof UploadCompletionUncertainError) return error.message;\n if (error instanceof UploadCanceledError) return error.message;\n const codes = getErrorCodes(error);\n if (codes.some((code) => FILES_QUOTA_ERROR_CODES.has(code))) {\n return 'Your Files plan limit does not allow this upload. Remove it or change your plan before retrying.';\n }\n return 'The attachment could not be uploaded. Retry or remove it.';\n}\n\nfunction getErrorCodes(error: unknown): string[] {\n if (!error || typeof error !== 'object') return [];\n // Apollo v4 exposes `.errors`, v3 uses `.graphQLErrors`; this structural view reads\n // only optional code fields instead of assuming either runtime class is installed.\n const value = error as {\n extensions?: { code?: unknown };\n errors?: ReadonlyArray<{ extensions?: { code?: unknown } }>;\n graphQLErrors?: ReadonlyArray<{ extensions?: { code?: unknown } }>;\n };\n return [\n value.extensions?.code,\n ...(value.errors ?? []).map((entry) => entry.extensions?.code),\n ...(value.graphQLErrors ?? []).map((entry) => entry.extensions?.code),\n ].filter((code): code is string => typeof code === 'string');\n}\n\n/** Retrying only ambiguous gateway/storage failures avoids repeating a definitive quota or auth denial. */\nfunction completionRetryDelay(error: unknown, attempt: number): number | null {\n const codes = getErrorCodes(error);\n if (\n codes.some(\n (code) =>\n FILES_QUOTA_ERROR_CODES.has(code) ||\n [\n 'FORBIDDEN',\n 'UNAUTHENTICATED',\n 'UNAUTHORIZED',\n 'BAD_USER_INPUT',\n 'GRAPHQL_VALIDATION_FAILED',\n 'NOT_FOUND',\n 'CONFLICT',\n ].includes(code)\n )\n )\n return null;\n\n const timeout = codes.some((code) =>\n ['OPERATION_TIMEOUT', 'GATEWAY_TIMEOUT', 'REQUEST_TIMEOUT', 'TIMEOUT'].includes(code)\n );\n\n // Apollo network errors carry HTTP status on different envelopes; inspect the\n // minimal optional shape rather than depending on a particular link implementation.\n const detail =\n error && typeof error === 'object'\n ? (error as {\n statusCode?: number;\n status?: number;\n networkError?: unknown;\n message?: unknown;\n })\n : null;\n const status = detail?.statusCode ?? detail?.status;\n if (status && status >= 400 && status < 500 && status !== 408) return null;\n if (timeout || (status && [408, 504, 524].includes(status)))\n return [10_000, 20_000][attempt - 1] ?? null;\n if (\n error instanceof MissingCompletionResponseError ||\n codes.includes('INTERNAL_SERVER_ERROR') ||\n (status && status >= 500) ||\n detail?.networkError ||\n error instanceof TypeError\n ) {\n return [400, 800][attempt - 1] ?? null;\n }\n if (typeof detail?.message === 'string' && /\\boperation\\s+timed?\\s?out\\b/i.test(detail.message)) {\n return [10_000, 20_000][attempt - 1] ?? null;\n }\n return null;\n}\n\nfunction waitForRetry(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal.aborted) {\n reject(new UploadCanceledError());\n return;\n }\n const done = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', canceled);\n resolve();\n };\n const canceled = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', canceled);\n reject(new UploadCanceledError());\n };\n const timer = setTimeout(done, ms);\n signal.addEventListener('abort', canceled, { once: true });\n });\n}\n\n/** A pure byte-range calculation. Parts are 1-indexed and the final part may be shorter. */\nexport function attachmentPartRange(\n fileSize: number,\n partSize: number,\n partNumber: number\n): { start: number; end: number } {\n if (\n !Number.isSafeInteger(fileSize) ||\n fileSize <= 0 ||\n !Number.isSafeInteger(partSize) ||\n partSize <= 0 ||\n !Number.isSafeInteger(partNumber) ||\n partNumber < 1 ||\n partNumber > Math.ceil(fileSize / partSize)\n ) {\n throw new Error('Invalid attachment part range');\n }\n const start = (partNumber - 1) * partSize;\n return { start, end: Math.min(start + partSize, fileSize) };\n}\n\nfunction isAbsoluteUploadUrl(url: unknown): url is string {\n if (typeof url !== 'string' || !url.trim()) return false;\n try {\n const parsed = new URL(url);\n return parsed.protocol === 'https:' || parsed.protocol === 'http:';\n } catch {\n return false;\n }\n}\n\nfunction validateInitiation(initiation: UploadInitiation | null, size: number): UploadSession {\n const session = initiation?.session;\n if (!session?.id || !session.startedById || session.status !== 'IN_PROGRESS') {\n throw new Error('Files did not start an upload session');\n }\n const expectedType = size <= DIRECT_UPLOAD_LIMIT_BYTES ? 'DIRECT' : 'MULTIPART';\n if (session.uploadType !== expectedType)\n throw new Error('Files returned an unsupported upload type');\n if (expectedType === 'DIRECT' && !isAbsoluteUploadUrl(initiation?.uploadUrl)) {\n throw new Error('Files did not return a direct upload URL');\n }\n if (\n expectedType === 'MULTIPART' &&\n (!Number.isSafeInteger(session.partSize) ||\n session.partSize < DIRECT_UPLOAD_LIMIT_BYTES ||\n !Number.isSafeInteger(session.totalParts) ||\n session.totalParts !== Math.ceil(size / session.partSize))\n ) {\n throw new Error('Files returned an invalid multipart layout');\n }\n return session;\n}\n\nfunction validatePut(result: PutResult, partNumber: number): string;\nfunction validatePut(result: PutResult): string | null;\nfunction validatePut(result: PutResult, partNumber?: number): string | null {\n if (!Number.isInteger(result.status) || result.status < 200 || result.status >= 300) {\n throw new Error(\n partNumber ? `Attachment part ${partNumber} failed to upload` : 'Attachment failed to upload'\n );\n }\n if (partNumber !== undefined && !result.etag?.trim()) {\n throw new Error(`Attachment part ${partNumber} did not return an ETag`);\n }\n // An S3 ETag includes literal double quotes. Do not strip, normalize, or re-hash it.\n return result.etag;\n}\n\n/** Completes only the original session: completeUpload is idempotent server-side. */\nexport async function retryCommentAttachmentCompletion(\n session: CompletionSession,\n transport: CommentAttachmentUploadTransport,\n signal: AbortSignal\n): Promise<string> {\n if (\n !session.sessionId ||\n !session.startedById ||\n !Number.isSafeInteger(session.fileSize) ||\n session.fileSize > MAX_COMMENT_ATTACHMENT_BYTES ||\n session.fileSize < 0\n ) {\n throw new Error('Invalid attachment upload session');\n }\n for (let attempt = 1; attempt <= MAX_COMPLETION_ATTEMPTS; attempt += 1) {\n // A cancellation after completion was attempted cannot safely abort the server\n // session: the first request may already have finalized it.\n if (signal.aborted) throw new UploadCompletionUncertainError(session);\n try {\n const result = await transport.complete(\n { sessionId: session.sessionId, fileSize: String(session.fileSize) },\n signal\n );\n if (!result?.session || !result.file?.id) {\n // A response lost after the service finalized is indistinguishable from an\n // unfinalized session. The server supports repeating this exact mutation.\n throw new MissingCompletionResponseError();\n }\n if (\n result.session.status !== 'FINALIZED' ||\n result.file.folderId !== null ||\n result.file.visibility !== 'PRIVATE' ||\n result.file.uploadedById !== session.startedById\n ) {\n throw new Error('Files did not return the completed private attachment');\n }\n return result.file.id;\n } catch (error) {\n const quotaLimited = getErrorCodes(error).some((code) => FILES_QUOTA_ERROR_CODES.has(code));\n if (signal.aborted || attempt >= MAX_COMPLETION_ATTEMPTS) {\n throw new UploadCompletionUncertainError(session, quotaLimited);\n }\n const delay = completionRetryDelay(error, attempt);\n if (delay === null) throw new UploadCompletionUncertainError(session, quotaLimited);\n try {\n await (transport.wait ?? waitForRetry)(delay, signal);\n } catch {\n throw new UploadCompletionUncertainError(session);\n }\n }\n }\n throw new UploadCompletionUncertainError(session);\n}\n\n/**\n * Private-root uploads only: no shared path, no folderId, and no broad workspace visibility.\n * Once completion has been sent its outcome may be ambiguous; NEVER abort that session (the\n * backend's abortUpload can mark even an already-FINALIZED session ABORTED). Retry completion\n * on the same session instead. Prior failures and canceled PUTs are aborted best-effort.\n */\nexport async function uploadCommentAttachment(\n file: File,\n transport: CommentAttachmentUploadTransport,\n signal: AbortSignal,\n onCompleting?: () => void\n): Promise<string> {\n if (file.size > MAX_COMMENT_ATTACHMENT_BYTES) throw new Error('Attachment exceeds 100 MiB');\n const mimeType = file.type || 'application/octet-stream';\n const extension = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.') + 1) : '';\n let sessionId: string | null = null;\n let abortableSession = false;\n let completionStarted = false;\n try {\n assertNotCanceled(signal);\n const initiation = await transport.initiate(\n {\n fileName: file.name,\n fileSize: String(file.size),\n mimeType,\n extension,\n visibility: 'PRIVATE',\n },\n signal\n );\n sessionId = initiation?.session?.id || null;\n // abortUpload is unsafe on an already-FINALIZED session. Only a newly initiated\n // IN_PROGRESS session may be cleaned up after a failed upload.\n abortableSession = initiation?.session?.status === 'IN_PROGRESS';\n assertNotCanceled(signal);\n const session = validateInitiation(initiation, file.size);\n\n if (session.uploadType === 'DIRECT') {\n const url = initiation?.uploadUrl;\n if (!isAbsoluteUploadUrl(url)) throw new Error('Files did not return a direct upload URL');\n validatePut(await transport.put(url, file, mimeType, signal));\n } else {\n for (let partNumber = 1; partNumber <= session.totalParts; partNumber += 1) {\n assertNotCanceled(signal);\n // URLs are fetched just-in-time and matched by partNumber, never by array order.\n const urls = await transport.partUrls(session.id, [partNumber], signal);\n if (\n urls?.length !== 1 ||\n urls[0]?.partNumber !== partNumber ||\n !isAbsoluteUploadUrl(urls[0].url)\n ) {\n throw new Error(`Files did not return the URL for attachment part ${partNumber}`);\n }\n const { start, end } = attachmentPartRange(file.size, session.partSize, partNumber);\n const etag = validatePut(\n await transport.put(urls[0].url, file.slice(start, end), mimeType, signal),\n partNumber\n );\n assertNotCanceled(signal);\n const registered = await transport.registerPart(\n { sessionId: session.id, partNumber, etag },\n signal\n );\n const expectedStatus = partNumber === session.totalParts ? 'COMPLETED' : 'IN_PROGRESS';\n if (\n registered?.session?.id !== session.id ||\n registered.session.status !== expectedStatus ||\n registered.session.uploadedParts !== partNumber\n ) {\n throw new Error(`Files did not register attachment part ${partNumber}`);\n }\n }\n }\n\n assertNotCanceled(signal);\n onCompleting?.();\n completionStarted = true;\n return await retryCommentAttachmentCompletion(\n { sessionId: session.id, startedById: session.startedById, fileSize: file.size },\n transport,\n signal\n );\n } catch (error) {\n if (sessionId && abortableSession && !completionStarted) {\n // The aborted signal is intentionally NOT used here. A failed multipart upload can\n // hold paid storage parts until its session expires if cleanup is skipped.\n try {\n await transport.abort(sessionId);\n } catch {\n /* Expiry cleanup is the fallback. */\n }\n }\n if (signal.aborted && !completionStarted) throw new UploadCanceledError();\n throw error;\n }\n}\n\n/** The host shell owns the Apollo provider (and its workspace/auth headers). */\nexport function createCommentAttachmentUploadTransport(\n client: ApolloClient\n): CommentAttachmentUploadTransport {\n return {\n async initiate(input, signal) {\n const result = await client.mutate<{ initiateUpload: UploadInitiation | null }>({\n mutation: INITIATE_UPLOAD,\n variables: { input },\n context: { fetchOptions: { signal } },\n errorPolicy: 'all',\n });\n if (result.error) throw result.error;\n return result.data?.initiateUpload ?? null;\n },\n async partUrls(sessionId, partNumbers, signal) {\n const result = await client.query<{ getBatchPartUploadUrls: PresignedPartUrl[] | null }>({\n query: PART_UPLOAD_URLS,\n variables: { sessionId, partNumbers },\n context: { fetchOptions: { signal } },\n fetchPolicy: 'no-cache',\n errorPolicy: 'all',\n });\n if (result.error) throw result.error;\n return result.data?.getBatchPartUploadUrls ?? null;\n },\n async put(url, bytes, mimeType, signal) {\n try {\n const result = await fetch(url, {\n method: 'PUT',\n headers: { 'Content-Type': mimeType },\n body: bytes,\n signal,\n credentials: 'omit',\n redirect: 'error',\n });\n return { status: result.status, etag: result.headers.get('ETag') };\n } catch {\n // fetch errors can contain the full signed URL; never display or log one.\n if (signal.aborted) throw new UploadCanceledError();\n throw new Error('Storage PUT failed');\n }\n },\n async registerPart(input, signal) {\n const result = await client.mutate<{ registerUploadedPart: RegisteredPart | null }>({\n mutation: REGISTER_PART,\n variables: { input },\n context: { fetchOptions: { signal } },\n errorPolicy: 'all',\n });\n if (result.error) throw result.error;\n return result.data?.registerUploadedPart ?? null;\n },\n async complete(input, signal) {\n const result = await client.mutate<{ completeUpload: CompletedUpload | null }>({\n mutation: COMPLETE_UPLOAD,\n variables: { input },\n context: { fetchOptions: { signal } },\n errorPolicy: 'all',\n });\n if (result.error) throw result.error;\n return result.data?.completeUpload ?? null;\n },\n async abort(sessionId) {\n const result = await client.mutate<{ abortUpload: boolean }>({\n mutation: ABORT_UPLOAD,\n variables: { sessionId },\n errorPolicy: 'all',\n });\n if (result.error) throw result.error;\n return result.data?.abortUpload === true;\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Append an image token only for a successfully uploaded Files ID; the server accepts flat alt text. */
2
+ export declare function appendImageEmbed(body: string, fileId: string, filename: string): string;
3
+ /** Removing an attached file must also remove any inline references to that same file. */
4
+ export declare function removeImageEmbed(body: string, fileId: string): string;
5
+ //# sourceMappingURL=commentImageTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commentImageTokens.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/commentImageTokens.ts"],"names":[],"mappings":"AAEA,yGAAyG;AACzG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASvF;AAED,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGrE"}
@@ -0,0 +1,14 @@
1
+ //#region src/planmagnet/comments/commentImageTokens.ts
2
+ var e = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ function t(t, n, r) {
4
+ if (!e.test(n) || t.includes(`(fileId:${n})`)) return t;
5
+ let i = r.replace(/[[\]\r\n]/g, " ").trim().slice(0, 200) || "Image";
6
+ return `${t}${t && !/\s$/.test(t) ? "\n" : ""}![${i}](fileId:${n})`;
7
+ }
8
+ function n(t, n) {
9
+ return e.test(n) ? t.replace(RegExp(`!\\[[^\\]\\n]*\\]\\(fileId:${n}\\)`, "gi"), "") : t;
10
+ }
11
+ //#endregion
12
+ export { t as appendImageEmbed, n as removeImageEmbed };
13
+
14
+ //# sourceMappingURL=commentImageTokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commentImageTokens.js","names":[],"sources":["../../../src/planmagnet/comments/commentImageTokens.ts"],"sourcesContent":["const FILE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Append an image token only for a successfully uploaded Files ID; the server accepts flat alt text. */\nexport function appendImageEmbed(body: string, fileId: string, filename: string): string {\n if (!FILE_ID_PATTERN.test(fileId) || body.includes(`(fileId:${fileId})`)) return body;\n const alt =\n filename\n .replace(/[[\\]\\r\\n]/g, ' ')\n .trim()\n .slice(0, 200) || 'Image';\n const spacer = body && !/\\s$/.test(body) ? '\\n' : '';\n return `${body}${spacer}![${alt}](fileId:${fileId})`;\n}\n\n/** Removing an attached file must also remove any inline references to that same file. */\nexport function removeImageEmbed(body: string, fileId: string): string {\n if (!FILE_ID_PATTERN.test(fileId)) return body;\n return body.replace(new RegExp(`!\\\\[[^\\\\]\\\\n]*\\\\]\\\\(fileId:${fileId}\\\\)`, 'gi'), '');\n}\n"],"mappings":";AAAA,IAAM,IAAkB;AAGxB,SAAgB,EAAiB,GAAc,GAAgB,GAA0B;AACvF,KAAI,CAAC,EAAgB,KAAK,EAAO,IAAI,EAAK,SAAS,WAAW,EAAO,GAAG,CAAE,QAAO;CACjF,IAAM,IACJ,EACG,QAAQ,cAAc,IAAI,CAC1B,MAAM,CACN,MAAM,GAAG,IAAI,IAAI;AAEtB,QAAO,GAAG,IADK,KAAQ,CAAC,MAAM,KAAK,EAAK,GAAG,OAAO,GAC1B,IAAI,EAAI,WAAW,EAAO;;AAIpD,SAAgB,EAAiB,GAAc,GAAwB;AAErE,QADK,EAAgB,KAAK,EAAO,GAC1B,EAAK,QAAY,OAAO,8BAA8B,EAAO,MAAM,KAAK,EAAE,GAAG,GAD1C"}
@@ -0,0 +1,31 @@
1
+ export type CommentToken = {
2
+ kind: 'text';
3
+ value: string;
4
+ } | {
5
+ kind: 'mention';
6
+ name: string;
7
+ actorId: string;
8
+ } | {
9
+ kind: 'image';
10
+ alt: string;
11
+ fileId: string;
12
+ } | {
13
+ kind: 'link';
14
+ text: string;
15
+ url: string;
16
+ } | {
17
+ kind: 'inline-code';
18
+ value: string;
19
+ } | {
20
+ kind: 'code-block';
21
+ value: string;
22
+ } | {
23
+ kind: 'bold';
24
+ value: string;
25
+ } | {
26
+ kind: 'italic';
27
+ value: string;
28
+ };
29
+ /** Parse only the server's six safe token shapes. Rejected or malformed candidates remain plain text. */
30
+ export declare function tokenizeCommentBody(text: string): CommentToken[];
31
+ //# sourceMappingURL=commentTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commentTokens.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/commentTokens.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA6EtC,yGAAyG;AACzG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAsGhE"}
@@ -0,0 +1,142 @@
1
+ //#region src/planmagnet/comments/commentTokens.ts
2
+ var e = /^fileId:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i, t = /^https:\/\/[^\s<>"'`]+$/i, n = /```([\s\S]{0,4000}?)```/y, r = /`([^`\n]{1,500})`/y, i = /\*\*([^\n]{1,1000}?)\*\*/y, a = /\*([^\n*]{1,1000}?)\*/y, o = /_([^\n_]{1,1000}?)_/y;
3
+ function s(e) {
4
+ let t = new Int32Array(e.length).fill(-1), n = new Int32Array(e.length).fill(-1), r = [], i = [];
5
+ for (let a = 0; a < e.length; a += 1) {
6
+ let o = e[a];
7
+ if (o === "\n") r.length = 0, i.length = 0;
8
+ else if (o === "[") r.push(a);
9
+ else if (o === "]") {
10
+ let e = r.pop();
11
+ e !== void 0 && (t[e] = a);
12
+ } else if (o === "(") i.push(a);
13
+ else if (o === ")") {
14
+ let e = i.pop();
15
+ e !== void 0 && (n[e] = a);
16
+ }
17
+ }
18
+ return {
19
+ brackets: t,
20
+ parens: n
21
+ };
22
+ }
23
+ function c(e, t, n) {
24
+ let r = n.brackets[t];
25
+ if (r < 0 || e[r + 1] !== "(") return null;
26
+ let i = r + 1, a = n.parens[i];
27
+ if (a < 0) return null;
28
+ let o = e.slice(t + 1, r), s = e.slice(i + 1, a);
29
+ return {
30
+ label: o,
31
+ url: s,
32
+ end: a + 1,
33
+ flat: !/[[\]]/.test(o) && !/[[\]]/.test(s)
34
+ };
35
+ }
36
+ function l(e, t, n) {
37
+ return e.lastIndex = n, e.exec(t);
38
+ }
39
+ function u(u) {
40
+ let d = [], f = s(u), p = 0, m = 0, h = (e) => {
41
+ e > m && d.push({
42
+ kind: "text",
43
+ value: u.slice(m, e)
44
+ });
45
+ }, g = (e, t) => {
46
+ h(p), d.push(e), p = t, m = t;
47
+ };
48
+ for (; p < u.length;) {
49
+ let s = u[p];
50
+ if (s === "@" && u[p + 1] === "[") {
51
+ let e = c(u, p + 1, f);
52
+ if (e) {
53
+ e.flat && e.label && e.url.trim() ? g({
54
+ kind: "mention",
55
+ name: e.label,
56
+ actorId: e.url.trim()
57
+ }, e.end) : p = e.end;
58
+ continue;
59
+ }
60
+ }
61
+ if (s === "!" && u[p + 1] === "[") {
62
+ let t = c(u, p + 1, f);
63
+ if (t) {
64
+ let n = t.flat ? e.exec(t.url.trim())?.[1] : void 0;
65
+ n ? g({
66
+ kind: "image",
67
+ alt: t.label,
68
+ fileId: n
69
+ }, t.end) : p = t.end;
70
+ continue;
71
+ }
72
+ }
73
+ if (s === "[") {
74
+ let e = c(u, p, f);
75
+ if (e) {
76
+ let n = e.url.trim();
77
+ e.flat && t.test(n) ? g({
78
+ kind: "link",
79
+ text: e.label || n,
80
+ url: n
81
+ }, e.end) : p = e.end;
82
+ continue;
83
+ }
84
+ }
85
+ if (s === "`" && u.startsWith("```", p)) {
86
+ let e = l(n, u, p);
87
+ if (e) {
88
+ g({
89
+ kind: "code-block",
90
+ value: e[1]
91
+ }, p + e[0].length);
92
+ continue;
93
+ }
94
+ }
95
+ if (s === "`") {
96
+ let e = l(r, u, p);
97
+ if (e) {
98
+ g({
99
+ kind: "inline-code",
100
+ value: e[1]
101
+ }, p + e[0].length);
102
+ continue;
103
+ }
104
+ }
105
+ if (s === "*" && u[p + 1] === "*") {
106
+ let e = l(i, u, p);
107
+ if (e) {
108
+ g({
109
+ kind: "bold",
110
+ value: e[1]
111
+ }, p + e[0].length);
112
+ continue;
113
+ }
114
+ }
115
+ if (s === "*") {
116
+ let e = l(a, u, p);
117
+ if (e) {
118
+ g({
119
+ kind: "italic",
120
+ value: e[1]
121
+ }, p + e[0].length);
122
+ continue;
123
+ }
124
+ }
125
+ if (s === "_") {
126
+ let e = l(o, u, p);
127
+ if (e) {
128
+ g({
129
+ kind: "italic",
130
+ value: e[1]
131
+ }, p + e[0].length);
132
+ continue;
133
+ }
134
+ }
135
+ p += 1;
136
+ }
137
+ return h(u.length), d;
138
+ }
139
+ //#endregion
140
+ export { u as tokenizeCommentBody };
141
+
142
+ //# sourceMappingURL=commentTokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commentTokens.js","names":[],"sources":["../../../src/planmagnet/comments/commentTokens.ts"],"sourcesContent":["export type CommentToken =\n | { kind: 'text'; value: string }\n | { kind: 'mention'; name: string; actorId: string }\n | { kind: 'image'; alt: string; fileId: string }\n | { kind: 'link'; text: string; url: string }\n | { kind: 'inline-code'; value: string }\n | { kind: 'code-block'; value: string }\n | { kind: 'bold'; value: string }\n | { kind: 'italic'; value: string };\n\nconst IMAGE_TARGET_RE = /^fileId:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;\nconst HTTPS_URL_RE = /^https:\\/\\/[^\\s<>\"'`]+$/i;\nconst FENCED_CODE_RE = /```([\\s\\S]{0,4000}?)```/y;\nconst INLINE_CODE_RE = /`([^`\\n]{1,500})`/y;\nconst BOLD_RE = /\\*\\*([^\\n]{1,1000}?)\\*\\*/y;\nconst ITALIC_STAR_RE = /\\*([^\\n*]{1,1000}?)\\*/y;\nconst ITALIC_UNDERSCORE_RE = /_([^\\n_]{1,1000}?)_/y;\n\ninterface ClosingPairs {\n brackets: Int32Array;\n parens: Int32Array;\n}\n\n// The server uses balanced bracket/paren matching rather than stopping at the first ')' in a URL.\n// Pair every delimiter once so malformed or deeply nested text remains linear on every read, not\n// quadratic in the number of '[' characters (comments are read far more often than they are saved).\nfunction closingPairs(text: string): ClosingPairs {\n const brackets = new Int32Array(text.length).fill(-1);\n const parens = new Int32Array(text.length).fill(-1);\n const bracketStack: number[] = [];\n const parenStack: number[] = [];\n\n for (let index = 0; index < text.length; index += 1) {\n const char = text[index];\n if (char === '\\n') {\n bracketStack.length = 0;\n parenStack.length = 0;\n } else if (char === '[') {\n bracketStack.push(index);\n } else if (char === ']') {\n const open = bracketStack.pop();\n if (open !== undefined) brackets[open] = index;\n } else if (char === '(') {\n parenStack.push(index);\n } else if (char === ')') {\n const open = parenStack.pop();\n if (open !== undefined) parens[open] = index;\n }\n }\n\n return { brackets, parens };\n}\n\ninterface BracketParenSpan {\n label: string;\n url: string;\n end: number;\n flat: boolean;\n}\n\nfunction bracketParenSpan(\n text: string,\n open: number,\n pairs: ClosingPairs\n): BracketParenSpan | null {\n const labelClose = pairs.brackets[open];\n if (labelClose < 0 || text[labelClose + 1] !== '(') return null;\n const parenOpen = labelClose + 1;\n const parenClose = pairs.parens[parenOpen];\n if (parenClose < 0) return null;\n const label = text.slice(open + 1, labelClose);\n const url = text.slice(parenOpen + 1, parenClose);\n return {\n label,\n url,\n end: parenClose + 1,\n flat: !/[[\\]]/.test(label) && !/[[\\]]/.test(url),\n };\n}\n\nfunction execAt(re: RegExp, text: string, index: number): RegExpExecArray | null {\n re.lastIndex = index;\n return re.exec(text);\n}\n\n/** Parse only the server's six safe token shapes. Rejected or malformed candidates remain plain text. */\nexport function tokenizeCommentBody(text: string): CommentToken[] {\n const tokens: CommentToken[] = [];\n const pairs = closingPairs(text);\n let index = 0;\n let textStart = 0;\n\n const flush = (end: number) => {\n if (end > textStart) tokens.push({ kind: 'text', value: text.slice(textStart, end) });\n };\n const accept = (token: CommentToken, end: number) => {\n flush(index);\n tokens.push(token);\n index = end;\n textStart = end;\n };\n\n while (index < text.length) {\n const char = text[index];\n\n if (char === '@' && text[index + 1] === '[') {\n const span = bracketParenSpan(text, index + 1, pairs);\n if (span) {\n if (span.flat && span.label && span.url.trim()) {\n accept({ kind: 'mention', name: span.label, actorId: span.url.trim() }, span.end);\n } else {\n index = span.end;\n }\n continue;\n }\n }\n\n if (char === '!' && text[index + 1] === '[') {\n const span = bracketParenSpan(text, index + 1, pairs);\n if (span) {\n const fileId = span.flat ? IMAGE_TARGET_RE.exec(span.url.trim())?.[1] : undefined;\n if (fileId) {\n accept({ kind: 'image', alt: span.label, fileId }, span.end);\n } else {\n index = span.end;\n }\n continue;\n }\n }\n\n if (char === '[') {\n const span = bracketParenSpan(text, index, pairs);\n if (span) {\n const url = span.url.trim();\n if (span.flat && HTTPS_URL_RE.test(url)) {\n accept({ kind: 'link', text: span.label || url, url }, span.end);\n } else {\n index = span.end;\n }\n continue;\n }\n }\n\n if (char === '`' && text.startsWith('```', index)) {\n const match = execAt(FENCED_CODE_RE, text, index);\n if (match) {\n accept({ kind: 'code-block', value: match[1] }, index + match[0].length);\n continue;\n }\n }\n\n if (char === '`') {\n const match = execAt(INLINE_CODE_RE, text, index);\n if (match) {\n accept({ kind: 'inline-code', value: match[1] }, index + match[0].length);\n continue;\n }\n }\n\n if (char === '*' && text[index + 1] === '*') {\n const match = execAt(BOLD_RE, text, index);\n if (match) {\n accept({ kind: 'bold', value: match[1] }, index + match[0].length);\n continue;\n }\n }\n\n if (char === '*') {\n const match = execAt(ITALIC_STAR_RE, text, index);\n if (match) {\n accept({ kind: 'italic', value: match[1] }, index + match[0].length);\n continue;\n }\n }\n\n if (char === '_') {\n const match = execAt(ITALIC_UNDERSCORE_RE, text, index);\n if (match) {\n accept({ kind: 'italic', value: match[1] }, index + match[0].length);\n continue;\n }\n }\n\n index += 1;\n }\n\n flush(text.length);\n return tokens;\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { ReactElement } from 'react';
2
+
3
+ /**
4
+ * Shared display helpers for CWF-12 comment attachments (and the pending attachments still uploading in the
5
+ * composer) — byte-count formatting and a mime-type → icon mapping, used by `CommentAttachments.tsx` (posted
6
+ * attachments) and `CommentAttachmentUpload.tsx` (files queued/uploading before the comment is posted).
7
+ *
8
+ * `mimeTypeIcon` returns the icon ELEMENT directly (never a component reference picked by a lookup and then
9
+ * rendered as a JSX tag, e.g. `const Icon = ...; <Icon/>`) — `react-hooks/static-components` (this repo's
10
+ * eslint config enables the full React Compiler rule set) rejects that shape outright, on the reasoning that a
11
+ * JSX tag resolved from a value computed during render cannot be proven stable across renders. Returning the
12
+ * element from a plain call (`{mimeTypeIcon(type)}`) sidesteps the rule entirely — it's never a JSX tag, just an
13
+ * ordinary value embedded in the tree, same as any other expression child.
14
+ */
15
+ /** `1536` -> `"1.5 KB"`. Binary (1024) units, one decimal place under 10 of a unit, whole otherwise — matches the
16
+ * precision wspace-files-svc's own file browser shows elsewhere in the platform. */
17
+ export declare function formatBytes(bytes: number): string;
18
+ /** A representative icon for a comment attachment's snapshotted `mimeType` — purely decorative (chips always
19
+ * also show the real file name), so an unrecognized type falls back to a generic file glyph rather than
20
+ * guessing wrong. */
21
+ export declare function mimeTypeIcon(mimeType: string, className?: string): ReactElement;
22
+ //# sourceMappingURL=fileDisplay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileDisplay.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/fileDisplay.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAE1C;;;;;;;;;;;GAWG;AAEH;oFACoF;AACpF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;qBAEqB;AACrB,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,SAA+C,GACvD,YAAY,CAgBd"}
@@ -0,0 +1,48 @@
1
+ import { File as e, FileArchive as t, FileAudio as n, FileCode2 as r, FileSpreadsheet as i, FileText as a, FileVideo as o, Image as s } from "lucide-react";
2
+ import { jsx as c } from "react/jsx-runtime";
3
+ //#region src/planmagnet/comments/fileDisplay.tsx
4
+ function l(e) {
5
+ if (!Number.isFinite(e) || e <= 0) return "0 B";
6
+ let t = [
7
+ "B",
8
+ "KB",
9
+ "MB",
10
+ "GB",
11
+ "TB"
12
+ ], n = Math.min(Math.floor(Math.log(e) / Math.log(1024)), t.length - 1);
13
+ if (n === 0) return `${e} B`;
14
+ let r = e / 1024 ** n;
15
+ return `${r < 10 ? r.toFixed(1) : Math.round(r)} ${t[n]}`;
16
+ }
17
+ function u(l, u = "h-3.5 w-3.5 shrink-0 text-muted-foreground") {
18
+ let d = l.toLowerCase();
19
+ return d.startsWith("image/") ? /* @__PURE__ */ c(s, {
20
+ className: u,
21
+ "aria-hidden": "true"
22
+ }) : d.startsWith("video/") ? /* @__PURE__ */ c(o, {
23
+ className: u,
24
+ "aria-hidden": "true"
25
+ }) : d.startsWith("audio/") ? /* @__PURE__ */ c(n, {
26
+ className: u,
27
+ "aria-hidden": "true"
28
+ }) : d === "application/pdf" ? /* @__PURE__ */ c(a, {
29
+ className: u,
30
+ "aria-hidden": "true"
31
+ }) : d.includes("spreadsheet") || d.includes("csv") || d.includes("excel") ? /* @__PURE__ */ c(i, {
32
+ className: u,
33
+ "aria-hidden": "true"
34
+ }) : d.includes("zip") || d.includes("compressed") || d.includes("tar") ? /* @__PURE__ */ c(t, {
35
+ className: u,
36
+ "aria-hidden": "true"
37
+ }) : d.startsWith("text/") || d.includes("json") || d.includes("xml") ? /* @__PURE__ */ c(r, {
38
+ className: u,
39
+ "aria-hidden": "true"
40
+ }) : /* @__PURE__ */ c(e, {
41
+ className: u,
42
+ "aria-hidden": "true"
43
+ });
44
+ }
45
+ //#endregion
46
+ export { l as formatBytes, u as mimeTypeIcon };
47
+
48
+ //# sourceMappingURL=fileDisplay.js.map