@agimon-ai/doompi-author 0.0.1-alpha.10

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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/api-contracts.cjs +1 -0
  5. package/dist/api-contracts.d.cts +2 -0
  6. package/dist/api-contracts.d.mts +2 -0
  7. package/dist/api-contracts.mjs +1 -0
  8. package/dist/authorFacade.cjs +1 -0
  9. package/dist/authorFacade.d.cts +3 -0
  10. package/dist/authorFacade.d.mts +3 -0
  11. package/dist/authorFacade.mjs +1 -0
  12. package/dist/constants/author.cjs +4 -0
  13. package/dist/constants/author.cjs.map +1 -0
  14. package/dist/constants/author.mjs +4 -0
  15. package/dist/constants/author.mjs.map +1 -0
  16. package/dist/controllers/authorApi.cjs +2 -0
  17. package/dist/controllers/authorApi.cjs.map +1 -0
  18. package/dist/controllers/authorApi.mjs +2 -0
  19. package/dist/controllers/authorApi.mjs.map +1 -0
  20. package/dist/controllers/authorBridgeApi.cjs +2 -0
  21. package/dist/controllers/authorBridgeApi.cjs.map +1 -0
  22. package/dist/controllers/authorBridgeApi.mjs +2 -0
  23. package/dist/controllers/authorBridgeApi.mjs.map +1 -0
  24. package/dist/controllers/authorBridgeMethod.cjs +2 -0
  25. package/dist/controllers/authorBridgeMethod.cjs.map +1 -0
  26. package/dist/controllers/authorBridgeMethod.mjs +2 -0
  27. package/dist/controllers/authorBridgeMethod.mjs.map +1 -0
  28. package/dist/controllers/authorDocumentApi.cjs +2 -0
  29. package/dist/controllers/authorDocumentApi.cjs.map +1 -0
  30. package/dist/controllers/authorDocumentApi.mjs +2 -0
  31. package/dist/controllers/authorDocumentApi.mjs.map +1 -0
  32. package/dist/controllers/authorPiMode.cjs +2 -0
  33. package/dist/controllers/authorPiMode.cjs.map +1 -0
  34. package/dist/controllers/authorPiMode.mjs +2 -0
  35. package/dist/controllers/authorPiMode.mjs.map +1 -0
  36. package/dist/controllers/doomAuthorCommand.cjs +2 -0
  37. package/dist/controllers/doomAuthorCommand.cjs.map +1 -0
  38. package/dist/controllers/doomAuthorCommand.d.cts +5 -0
  39. package/dist/controllers/doomAuthorCommand.d.cts.map +1 -0
  40. package/dist/controllers/doomAuthorCommand.d.mts +5 -0
  41. package/dist/controllers/doomAuthorCommand.d.mts.map +1 -0
  42. package/dist/controllers/doomAuthorCommand.mjs +2 -0
  43. package/dist/controllers/doomAuthorCommand.mjs.map +1 -0
  44. package/dist/controllers/webAuthorChannel.cjs +2 -0
  45. package/dist/controllers/webAuthorChannel.cjs.map +1 -0
  46. package/dist/controllers/webAuthorChannel.mjs +2 -0
  47. package/dist/controllers/webAuthorChannel.mjs.map +1 -0
  48. package/dist/extensions/pi.cjs +2 -0
  49. package/dist/extensions/pi.cjs.map +1 -0
  50. package/dist/extensions/pi.d.cts +7 -0
  51. package/dist/extensions/pi.d.cts.map +1 -0
  52. package/dist/extensions/pi.d.mts +7 -0
  53. package/dist/extensions/pi.d.mts.map +1 -0
  54. package/dist/extensions/pi.mjs +2 -0
  55. package/dist/extensions/pi.mjs.map +1 -0
  56. package/dist/extensions/server.cjs +2 -0
  57. package/dist/extensions/server.cjs.map +1 -0
  58. package/dist/extensions/server.d.cts +5 -0
  59. package/dist/extensions/server.d.cts.map +1 -0
  60. package/dist/extensions/server.d.mts +5 -0
  61. package/dist/extensions/server.d.mts.map +1 -0
  62. package/dist/extensions/server.mjs +2 -0
  63. package/dist/extensions/server.mjs.map +1 -0
  64. package/dist/index.cjs +1 -0
  65. package/dist/index.d.cts +20 -0
  66. package/dist/index.d.mts +20 -0
  67. package/dist/index.mjs +1 -0
  68. package/dist/models/authorBridgeState.cjs +2 -0
  69. package/dist/models/authorBridgeState.cjs.map +1 -0
  70. package/dist/models/authorBridgeState.d.cts +31 -0
  71. package/dist/models/authorBridgeState.d.cts.map +1 -0
  72. package/dist/models/authorBridgeState.d.mts +31 -0
  73. package/dist/models/authorBridgeState.d.mts.map +1 -0
  74. package/dist/models/authorBridgeState.mjs +2 -0
  75. package/dist/models/authorBridgeState.mjs.map +1 -0
  76. package/dist/models/authorMode.cjs +2 -0
  77. package/dist/models/authorMode.cjs.map +1 -0
  78. package/dist/models/authorMode.d.cts +9 -0
  79. package/dist/models/authorMode.d.cts.map +1 -0
  80. package/dist/models/authorMode.d.mts +9 -0
  81. package/dist/models/authorMode.d.mts.map +1 -0
  82. package/dist/models/authorMode.mjs +2 -0
  83. package/dist/models/authorMode.mjs.map +1 -0
  84. package/dist/schemas/apiContracts.cjs +4 -0
  85. package/dist/schemas/apiContracts.cjs.map +1 -0
  86. package/dist/schemas/apiContracts.d.cts +53 -0
  87. package/dist/schemas/apiContracts.d.cts.map +1 -0
  88. package/dist/schemas/apiContracts.d.mts +53 -0
  89. package/dist/schemas/apiContracts.d.mts.map +1 -0
  90. package/dist/schemas/apiContracts.mjs +4 -0
  91. package/dist/schemas/apiContracts.mjs.map +1 -0
  92. package/dist/schemas/authorFacade.cjs +2 -0
  93. package/dist/schemas/authorFacade.cjs.map +1 -0
  94. package/dist/schemas/authorFacade.d.cts +112 -0
  95. package/dist/schemas/authorFacade.d.cts.map +1 -0
  96. package/dist/schemas/authorFacade.d.mts +112 -0
  97. package/dist/schemas/authorFacade.d.mts.map +1 -0
  98. package/dist/schemas/authorFacade.mjs +2 -0
  99. package/dist/schemas/authorFacade.mjs.map +1 -0
  100. package/dist/schemas/authorTools.cjs +2 -0
  101. package/dist/schemas/authorTools.cjs.map +1 -0
  102. package/dist/schemas/authorTools.d.cts +12 -0
  103. package/dist/schemas/authorTools.d.cts.map +1 -0
  104. package/dist/schemas/authorTools.d.mts +12 -0
  105. package/dist/schemas/authorTools.d.mts.map +1 -0
  106. package/dist/schemas/authorTools.mjs +2 -0
  107. package/dist/schemas/authorTools.mjs.map +1 -0
  108. package/dist/services/authorCatalog/index.cjs +2 -0
  109. package/dist/services/authorCatalog/index.cjs.map +1 -0
  110. package/dist/services/authorCatalog/index.d.cts +6 -0
  111. package/dist/services/authorCatalog/index.d.cts.map +1 -0
  112. package/dist/services/authorCatalog/index.d.mts +6 -0
  113. package/dist/services/authorCatalog/index.d.mts.map +1 -0
  114. package/dist/services/authorCatalog/index.mjs +2 -0
  115. package/dist/services/authorCatalog/index.mjs.map +1 -0
  116. package/dist/services/authorCatalog/monitor.cjs +2 -0
  117. package/dist/services/authorCatalog/monitor.cjs.map +1 -0
  118. package/dist/services/authorCatalog/monitor.d.cts +20 -0
  119. package/dist/services/authorCatalog/monitor.d.cts.map +1 -0
  120. package/dist/services/authorCatalog/monitor.d.mts +20 -0
  121. package/dist/services/authorCatalog/monitor.d.mts.map +1 -0
  122. package/dist/services/authorCatalog/monitor.mjs +2 -0
  123. package/dist/services/authorCatalog/monitor.mjs.map +1 -0
  124. package/dist/services/authorCatalog/type.d.cts +10 -0
  125. package/dist/services/authorCatalog/type.d.cts.map +1 -0
  126. package/dist/services/authorCatalog/type.d.mts +10 -0
  127. package/dist/services/authorCatalog/type.d.mts.map +1 -0
  128. package/dist/services/authorPrompt/index.cjs +2 -0
  129. package/dist/services/authorPrompt/index.cjs.map +1 -0
  130. package/dist/services/authorPrompt/index.mjs +2 -0
  131. package/dist/services/authorPrompt/index.mjs.map +1 -0
  132. package/dist/services/structuredDocuments/common.cjs +2 -0
  133. package/dist/services/structuredDocuments/common.cjs.map +1 -0
  134. package/dist/services/structuredDocuments/common.mjs +2 -0
  135. package/dist/services/structuredDocuments/common.mjs.map +1 -0
  136. package/dist/services/structuredDocuments/csv.cjs +7 -0
  137. package/dist/services/structuredDocuments/csv.cjs.map +1 -0
  138. package/dist/services/structuredDocuments/csv.d.cts +7 -0
  139. package/dist/services/structuredDocuments/csv.d.cts.map +1 -0
  140. package/dist/services/structuredDocuments/csv.d.mts +7 -0
  141. package/dist/services/structuredDocuments/csv.d.mts.map +1 -0
  142. package/dist/services/structuredDocuments/csv.mjs +7 -0
  143. package/dist/services/structuredDocuments/csv.mjs.map +1 -0
  144. package/dist/services/structuredDocuments/document.cjs +2 -0
  145. package/dist/services/structuredDocuments/document.cjs.map +1 -0
  146. package/dist/services/structuredDocuments/document.mjs +2 -0
  147. package/dist/services/structuredDocuments/document.mjs.map +1 -0
  148. package/dist/services/structuredDocuments/index.cjs +2 -0
  149. package/dist/services/structuredDocuments/index.cjs.map +1 -0
  150. package/dist/services/structuredDocuments/index.d.cts +13 -0
  151. package/dist/services/structuredDocuments/index.d.cts.map +1 -0
  152. package/dist/services/structuredDocuments/index.d.mts +13 -0
  153. package/dist/services/structuredDocuments/index.d.mts.map +1 -0
  154. package/dist/services/structuredDocuments/index.mjs +2 -0
  155. package/dist/services/structuredDocuments/index.mjs.map +1 -0
  156. package/dist/services/structuredDocuments/markdownSlides.cjs +6 -0
  157. package/dist/services/structuredDocuments/markdownSlides.cjs.map +1 -0
  158. package/dist/services/structuredDocuments/markdownSlides.d.cts +7 -0
  159. package/dist/services/structuredDocuments/markdownSlides.d.cts.map +1 -0
  160. package/dist/services/structuredDocuments/markdownSlides.d.mts +7 -0
  161. package/dist/services/structuredDocuments/markdownSlides.d.mts.map +1 -0
  162. package/dist/services/structuredDocuments/markdownSlides.mjs +6 -0
  163. package/dist/services/structuredDocuments/markdownSlides.mjs.map +1 -0
  164. package/dist/services/structuredDocuments/ooxmlArchive.cjs +2 -0
  165. package/dist/services/structuredDocuments/ooxmlArchive.cjs.map +1 -0
  166. package/dist/services/structuredDocuments/ooxmlArchive.d.cts +13 -0
  167. package/dist/services/structuredDocuments/ooxmlArchive.d.cts.map +1 -0
  168. package/dist/services/structuredDocuments/ooxmlArchive.d.mts +13 -0
  169. package/dist/services/structuredDocuments/ooxmlArchive.d.mts.map +1 -0
  170. package/dist/services/structuredDocuments/ooxmlArchive.mjs +2 -0
  171. package/dist/services/structuredDocuments/ooxmlArchive.mjs.map +1 -0
  172. package/dist/services/structuredDocuments/pptx.cjs +2 -0
  173. package/dist/services/structuredDocuments/pptx.cjs.map +1 -0
  174. package/dist/services/structuredDocuments/pptx.d.cts +7 -0
  175. package/dist/services/structuredDocuments/pptx.d.cts.map +1 -0
  176. package/dist/services/structuredDocuments/pptx.d.mts +7 -0
  177. package/dist/services/structuredDocuments/pptx.d.mts.map +1 -0
  178. package/dist/services/structuredDocuments/pptx.mjs +2 -0
  179. package/dist/services/structuredDocuments/pptx.mjs.map +1 -0
  180. package/dist/services/structuredDocuments/xlsx.cjs +2 -0
  181. package/dist/services/structuredDocuments/xlsx.cjs.map +1 -0
  182. package/dist/services/structuredDocuments/xlsx.d.cts +7 -0
  183. package/dist/services/structuredDocuments/xlsx.d.cts.map +1 -0
  184. package/dist/services/structuredDocuments/xlsx.d.mts +7 -0
  185. package/dist/services/structuredDocuments/xlsx.d.mts.map +1 -0
  186. package/dist/services/structuredDocuments/xlsx.mjs +2 -0
  187. package/dist/services/structuredDocuments/xlsx.mjs.map +1 -0
  188. package/dist/services/structuredDocuments/xmlText.cjs +2 -0
  189. package/dist/services/structuredDocuments/xmlText.cjs.map +1 -0
  190. package/dist/services/structuredDocuments/xmlText.mjs +2 -0
  191. package/dist/services/structuredDocuments/xmlText.mjs.map +1 -0
  192. package/dist/tools/authorTools.cjs +2 -0
  193. package/dist/tools/authorTools.cjs.map +1 -0
  194. package/dist/tools/authorTools.d.cts +6 -0
  195. package/dist/tools/authorTools.d.cts.map +1 -0
  196. package/dist/tools/authorTools.d.mts +6 -0
  197. package/dist/tools/authorTools.d.mts.map +1 -0
  198. package/dist/tools/authorTools.mjs +2 -0
  199. package/dist/tools/authorTools.mjs.map +1 -0
  200. package/dist/types/author.cjs +2 -0
  201. package/dist/types/author.cjs.map +1 -0
  202. package/dist/types/author.d.cts +39 -0
  203. package/dist/types/author.d.cts.map +1 -0
  204. package/dist/types/author.d.mts +39 -0
  205. package/dist/types/author.d.mts.map +1 -0
  206. package/dist/types/author.mjs +2 -0
  207. package/dist/types/author.mjs.map +1 -0
  208. package/dist/types/authorApi.cjs +2 -0
  209. package/dist/types/authorApi.cjs.map +1 -0
  210. package/dist/types/authorApi.d.cts +23 -0
  211. package/dist/types/authorApi.d.cts.map +1 -0
  212. package/dist/types/authorApi.d.mts +23 -0
  213. package/dist/types/authorApi.d.mts.map +1 -0
  214. package/dist/types/authorApi.mjs +2 -0
  215. package/dist/types/authorApi.mjs.map +1 -0
  216. package/dist/types/extension.d.cts +16 -0
  217. package/dist/types/extension.d.cts.map +1 -0
  218. package/dist/types/extension.d.mts +16 -0
  219. package/dist/types/extension.d.mts.map +1 -0
  220. package/dist/types/structuredDocuments.d.cts +60 -0
  221. package/dist/types/structuredDocuments.d.cts.map +1 -0
  222. package/dist/types/structuredDocuments.d.mts +60 -0
  223. package/dist/types/structuredDocuments.d.mts.map +1 -0
  224. package/dist/types/webAuthor.cjs +2 -0
  225. package/dist/types/webAuthor.cjs.map +1 -0
  226. package/dist/types/webAuthor.d.cts +67 -0
  227. package/dist/types/webAuthor.d.cts.map +1 -0
  228. package/dist/types/webAuthor.d.mts +67 -0
  229. package/dist/types/webAuthor.d.mts.map +1 -0
  230. package/dist/types/webAuthor.mjs +2 -0
  231. package/dist/types/webAuthor.mjs.map +1 -0
  232. package/llms.txt +11 -0
  233. package/package.json +147 -0
  234. package/src/extensions/web.ts +71 -0
  235. package/src/prompts/doompi-use-author/SKILL.md +17 -0
  236. package/src/types/author.ts +45 -0
  237. package/src/types/structuredDocuments.ts +63 -0
  238. package/src/types/webAuthor.ts +85 -0
  239. package/src/web/api/authorBroker.ts +14 -0
  240. package/src/web/api/authorBrowserBridge.ts +229 -0
  241. package/src/web/api/authorFiles.ts +221 -0
  242. package/src/web/api/authorMedia.ts +19 -0
  243. package/src/web/api/authorRuntime.ts +165 -0
  244. package/src/web/components/AuthorDocumentPanel.tsx +233 -0
  245. package/src/web/components/AuthorGridOverlay.tsx +82 -0
  246. package/src/web/components/AuthorMediaPreview.tsx +42 -0
  247. package/src/web/components/AuthorMediaView.tsx +357 -0
  248. package/src/web/components/AuthorPanel.tsx +102 -0
  249. package/src/web/components/AuthorRegionDrafts.tsx +103 -0
  250. package/src/web/components/AuthorRequestLog.tsx +179 -0
  251. package/src/web/components/AuthorStructuredView.tsx +122 -0
  252. package/src/web/components/AuthorTextView.tsx +98 -0
  253. package/src/web/components/AuthorToolPalette.tsx +104 -0
  254. package/src/web/components/AuthorVideoControls.tsx +111 -0
  255. package/src/web/components/DescribeAuthorToolsToolCard.tsx +51 -0
  256. package/src/web/components/OpenAuthoringFileToolCard.tsx +67 -0
  257. package/src/web/components/UseAuthorToolsToolCard.tsx +51 -0
  258. package/src/web/lib/authorGrid.ts +125 -0
  259. package/src/web/lib/authorRegions.ts +30 -0
  260. package/src/web/lib/authorViewportTypes.ts +202 -0
  261. package/src/web/lib/describeAuthorToolsToolRender.ts +20 -0
  262. package/src/web/lib/useAuthorToolsToolRender.ts +20 -0
  263. package/src/web/stores/authorCapture.ts +323 -0
  264. package/src/web/stores/authorGridTools.ts +316 -0
  265. package/src/web/stores/authorProfiles.ts +130 -0
  266. package/src/web/stores/authorRequestLifecycle.ts +83 -0
  267. package/src/web/stores/authorStore.ts +61 -0
  268. package/src/web/stores/authorWorkspaceStore.ts +526 -0
  269. package/src/web/tsconfig.json +22 -0
@@ -0,0 +1,526 @@
1
+ import { defineGlobalStore } from '@agimon-ai/doompi-core/web';
2
+
3
+ import type {
4
+ AuthorAnnotation,
5
+ AuthorCrop,
6
+ AuthorDocumentInput,
7
+ AuthorDraftRevision,
8
+ AuthorFocusedDocument,
9
+ AuthorRegionCandidate,
10
+ AuthorRegionDraft,
11
+ AuthorRequestRecord,
12
+ AuthorRequestStatus,
13
+ AuthorToolMode,
14
+ } from '../lib/authorViewportTypes';
15
+
16
+ export const AUTHOR_REGION_LIMIT = 16;
17
+ export const AUTHOR_HISTORY_RECORD_LIMIT = 100;
18
+ export const AUTHOR_HISTORY_BYTE_LIMIT = 512 * 1024;
19
+ const TERMINAL_REQUEST_STATUSES: ReadonlySet<AuthorRequestStatus> = new Set(['COMPLETE', 'FAILED', 'CANCELLED']);
20
+
21
+ export interface AuthorWorkspaceDocument extends AuthorDocumentInput {
22
+ path: string;
23
+ annotations: readonly AuthorAnnotation[];
24
+ revisions: readonly AuthorDraftRevision[];
25
+ crop?: AuthorCrop;
26
+ saveRequest: number;
27
+ version: number;
28
+ savedVersion: number;
29
+ savingVersion?: number;
30
+ }
31
+
32
+ export interface AuthorSessionWorkspace {
33
+ generation: number;
34
+ focusedDocument?: AuthorFocusedDocument;
35
+ activeTool: AuthorToolMode;
36
+ candidate?: AuthorRegionCandidate;
37
+ videoSeekRequest?: { path: string; generation: number; timeSeconds: number; sequence: number };
38
+ regions: readonly AuthorRegionDraft[];
39
+ requests: readonly AuthorRequestRecord[];
40
+ }
41
+
42
+ export interface AuthorWorkspaceState {
43
+ documents: Readonly<Record<string, AuthorWorkspaceDocument>>;
44
+ sessions: Readonly<Record<string, AuthorSessionWorkspace>>;
45
+ }
46
+
47
+ const EMPTY_SESSION: AuthorSessionWorkspace = { generation: 0, activeTool: 'select', regions: [], requests: [] };
48
+
49
+ export function normalizeAuthorPath(path: string): string {
50
+ const parts: string[] = [];
51
+ for (const part of path.trim().replaceAll('\\', '/').split('/')) {
52
+ if (part === '' || part === '.') continue;
53
+ if (part === '..') parts.pop();
54
+ else parts.push(part);
55
+ }
56
+ return parts.join('/');
57
+ }
58
+
59
+ export function authorDocumentKey(sessionId: string, path: string): string {
60
+ return `${sessionId}\n${normalizeAuthorPath(path)}`;
61
+ }
62
+
63
+ export const authorWorkspace = defineGlobalStore<AuthorWorkspaceState>({ documents: {}, sessions: {} });
64
+
65
+ export function authorDocument(sessionId: string | null, path: string): AuthorWorkspaceDocument | undefined {
66
+ if (sessionId === null) return undefined;
67
+ return authorWorkspace.store.state.documents[authorDocumentKey(sessionId, path)];
68
+ }
69
+
70
+ export function authorSessionWorkspace(sessionId: string | null): AuthorSessionWorkspace {
71
+ if (sessionId === null) return EMPTY_SESSION;
72
+ return authorWorkspace.store.state.sessions[sessionId] ?? EMPTY_SESSION;
73
+ }
74
+
75
+ export function putAuthorDocument(sessionId: string, input: AuthorDocumentInput): AuthorWorkspaceDocument {
76
+ const path = normalizeAuthorPath(input.path);
77
+ if (path === '') throw new Error('Author document path must not be empty');
78
+ const key = authorDocumentKey(sessionId, path);
79
+ const staleThumbnails: string[] = [];
80
+ let result!: AuthorWorkspaceDocument;
81
+ authorWorkspace.update((state) => {
82
+ const previous = state.documents[key];
83
+ const sourceChanged =
84
+ previous?.sourceSha256 !== undefined &&
85
+ input.sourceSha256 !== undefined &&
86
+ previous.sourceSha256 !== input.sourceSha256;
87
+ const baselineVersion = sourceChanged ? previous.version + 1 : (previous?.version ?? 0);
88
+ result = {
89
+ ...input,
90
+ path,
91
+ annotations: sourceChanged ? [] : (previous?.annotations ?? []),
92
+ revisions: sourceChanged ? [] : (previous?.revisions ?? []),
93
+ saveRequest: previous?.saveRequest ?? 0,
94
+ version: baselineVersion,
95
+ savedVersion: sourceChanged ? baselineVersion : (previous?.savedVersion ?? 0),
96
+ ...(sourceChanged || previous?.crop === undefined ? {} : { crop: previous.crop }),
97
+ };
98
+ const session = state.sessions[sessionId];
99
+ let sessions = state.sessions;
100
+ if (sourceChanged && session?.focusedDocument?.path === path) {
101
+ staleThumbnails.push(
102
+ ...(session.candidate?.thumbnailUrl === undefined ? [] : [session.candidate.thumbnailUrl]),
103
+ ...session.regions.flatMap((region) => (region.thumbnailUrl === undefined ? [] : [region.thumbnailUrl])),
104
+ );
105
+ const now = Date.now();
106
+ const generation = session.generation + 1;
107
+ sessions = {
108
+ ...sessions,
109
+ [sessionId]: {
110
+ ...session,
111
+ generation,
112
+ focusedDocument: {
113
+ path,
114
+ generation,
115
+ revision: baselineVersion,
116
+ sourceSha256: input.sourceSha256,
117
+ focusedAt: now,
118
+ },
119
+ regions: [],
120
+ candidate: undefined,
121
+ requests: session.requests.map((request) =>
122
+ request.documentPath === path &&
123
+ (request.status === 'REQUESTED' || request.status === 'CHANGING' || request.status === 'CHANGED')
124
+ ? {
125
+ ...request,
126
+ status: 'FAILED',
127
+ currentOperation: undefined,
128
+ error: 'The document source changed before the request completed.',
129
+ updatedAt: now,
130
+ }
131
+ : request,
132
+ ),
133
+ },
134
+ };
135
+ }
136
+ return { documents: { ...state.documents, [key]: result }, sessions };
137
+ });
138
+ staleThumbnails.forEach(revokeThumbnail);
139
+ return result;
140
+ }
141
+
142
+ /** Claims focus and returns a generation token that makes cleanup race-safe. */
143
+ export function focusAuthorDocument(sessionId: string, path: string, revision: number, sourceSha256?: string): number {
144
+ const normalized = normalizeAuthorPath(path);
145
+ const staleThumbnails: string[] = [];
146
+ let generation = 0;
147
+ authorWorkspace.update((state) => {
148
+ const session = state.sessions[sessionId] ?? EMPTY_SESSION;
149
+ generation = session.generation + 1;
150
+ const keepRegions = session.focusedDocument?.path === normalized;
151
+ if (!keepRegions) {
152
+ staleThumbnails.push(
153
+ ...(session.candidate?.thumbnailUrl === undefined ? [] : [session.candidate.thumbnailUrl]),
154
+ ...session.regions.flatMap((region) => (region.thumbnailUrl === undefined ? [] : [region.thumbnailUrl])),
155
+ );
156
+ }
157
+ return {
158
+ ...state,
159
+ sessions: {
160
+ ...state.sessions,
161
+ [sessionId]: {
162
+ ...session,
163
+ generation,
164
+ focusedDocument: {
165
+ path: normalized,
166
+ generation,
167
+ revision,
168
+ sourceSha256,
169
+ focusedAt: Date.now(),
170
+ },
171
+ regions: keepRegions ? session.regions : [],
172
+ candidate: keepRegions ? session.candidate : undefined,
173
+ },
174
+ },
175
+ };
176
+ });
177
+ staleThumbnails.forEach(revokeThumbnail);
178
+ return generation;
179
+ }
180
+
181
+ export function syncAuthorDocumentFocus(
182
+ sessionId: string,
183
+ generation: number,
184
+ revision: number,
185
+ sourceSha256?: string,
186
+ ): void {
187
+ updateSession(sessionId, (session) => {
188
+ const focused = session.focusedDocument;
189
+ if (focused === undefined || focused.generation !== generation) return session;
190
+ if (focused.revision === revision && focused.sourceSha256 === sourceSha256) return session;
191
+ return { ...session, focusedDocument: { ...focused, revision, sourceSha256 } };
192
+ });
193
+ }
194
+
195
+ export function releaseAuthorDocumentFocus(sessionId: string, generation: number): void {
196
+ const staleThumbnails: string[] = [];
197
+ updateSession(sessionId, (session) => {
198
+ if (session.focusedDocument?.generation !== generation) return session;
199
+ staleThumbnails.push(
200
+ ...(session.candidate?.thumbnailUrl === undefined ? [] : [session.candidate.thumbnailUrl]),
201
+ ...session.regions.flatMap((region) => (region.thumbnailUrl === undefined ? [] : [region.thumbnailUrl])),
202
+ );
203
+ return { ...session, focusedDocument: undefined, candidate: undefined, regions: [] };
204
+ });
205
+ staleThumbnails.forEach(revokeThumbnail);
206
+ }
207
+
208
+ export function seekAuthorVideo(sessionId: string, timeSeconds: number): boolean {
209
+ const workspace = authorSessionWorkspace(sessionId);
210
+ const focused = workspace.focusedDocument;
211
+ if (
212
+ !focused ||
213
+ workspace.candidate ||
214
+ !Number.isFinite(timeSeconds) ||
215
+ timeSeconds < 0 ||
216
+ authorDocument(sessionId, focused.path)?.kind !== 'video'
217
+ )
218
+ return false;
219
+ updateSession(sessionId, (session) => ({
220
+ ...session,
221
+ activeTool: 'select',
222
+ videoSeekRequest: {
223
+ path: focused.path,
224
+ generation: focused.generation,
225
+ timeSeconds,
226
+ sequence: (session.videoSeekRequest?.sequence ?? 0) + 1,
227
+ },
228
+ }));
229
+ return true;
230
+ }
231
+
232
+ export function setAuthorToolMode(sessionId: string, activeTool: AuthorToolMode): void {
233
+ updateSession(sessionId, (session) => (session.activeTool === activeTool ? session : { ...session, activeTool }));
234
+ }
235
+
236
+ export function setAuthorRegionCandidate(sessionId: string, candidate: AuthorRegionCandidate | undefined): void {
237
+ let staleThumbnail: string | undefined;
238
+ updateSession(sessionId, (session) => {
239
+ const focused = session.focusedDocument;
240
+ if (candidate !== undefined) {
241
+ if (focused === undefined || focused.path !== normalizeAuthorPath(candidate.documentPath)) {
242
+ throw new Error('The Author selection does not belong to the focused document.');
243
+ }
244
+ if (focused.revision !== candidate.revision || focused.sourceSha256 !== candidate.sourceSha256) {
245
+ throw new Error('The Author selection is stale for the focused document.');
246
+ }
247
+ }
248
+ staleThumbnail = session.candidate?.thumbnailUrl;
249
+ return { ...session, candidate: candidate === undefined ? undefined : structuredClone(candidate) };
250
+ });
251
+ if (staleThumbnail !== undefined && staleThumbnail !== candidate?.thumbnailUrl) revokeThumbnail(staleThumbnail);
252
+ }
253
+
254
+ export function commitAuthorRegion(sessionId: string, comment: string): string {
255
+ const id = crypto.randomUUID();
256
+ const session = authorSessionWorkspace(sessionId);
257
+ if (session.candidate === undefined) throw new Error('Select a document region before adding a comment.');
258
+ addAuthorRegion(sessionId, { ...session.candidate, id, comment });
259
+ updateSession(sessionId, (current) => ({ ...current, candidate: undefined }));
260
+ return id;
261
+ }
262
+
263
+ export function addAuthorRegion(sessionId: string, region: AuthorRegionDraft): void {
264
+ if (region.comment.trim() === '') throw new Error('Every Author region requires a comment.');
265
+ updateSession(sessionId, (session) => {
266
+ const focused = session.focusedDocument;
267
+ if (focused === undefined || focused.path !== normalizeAuthorPath(region.documentPath)) {
268
+ throw new Error('The Author region does not belong to the focused document.');
269
+ }
270
+ if (focused.revision !== region.revision || focused.sourceSha256 !== region.sourceSha256) {
271
+ throw new Error('The Author region is stale for the focused document.');
272
+ }
273
+ if (session.regions.length >= AUTHOR_REGION_LIMIT)
274
+ throw new Error(`Author requests support at most ${AUTHOR_REGION_LIMIT} regions.`);
275
+ if (session.regions.some((candidate) => candidate.id === region.id))
276
+ throw new Error(`Author region '${region.id}' already exists.`);
277
+ return { ...session, regions: [...session.regions, copyRegion(region)] };
278
+ });
279
+ }
280
+
281
+ export function removeAuthorRegion(sessionId: string, regionId: string): void {
282
+ let thumbnail: string | undefined;
283
+ updateSession(sessionId, (session) => {
284
+ const region = session.regions.find((candidate) => candidate.id === regionId);
285
+ thumbnail = region?.thumbnailUrl;
286
+ const regions = session.regions.filter((candidate) => candidate.id !== regionId);
287
+ return regions.length === session.regions.length ? session : { ...session, regions };
288
+ });
289
+ if (thumbnail !== undefined) revokeThumbnail(thumbnail);
290
+ }
291
+
292
+ export function updateAuthorRegionComment(sessionId: string, regionId: string, comment: string): void {
293
+ if (comment.trim() === '') throw new Error('Every Author region requires a comment.');
294
+ updateSession(sessionId, (session) => ({
295
+ ...session,
296
+ regions: session.regions.map((region) => (region.id === regionId ? { ...region, comment } : region)),
297
+ }));
298
+ }
299
+
300
+ export function putAuthorRequest(sessionId: string, record: AuthorRequestRecord): void {
301
+ if (record.requestText.trim() === '') throw new Error('An Author request requires verbatim request text.');
302
+ if (record.regions.length === 0 || record.regions.some((region) => region.comment.trim() === '')) {
303
+ throw new Error('An Author request requires at least one commented region.');
304
+ }
305
+ updateSession(sessionId, (session) => {
306
+ if (session.requests.some((request) => request.id === record.id))
307
+ throw new Error(`Author request '${record.id}' already exists.`);
308
+ return { ...session, requests: boundHistory([...session.requests, copyRequest(record)]) };
309
+ });
310
+ }
311
+
312
+ export function updateAuthorRequest(
313
+ sessionId: string,
314
+ requestId: string,
315
+ update: (record: AuthorRequestRecord) => AuthorRequestRecord,
316
+ ): void {
317
+ updateSession(sessionId, (session) => ({
318
+ ...session,
319
+ requests: boundHistory(
320
+ session.requests.map((request) => (request.id === requestId ? copyRequest(update(request)) : request)),
321
+ ),
322
+ }));
323
+ }
324
+
325
+ export function reviseAuthorDocument(sessionId: string, path: string, content: string): void {
326
+ updateDocument(sessionId, path, (document) => {
327
+ if (document.content === content) return document;
328
+ const version = document.version + 1;
329
+ return {
330
+ ...document,
331
+ content,
332
+ version,
333
+ revisions: [...document.revisions, { revision: version, content }],
334
+ };
335
+ });
336
+ }
337
+
338
+ export function reviseAuthorFragment(sessionId: string, path: string, fragmentId: string, text: string): void {
339
+ updateDocument(sessionId, path, (document) => {
340
+ const fragment = document.fragments?.find((candidate) => candidate.id === fragmentId);
341
+ if (fragment === undefined || fragment.text === text) return document;
342
+ const version = document.version + 1;
343
+ return {
344
+ ...document,
345
+ fragments: document.fragments?.map((fragment) => (fragment.id === fragmentId ? { ...fragment, text } : fragment)),
346
+ version,
347
+ revisions: [...document.revisions, { revision: version, content: text }],
348
+ };
349
+ });
350
+ }
351
+
352
+ export function addAuthorAnnotation(sessionId: string, path: string, annotation: AuthorAnnotation): void {
353
+ updateDocument(sessionId, path, (document) => ({ ...document, annotations: [...document.annotations, annotation] }));
354
+ }
355
+
356
+ export function setAuthorCrop(sessionId: string, path: string, crop: AuthorCrop | undefined): void {
357
+ updateDocument(sessionId, path, (document) => {
358
+ const version = document.version + 1;
359
+ return {
360
+ ...document,
361
+ ...(crop === undefined ? { crop: undefined } : { crop }),
362
+ version,
363
+ revisions: [...document.revisions, { revision: version, content: JSON.stringify(crop ?? null) }],
364
+ };
365
+ });
366
+ }
367
+
368
+ export function requestAuthorSave(sessionId: string, path: string): void {
369
+ updateDocument(sessionId, path, (document) => ({
370
+ ...document,
371
+ saveRequest: document.saveRequest + 1,
372
+ savingVersion: document.version,
373
+ }));
374
+ }
375
+
376
+ export function failAuthorSave(sessionId: string, path: string, savingVersion: number): void {
377
+ updateDocument(sessionId, path, (document) =>
378
+ document.savingVersion === savingVersion ? { ...document, savingVersion: undefined } : document,
379
+ );
380
+ }
381
+
382
+ export function completeAuthorSave(
383
+ sessionId: string,
384
+ path: string,
385
+ sourceSha256: string,
386
+ savedVersion: number,
387
+ savedFragments: AuthorDocumentInput['fragments'],
388
+ ): void {
389
+ updateDocument(sessionId, path, (document) => {
390
+ if (savedVersion < document.savedVersion) return document;
391
+ return {
392
+ ...document,
393
+ sourceSha256,
394
+ savedVersion,
395
+ savingVersion: document.savingVersion === savedVersion ? undefined : document.savingVersion,
396
+ originalFragments: savedFragments?.map((fragment) => ({ ...fragment })),
397
+ ...(document.version === savedVersion ? { crop: undefined } : {}),
398
+ revisions: document.revisions.filter((revision) => revision.revision > savedVersion),
399
+ };
400
+ });
401
+ const now = Date.now();
402
+ updateSession(sessionId, (session) => ({
403
+ ...session,
404
+ requests: boundHistory(
405
+ session.requests.map((request) =>
406
+ request.documentPath === normalizeAuthorPath(path) &&
407
+ request.status === 'CHANGED' &&
408
+ request.revision <= savedVersion
409
+ ? { ...request, status: 'COMPLETE', currentOperation: undefined, updatedAt: now }
410
+ : request,
411
+ ),
412
+ ),
413
+ }));
414
+ }
415
+
416
+ export function dropAuthorSession(sessionId: string): void {
417
+ const prefix = `${sessionId}\n`;
418
+ const session = authorWorkspace.store.state.sessions[sessionId];
419
+ if (session?.candidate?.thumbnailUrl !== undefined) revokeThumbnail(session.candidate.thumbnailUrl);
420
+ session?.regions.forEach((region) => {
421
+ if (region.thumbnailUrl !== undefined) revokeThumbnail(region.thumbnailUrl);
422
+ });
423
+ authorWorkspace.update((state) => {
424
+ const sessions = { ...state.sessions };
425
+ delete sessions[sessionId];
426
+ return {
427
+ documents: Object.fromEntries(Object.entries(state.documents).filter(([key]) => !key.startsWith(prefix))),
428
+ sessions,
429
+ };
430
+ });
431
+ }
432
+
433
+ function updateDocument(
434
+ sessionId: string,
435
+ path: string,
436
+ update: (document: AuthorWorkspaceDocument) => AuthorWorkspaceDocument,
437
+ ): void {
438
+ const key = authorDocumentKey(sessionId, path);
439
+ const staleThumbnails: string[] = [];
440
+ authorWorkspace.update((state) => {
441
+ const document = state.documents[key];
442
+ if (document === undefined) return state;
443
+ const next = update(document);
444
+ const session = state.sessions[sessionId];
445
+ const focused = session?.focusedDocument;
446
+ const identityChanged =
447
+ focused?.path === document.path &&
448
+ (focused.revision !== next.version || focused.sourceSha256 !== next.sourceSha256);
449
+ if (identityChanged) {
450
+ staleThumbnails.push(
451
+ ...(session.candidate?.thumbnailUrl === undefined ? [] : [session.candidate.thumbnailUrl]),
452
+ ...session.regions.flatMap((region) => (region.thumbnailUrl === undefined ? [] : [region.thumbnailUrl])),
453
+ );
454
+ }
455
+ const sessions = identityChanged
456
+ ? {
457
+ ...state.sessions,
458
+ [sessionId]: {
459
+ ...session,
460
+ focusedDocument: { ...focused, revision: next.version, sourceSha256: next.sourceSha256 },
461
+ candidate: undefined,
462
+ regions: [],
463
+ },
464
+ }
465
+ : state.sessions;
466
+ return { documents: { ...state.documents, [key]: next }, sessions };
467
+ });
468
+ staleThumbnails.forEach(revokeThumbnail);
469
+ }
470
+
471
+ function updateSession(sessionId: string, update: (session: AuthorSessionWorkspace) => AuthorSessionWorkspace): void {
472
+ authorWorkspace.update((state) => {
473
+ const session = state.sessions[sessionId] ?? EMPTY_SESSION;
474
+ const next = update(session);
475
+ return next === session ? state : { ...state, sessions: { ...state.sessions, [sessionId]: next } };
476
+ });
477
+ }
478
+
479
+ function copyRegion(region: AuthorRegionDraft): AuthorRegionDraft {
480
+ const anchor = 'rect' in region.anchor ? { ...region.anchor, rect: { ...region.anchor.rect } } : { ...region.anchor };
481
+ return {
482
+ ...region,
483
+ anchor,
484
+ viewport: { ...region.viewport },
485
+ voiceGrid: region.voiceGrid && { ...region.voiceGrid },
486
+ };
487
+ }
488
+
489
+ function boundedHistoryText(value: string | undefined): string | undefined {
490
+ if (value === undefined) return undefined;
491
+ const encoder = new TextEncoder();
492
+ if (encoder.encode(value).byteLength <= 16 * 1024) return value;
493
+ let end = value.length;
494
+ while (end > 0 && encoder.encode(value.slice(0, end)).byteLength > 16 * 1024) end -= 256;
495
+ return value.slice(0, end);
496
+ }
497
+
498
+ function copyRequest(record: AuthorRequestRecord): AuthorRequestRecord {
499
+ return {
500
+ ...record,
501
+ before: boundedHistoryText(record.before),
502
+ after: boundedHistoryText(record.after),
503
+ regions: record.regions.map(copyRegion),
504
+ pendingRegions: record.pendingRegions?.map(copyRegion),
505
+ };
506
+ }
507
+
508
+ function boundHistory(records: readonly AuthorRequestRecord[]): readonly AuthorRequestRecord[] {
509
+ const next = [...records];
510
+ const bytes = (): number => new TextEncoder().encode(JSON.stringify(next)).byteLength;
511
+ while (next.length > AUTHOR_HISTORY_RECORD_LIMIT || bytes() > AUTHOR_HISTORY_BYTE_LIMIT) {
512
+ const terminal = next.findIndex((record) => TERMINAL_REQUEST_STATUSES.has(record.status));
513
+ if (terminal < 0) throw new Error('Author request history is full while active requests are still running.');
514
+ next.splice(terminal, 1);
515
+ }
516
+ return next;
517
+ }
518
+
519
+ function revokeThumbnail(url: string): void {
520
+ if (!url.startsWith('blob:')) return;
521
+ try {
522
+ URL.revokeObjectURL(url);
523
+ } catch {
524
+ // Thumbnail cleanup must not make session teardown fail.
525
+ }
526
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noEmit": true,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true,
17
+ "skipLibCheck": true,
18
+ "allowImportingTsExtensions": true,
19
+ "forceConsistentCasingInFileNames": true
20
+ },
21
+ "include": [".", "../types", "../extensions/web.ts", "../../tests/web"]
22
+ }