@lincyaw/workbench 0.1.0 → 0.2.0-rc.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.
- package/README.md +40 -8
- package/package.json +27 -22
- package/src/MediaClipEditor.tsx +122 -0
- package/src/StepProductionEditor.tsx +130 -0
- package/src/approvals.tsx +60 -0
- package/src/assistant-mascot.ts +62 -0
- package/src/audio-recording-messages.ts +44 -0
- package/src/audio-recording.ts +192 -0
- package/src/audio-wav.ts +25 -0
- package/src/audio-waveform.ts +110 -0
- package/src/audition.ts +84 -0
- package/src/authoring-client.ts +136 -0
- package/src/authoring-draft-fragment.ts +28 -0
- package/src/base-controls.tsx +47 -0
- package/src/caption-timing-drafts.ts +61 -0
- package/src/caption-timing-messages.ts +14 -0
- package/src/caption-timing.css +20 -0
- package/src/caption-timing.ts +14 -0
- package/src/component-draft-actions.ts +54 -0
- package/src/component-draft-inspection.ts +35 -0
- package/src/component-properties.css +24 -0
- package/src/component-properties.ts +45 -0
- package/src/component-property-drafts.ts +273 -0
- package/src/component-property-messages.ts +31 -0
- package/src/components.tsx +547 -90
- package/src/context.tsx +27 -0
- package/src/diagnostics.ts +113 -0
- package/src/document-draft-store.ts +179 -0
- package/src/document-drafts.ts +178 -0
- package/src/document-editor/BlockNoteSurface.tsx +209 -0
- package/src/document-editor/ComponentMaterials.tsx +62 -0
- package/src/document-editor/ComponentPropertiesDialog.tsx +282 -0
- package/src/document-editor/DocumentBlockFallback.tsx +31 -0
- package/src/document-editor/DocumentBlockPreview.tsx +239 -0
- package/src/document-editor/DocumentComments.tsx +282 -0
- package/src/document-editor/DocumentEditor.tsx +192 -0
- package/src/document-editor/DocumentStepNarration.tsx +93 -0
- package/src/document-editor/NarrationEditor.tsx +110 -0
- package/src/document-editor/NewCommentDrafts.tsx +224 -0
- package/src/document-editor/authoring.tsx +259 -0
- package/src/document-editor/block-playback.ts +90 -0
- package/src/document-editor/block-preview-messages.ts +13 -0
- package/src/document-editor/blocknote-messages.ts +98 -0
- package/src/document-editor/clipboard.ts +40 -0
- package/src/document-editor/colors.ts +57 -0
- package/src/document-editor/comment-draft-actions.ts +58 -0
- package/src/document-editor/comment-draft-inspection.ts +38 -0
- package/src/document-editor/comment-drafts.ts +361 -0
- package/src/document-editor/comments.ts +410 -0
- package/src/document-editor/file-input.ts +71 -0
- package/src/document-editor/focus.ts +54 -0
- package/src/document-editor/inline.ts +104 -0
- package/src/document-editor/math.tsx +63 -0
- package/src/document-editor/messages.ts +14 -0
- package/src/document-editor/narration-schema.tsx +85 -0
- package/src/document-editor/narration.ts +37 -0
- package/src/document-editor/popover.tsx +107 -0
- package/src/document-editor/projection.ts +423 -0
- package/src/document-editor/rich-text.ts +152 -0
- package/src/document-editor/schema.tsx +117 -0
- package/src/document-editor/selection.ts +152 -0
- package/src/document-editor/style.css +9 -0
- package/src/document-editor/styles.d.ts +1 -0
- package/src/document-editor/table-identities.ts +67 -0
- package/src/document-preview.ts +112 -0
- package/src/document-return-point.ts +59 -0
- package/src/document-session.ts +678 -0
- package/src/document-timing-drafts.ts +61 -0
- package/src/document-voices.ts +2 -0
- package/src/history.ts +167 -0
- package/src/index.ts +33 -43
- package/src/jobs.ts +111 -0
- package/src/mascot-preference.ts +37 -0
- package/src/media-drafts.ts +83 -0
- package/src/media-messages.ts +53 -0
- package/src/media-time.ts +12 -0
- package/src/messages.ts +751 -203
- package/src/outline.ts +97 -0
- package/src/panels/ChangeTrail.tsx +79 -0
- package/src/panels/DocumentNarrationPanel.tsx +27 -0
- package/src/panels/DocumentView.tsx +456 -44
- package/src/panels/DocumentVoiceDialog.tsx +179 -0
- package/src/panels/DocumentVoiceField.tsx +35 -0
- package/src/panels/ExportPanel.tsx +133 -0
- package/src/panels/HistoryPanel.tsx +88 -0
- package/src/panels/HomePanel.tsx +184 -0
- package/src/panels/PageDeleteDialog.tsx +33 -0
- package/src/panels/PageRenameDialog.tsx +27 -0
- package/src/panels/PreviewPanel.tsx +79 -0
- package/src/panels/ProductionCaptionTiming.tsx +122 -0
- package/src/panels/ProductionMediaClips.tsx +156 -0
- package/src/panels/ProductionPanel.tsx +272 -0
- package/src/panels/ProductionRecording.tsx +107 -0
- package/src/panels/ProductionRecordingRange.tsx +118 -0
- package/src/panels/ProductionTimeline.tsx +62 -0
- package/src/panels/ProductionTranscription.tsx +140 -0
- package/src/panels/SourceDocumentPanel.tsx +215 -0
- package/src/panels/SpeechSettingsPanel.tsx +64 -0
- package/src/panels/TranscriptionAdoption.tsx +147 -0
- package/src/panels/VoiceCloneDialog.tsx +152 -0
- package/src/panels/VoicePreviewDialog.tsx +74 -0
- package/src/panels/VoiceScopeField.tsx +30 -0
- package/src/panels/VoiceoverDialog.tsx +216 -0
- package/src/places.ts +74 -6
- package/src/preview-playback.ts +125 -0
- package/src/preview-pool.ts +320 -0
- package/src/preview.ts +57 -6
- package/src/production-messages.ts +151 -0
- package/src/production-playback.ts +109 -0
- package/src/production-voice-playback.ts +114 -0
- package/src/recording-adoption.ts +33 -0
- package/src/schema-form/PmSchemaForm.tsx +187 -0
- package/src/schema-form/context.ts +26 -0
- package/src/schema-form/fields.tsx +59 -0
- package/src/schema-form/templates.tsx +50 -0
- package/src/schema-form/validator.ts +36 -0
- package/src/schema-form/widgets.tsx +60 -0
- package/src/shortcuts.ts +8 -0
- package/src/sortable-list.tsx +201 -0
- package/src/speech-settings-messages.ts +40 -0
- package/src/step-production.css +5 -0
- package/src/step-production.ts +51 -0
- package/src/styles.css +757 -361
- package/src/telemetry.ts +152 -0
- package/src/transcription-messages.ts +26 -0
- package/src/transcription-session.ts +159 -0
- package/src/use-recording-adoption.ts +84 -0
- package/src/voice-clone-messages.ts +92 -0
- package/src/voice-drafts.ts +89 -0
- package/src/voice-messages.ts +146 -0
- package/src/voice-snapshot.ts +55 -0
- package/src/voiceover-jobs.ts +87 -0
- package/src/workbench.ts +2328 -0
- package/src/workspace-client.ts +480 -0
- package/src/api.ts +0 -126
- package/src/editor.ts +0 -223
- package/src/merge.ts +0 -30
- package/src/panels/Changes.tsx +0 -128
- package/src/panels/Files.tsx +0 -73
- package/src/panels/Home.tsx +0 -224
- package/src/panels/Preview.tsx +0 -68
- package/src/panels/Problems.tsx +0 -42
- package/src/panels/Source.tsx +0 -118
- package/src/panels/Status.tsx +0 -26
- package/src/react.tsx +0 -59
- package/src/service.ts +0 -82
- package/src/store.ts +0 -599
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import type { ProjectOperation, ProjectPlan, ProjectPlanRequest, ProjectApply, ProjectApplied } from '@lincyaw/kernel'
|
|
2
|
+
import type { PreviewEventSubscription } from '@lincyaw/kernel'
|
|
3
|
+
import type { SpeechSettingsAnswer, SpeechSettingsChange } from '@lincyaw/kernel'
|
|
4
|
+
import type { AuthoringDiagnostics } from '@lincyaw/kernel'
|
|
5
|
+
import type { CompileJob, DiagnosticSnapshot, DocumentOperation, DocumentsAnswer, ErrorAnswer, FocusAnswer, FocusReport, IrAnswer, IrEvent, JobSource, JobsAnswer, OperationAnswer, PreviewAnswer, PreviewDiagnostics, PreviewRenderer, RevertResult, ReviewEnd, ReviewSnapshot, ReviewUpdate, RevisionsAnswer, SavedAnswer, SourceAnswer, SourceTransactionAnswer, SourceTransactionChange, StartJobRequest, UncommittedFile, WorkbenchAnswer } from '@lincyaw/kernel'
|
|
6
|
+
import type { ApprovalRequest } from '@lincyaw/kernel'
|
|
7
|
+
import type { OperationBatch } from './telemetry.ts'
|
|
8
|
+
import type { VoiceCatalogAnswer, VoiceCloneStatus, VoiceoverCancellation, VoiceoverJob, VoiceoverJobsAnswer, VoiceoverRequest, VoiceoverSnapshot, VoicePreviewAnswer, VoicePreviewQuery, VoicePreviewQuote, VoicePreviewRequest } from '@lincyaw/kernel'
|
|
9
|
+
import type { PageDeleteAnswer, PageDeletePlan, PageDeleteRequest } from '@lincyaw/kernel'
|
|
10
|
+
import type { DocumentAnswer, DocumentChange, DocumentChangeAnswer, DocumentEvent } from '@lincyaw/kernel'
|
|
11
|
+
import type { AppliedDocumentAuthoringAction, AuthoringCapabilities, DocumentAuthoringActionInspection, DocumentAuthoringActionPlan, DocumentAuthoringActionRead, DocumentAuthoringActionReference, DocumentAuthoringActionRequest } from '@lincyaw/kernel'
|
|
12
|
+
import type { DocumentRecordingAdoption, VerifiedDocumentRecordingAdoption, DocumentAudioImport, DocumentAudioImportAnswer, DocumentAudioInspectionAnswer, DocumentMediaClipChange } from '@lincyaw/kernel'
|
|
13
|
+
import type { DocumentImageImport, DocumentImageImportAnswer } from '@lincyaw/kernel'
|
|
14
|
+
import type { ProductionAssetAnswer, ProductionAssetQuery, ProductionMediaQuery, ProductionPlan, ProductionPlanQuery } from '@lincyaw/kernel'
|
|
15
|
+
import type { DocumentTranscriptionRequest, TranscriptionCapabilities, TranscriptionTask, TranscriptionListAnswer, TranscriptionRevision, TranscriptionCancellation, TranscriptionAdoptionDraft, TranscriptionAdoptionPlan, TranscriptionAdoptionApply, TranscriptionAdoptionAnswer } from '@lincyaw/kernel'
|
|
16
|
+
|
|
17
|
+
/** A change of the host's pending approvals that this author may answer. */
|
|
18
|
+
export type ApprovalEvent =
|
|
19
|
+
| { type: 'approval', id: string, request: ApprovalRequest }
|
|
20
|
+
| { type: 'settled', id: string }
|
|
21
|
+
/** The connection (re)opened: the pending approvals follow, so earlier ones are no longer known. */
|
|
22
|
+
| { type: 'reset' }
|
|
23
|
+
|
|
24
|
+
const observedFailures = new WeakSet<object>()
|
|
25
|
+
// A 503 with Retry-After is work still in progress (a preview being prepared), not a failure of the service.
|
|
26
|
+
const isRetryLater = (response: Response) => response.status === 503 && response.headers.has('Retry-After')
|
|
27
|
+
/** Whether the owned request observer already received this transport failure. */
|
|
28
|
+
export const isObservedWorkspaceFailure = (error: unknown): boolean => typeof error === 'object' && error !== null && observedFailures.has(error)
|
|
29
|
+
|
|
30
|
+
/** A route of the workspace server answered with an error. */
|
|
31
|
+
/** A cloned voice to train: the sample and the consent statement are WAV recordings of the same speaker. */
|
|
32
|
+
export interface VoiceCloneForm { engine?: string, voice: string, lang?: string, speaker: string, scope: string, sample: Blob, statement: Blob }
|
|
33
|
+
|
|
34
|
+
export class WorkspaceServerError extends Error {
|
|
35
|
+
readonly status: number
|
|
36
|
+
readonly answer: ErrorAnswer
|
|
37
|
+
constructor(status: number, answer: ErrorAnswer) {
|
|
38
|
+
super(answer.error ?? answer.detail ?? answer.title ?? `HTTP ${status}`)
|
|
39
|
+
this.status = status
|
|
40
|
+
this.answer = answer
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Only transport metadata is observed; request content, headers and query values remain private. */
|
|
45
|
+
export interface WorkspaceRequestEvent {
|
|
46
|
+
kind: 'start' | 'failure' | 'recovery' | 'cancel'
|
|
47
|
+
requestId: number
|
|
48
|
+
path: string
|
|
49
|
+
method: string
|
|
50
|
+
document?: string
|
|
51
|
+
status?: number
|
|
52
|
+
/** The caller renders HTTP failures locally; transport failures remain global. */
|
|
53
|
+
feedbackOwner?: 'inline-preview'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The workbench's client of the workspace server's routes; each `subscribe*` returns its unsubscribe. */
|
|
57
|
+
export interface WorkspaceCapabilities {
|
|
58
|
+
/** Omitted capabilities retain the local workspace's complete behavior. */
|
|
59
|
+
diagnostics?: 'none' | 'full'
|
|
60
|
+
production?: boolean
|
|
61
|
+
narration?: boolean
|
|
62
|
+
export?: boolean
|
|
63
|
+
readOnly?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WorkspaceClient {
|
|
67
|
+
readSpeechSettings?(signal?: AbortSignal): Promise<SpeechSettingsAnswer>
|
|
68
|
+
changeSpeechSettings?(change: SpeechSettingsChange, signal?: AbortSignal): Promise<SpeechSettingsAnswer>
|
|
69
|
+
openSpeechSettings?(): boolean
|
|
70
|
+
capabilities?: Readonly<WorkspaceCapabilities>
|
|
71
|
+
subscribePreviewEvents?: PreviewEventSubscription
|
|
72
|
+
openAuthoringClient?: AuthoringClientSubscription
|
|
73
|
+
subscribeRequests?(listener: (event: WorkspaceRequestEvent) => void): () => void
|
|
74
|
+
readDocuments(document?: string): Promise<DocumentsAnswer>
|
|
75
|
+
planProjectChange(request: ProjectPlanRequest): Promise<ProjectPlan>
|
|
76
|
+
operateDocument(operation: ProjectOperation, planned?: ProjectPlan): Promise<OperationAnswer>
|
|
77
|
+
readSource(document: string, path: string): Promise<SourceAnswer>
|
|
78
|
+
/** Saves the file; in a git workspace the save is committed at once, `message` its summary. */
|
|
79
|
+
saveSource(document: string, path: string, content: string, base: string, message?: string): Promise<SavedAnswer>
|
|
80
|
+
/** Saves a small set of files in one document with one version and rollback boundary. */
|
|
81
|
+
saveSourceTransaction(document: string, changes: SourceTransactionChange[], message?: string): Promise<SourceTransactionAnswer>
|
|
82
|
+
readIr(document: string): Promise<IrAnswer>
|
|
83
|
+
readDocument(document: string): Promise<DocumentAnswer>
|
|
84
|
+
readAuthoringCapabilities(document: string, signal?: AbortSignal): Promise<AuthoringCapabilities>
|
|
85
|
+
planDocumentAction(document: string, request: DocumentAuthoringActionRequest, signal?: AbortSignal): Promise<DocumentAuthoringActionPlan>
|
|
86
|
+
readDocumentActionPlan(document: string, reference: DocumentAuthoringActionRead, signal?: AbortSignal): Promise<DocumentAuthoringActionInspection>
|
|
87
|
+
applyDocumentAction(document: string, reference: DocumentAuthoringActionReference, signal?: AbortSignal): Promise<AppliedDocumentAuthoringAction>
|
|
88
|
+
/** A same-origin display URL; the canonical asset location remains untouched. */
|
|
89
|
+
documentAssetURL(document: string, assetId: string): string
|
|
90
|
+
changeDocument(document: string, change: DocumentChange): Promise<DocumentChangeAnswer>
|
|
91
|
+
importDocumentImage(document: string, file: File, change: DocumentImageImport, signal?: AbortSignal): Promise<DocumentImageImportAnswer>
|
|
92
|
+
readDocumentAudio(document: string, assetId: string, baseDocumentVersion: string, signal?: AbortSignal): Promise<DocumentAudioInspectionAnswer>
|
|
93
|
+
importDocumentAudio(document: string, file: File, change: DocumentAudioImport, signal?: AbortSignal): Promise<DocumentAudioImportAnswer>
|
|
94
|
+
adoptRecording(document: string, request: DocumentRecordingAdoption, signal?: AbortSignal): Promise<VerifiedDocumentRecordingAdoption>
|
|
95
|
+
changeDocumentMediaClip(document: string, change: DocumentMediaClipChange, signal?: AbortSignal): Promise<DocumentChangeAnswer>
|
|
96
|
+
readTranscriptionCapabilities(document: string, signal?: AbortSignal): Promise<TranscriptionCapabilities>
|
|
97
|
+
readTranscriptions(document: string, signal?: AbortSignal): Promise<TranscriptionListAnswer>
|
|
98
|
+
startTranscription(document: string, request: DocumentTranscriptionRequest, signal?: AbortSignal): Promise<TranscriptionTask>
|
|
99
|
+
readTranscription(document: string, query: { id: string } | { intent: string }, signal?: AbortSignal): Promise<TranscriptionTask>
|
|
100
|
+
readTranscriptionResult(document: string, id: string, revision: string, signal?: AbortSignal): Promise<TranscriptionRevision>
|
|
101
|
+
cancelTranscription(document: string, query: { id: string } | { intent: string }, signal?: AbortSignal): Promise<TranscriptionCancellation>
|
|
102
|
+
planTranscriptionAdoption(document: string, draft: TranscriptionAdoptionDraft, signal?: AbortSignal): Promise<TranscriptionAdoptionPlan>
|
|
103
|
+
applyTranscriptionAdoption(document: string, request: TranscriptionAdoptionApply, signal?: AbortSignal): Promise<TranscriptionAdoptionAnswer>
|
|
104
|
+
subscribeDocument(document: string, changed: (event: DocumentEvent) => void, failed?: () => void): () => void
|
|
105
|
+
readPageDeletion(document: string, section: string): Promise<PageDeletePlan>
|
|
106
|
+
deletePage(document: string, request: PageDeleteRequest): Promise<PageDeleteAnswer>
|
|
107
|
+
readVoiceCatalog(document: string, engine?: string, lang?: string, signal?: AbortSignal): Promise<VoiceCatalogAnswer>
|
|
108
|
+
/** Registers the speaker's consent and starts training a cloned voice: the owner's, and paid for at the provider. */
|
|
109
|
+
cloneVoice(document: string, request: VoiceCloneForm, signal?: AbortSignal): Promise<VoiceCloneStatus>
|
|
110
|
+
readCloneStatus(document: string, voice: string, engine?: string, signal?: AbortSignal): Promise<VoiceCloneStatus>
|
|
111
|
+
readVoiceover(document: string, signal?: AbortSignal): Promise<VoiceoverSnapshot>
|
|
112
|
+
readVoiceoverJobs(document: string, signal?: AbortSignal): Promise<VoiceoverJobsAnswer>
|
|
113
|
+
readProductionPlan(document: string, query: ProductionPlanQuery, signal?: AbortSignal): Promise<ProductionPlan>
|
|
114
|
+
readProductionAsset(document: string, query: ProductionAssetQuery, signal?: AbortSignal): Promise<ProductionAssetAnswer>
|
|
115
|
+
readProductionMedia(document: string, query: ProductionMediaQuery, signal?: AbortSignal): Promise<Blob>
|
|
116
|
+
/** Allocates an intent before sending; aborted or undelivered requests cancel by intent even without a job id. */
|
|
117
|
+
startVoiceover(document: string, request: Omit<VoiceoverRequest, 'intent'> & { intent?: string }, signal?: AbortSignal): Promise<VoiceoverJob>
|
|
118
|
+
cancelVoiceover(document: string, id: string, signal?: AbortSignal): Promise<VoiceoverJob>
|
|
119
|
+
cancelVoiceoverIntent(document: string, intent: string, signal?: AbortSignal): Promise<VoiceoverCancellation>
|
|
120
|
+
previewVoice(document: string, request: VoicePreviewRequest, signal?: AbortSignal): Promise<VoicePreviewAnswer>
|
|
121
|
+
readVoicePreview(document: string, query: VoicePreviewQuery, signal?: AbortSignal): Promise<VoicePreviewQuote>
|
|
122
|
+
subscribeIr(document: string, revision: (event: IrEvent) => void, failed?: () => void): () => void
|
|
123
|
+
readDiagnostics(document: string): Promise<DiagnosticSnapshot>
|
|
124
|
+
subscribeDiagnostics(document: string, snapshot: (event: DiagnosticSnapshot) => void, failed: () => void): () => void
|
|
125
|
+
reportDiagnostics(report: PreviewDiagnostics): Promise<unknown>
|
|
126
|
+
reportAuthoringDiagnostics(report: AuthoringDiagnostics): Promise<unknown>
|
|
127
|
+
readPreview(document: string, renderer?: PreviewRenderer, signal?: AbortSignal, feedbackOwner?: WorkspaceRequestEvent['feedbackOwner']): Promise<PreviewAnswer>
|
|
128
|
+
/** Asks the service to prepare a form of the document ahead of the author, when the machine has room. */
|
|
129
|
+
prefetchPreview(document: string, renderer: PreviewRenderer): Promise<PreviewAnswer>
|
|
130
|
+
/** The MCP address and the workspace root, read once. */
|
|
131
|
+
readWorkbenchInfo(): Promise<WorkbenchAnswer>
|
|
132
|
+
readFocus(document: string): Promise<FocusAnswer>
|
|
133
|
+
writeFocus(document: string, focus: FocusReport[]): Promise<FocusAnswer>
|
|
134
|
+
subscribeReview(document: string, update: (event: ReviewUpdate) => void): () => void
|
|
135
|
+
acceptReview(document: string, body: { reject?: string[], message?: string }): Promise<{ ended: ReviewEnd }>
|
|
136
|
+
rejectReview(document: string, paths?: string[]): Promise<ReviewUpdate>
|
|
137
|
+
restoreReview(document: string, step: string): Promise<{ review: ReviewSnapshot }>
|
|
138
|
+
readChanges(document: string): Promise<{ files: UncommittedFile[] }>
|
|
139
|
+
commitChanges(document: string, message: string, paths?: string[]): Promise<unknown>
|
|
140
|
+
discardChanges(document: string, paths?: string[]): Promise<unknown>
|
|
141
|
+
readRevisions(document: string, query?: import('@lincyaw/kernel').DocumentHistoryQuery): Promise<RevisionsAnswer>
|
|
142
|
+
readHistoryFile(document: string, query: import('@lincyaw/kernel').DocumentHistoryRead): Promise<import('@lincyaw/kernel').DocumentHistoryFile>
|
|
143
|
+
revertRevision(document: string, request: import('@lincyaw/kernel').DocumentHistoryChange): Promise<import('@lincyaw/kernel').DocumentHistoryChanged>
|
|
144
|
+
/** Takes back the document's latest change; `expect` is the `undo` of the history read, refused when it is not the latest any more. */
|
|
145
|
+
undo(document: string, request: import('@lincyaw/kernel').DocumentHistoryChange): Promise<import('@lincyaw/kernel').DocumentHistoryChanged>
|
|
146
|
+
/** Puts back the change the latest undo took back. */
|
|
147
|
+
redo(document: string, request: import('@lincyaw/kernel').DocumentHistoryChange): Promise<import('@lincyaw/kernel').DocumentHistoryChanged>
|
|
148
|
+
/** What the document can be made into, and its builds and exports. */
|
|
149
|
+
readJobs(document: string): Promise<JobsAnswer>
|
|
150
|
+
/** Starts a build or an export of the document (or answers the one already made from this revision). */
|
|
151
|
+
startJob(document: string, target: string, source: JobSource): Promise<CompileJob>
|
|
152
|
+
cancelJob(document: string, id: string): Promise<CompileJob>
|
|
153
|
+
/** Each change of the document's jobs, as it happens. */
|
|
154
|
+
subscribeJobs(document: string, changed: (job: CompileJob) => void, failed?: () => void, ready?: () => void): () => void
|
|
155
|
+
/** The host's approvals of agent operations; the workbench hears them without being a viewer window. */
|
|
156
|
+
subscribeApprovals(changed: (event: ApprovalEvent) => void, failed?: () => void): () => void
|
|
157
|
+
/** Answers one approval; a 404 means it already ended (answered elsewhere or timed out). */
|
|
158
|
+
answerApproval(id: string, allow: boolean): Promise<void>
|
|
159
|
+
/** Whether the host collects the workbench's operations for its operator. */
|
|
160
|
+
readTelemetry(): Promise<{ enabled: boolean }>
|
|
161
|
+
/** Sends a batch of operations; `beacon` hands it to the browser to deliver while the page goes away. */
|
|
162
|
+
sendTelemetry(batch: OperationBatch, beacon?: boolean): Promise<void>
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A host may carry these event subscriptions over its authenticated shared connection. */
|
|
166
|
+
export interface AuthoringClientChannel {
|
|
167
|
+
publish(connectionId: string, report: import('@lincyaw/kernel').AuthoringClientReport): Promise<void>
|
|
168
|
+
acknowledge(connectionId: string, report: import('@lincyaw/kernel').AuthoringClientAcknowledgement): Promise<void>
|
|
169
|
+
close(): void
|
|
170
|
+
}
|
|
171
|
+
export type AuthoringClientSubscription = (document: string, open: import('@lincyaw/kernel').AuthoringClientOpen, received: (event: import('@lincyaw/kernel').AuthoringClientEvent) => void, failed: () => void) => AuthoringClientChannel
|
|
172
|
+
export interface WorkspaceSubscription {
|
|
173
|
+
(address: string, event: string, received: (data: string, event?: string) => void, failed?: () => void, ready?: () => void): () => void
|
|
174
|
+
openAuthoringClient?: AuthoringClientSubscription
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** A remote host may add its CSRF header without replacing the page's global fetch. */
|
|
178
|
+
export interface WorkspaceTransport { fetch?: typeof globalThis.fetch }
|
|
179
|
+
|
|
180
|
+
export function workspaceClient(base = 'api/presmium/', subscription?: WorkspaceSubscription, transport: WorkspaceTransport = {}): WorkspaceClient {
|
|
181
|
+
let client: WorkspaceClient
|
|
182
|
+
const fetch = transport.fetch ?? globalThis.fetch.bind(globalThis)
|
|
183
|
+
const requestListeners = new Set<(event: WorkspaceRequestEvent) => void>()
|
|
184
|
+
let requestSequence = 0
|
|
185
|
+
const observedFetch = async (address: string, init?: RequestInit, feedbackOwner?: WorkspaceRequestEvent['feedbackOwner']): Promise<Response> => {
|
|
186
|
+
const target = new URL(address)
|
|
187
|
+
const prefix = new URL(base, globalThis.document.baseURI).pathname
|
|
188
|
+
const path = target.pathname.slice(prefix.length)
|
|
189
|
+
const method = (init?.method ?? 'GET').toUpperCase()
|
|
190
|
+
if (client.capabilities?.readOnly && !['GET', 'HEAD', 'OPTIONS'].includes(method)
|
|
191
|
+
&& /^(?:document(?:\/|$)|source(?:\/|$)|project\/|pages\/|history\/|revisions\/|review(?:\/|$))/.test(path)) throw new WorkspaceServerError(403, { error: 'permission-denied' })
|
|
192
|
+
if (path === 'telemetry' || (path === 'diagnostics' || path === 'diagnostics/authoring') && method === 'POST') return fetch(address, { credentials: 'same-origin', ...init })
|
|
193
|
+
const document = target.searchParams.get('document') ?? undefined
|
|
194
|
+
const requestId = ++requestSequence
|
|
195
|
+
const emit = (kind: WorkspaceRequestEvent['kind'], status?: number) => {
|
|
196
|
+
const event: WorkspaceRequestEvent = { kind, requestId, path, method, ...document !== undefined ? { document } : {}, ...status !== undefined ? { status } : {}, ...feedbackOwner ? { feedbackOwner } : {} }
|
|
197
|
+
for (const listener of requestListeners) {
|
|
198
|
+
const notify = () => {
|
|
199
|
+
if (!requestListeners.has(listener)) return
|
|
200
|
+
// Observers must not alter the request's return value or cancellation.
|
|
201
|
+
try { listener(event) } catch (error) { queueMicrotask(() => { throw error }) }
|
|
202
|
+
}
|
|
203
|
+
// Workspace-scoped source requests need the active document at invocation.
|
|
204
|
+
if (kind === 'start') notify()
|
|
205
|
+
else queueMicrotask(notify)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
emit('start')
|
|
209
|
+
try {
|
|
210
|
+
const response = await fetch(address, { credentials: 'same-origin', ...init })
|
|
211
|
+
if (response.status >= 500 && !isRetryLater(response)) emit('failure', response.status)
|
|
212
|
+
else emit('recovery', response.status)
|
|
213
|
+
return response
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const timedOut = error instanceof Error && error.name === 'TimeoutError' || init?.signal?.reason instanceof Error && init.signal.reason.name === 'TimeoutError'
|
|
216
|
+
const cancelled = !timedOut && (init?.signal?.aborted || error instanceof Error && error.name === 'AbortError')
|
|
217
|
+
if (!cancelled) {
|
|
218
|
+
if (typeof error === 'object' && error !== null) observedFailures.add(error)
|
|
219
|
+
emit('failure')
|
|
220
|
+
} else emit('cancel')
|
|
221
|
+
throw error
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const irCacheLimit = 16 << 20
|
|
225
|
+
type CachedIR = { etag: string, answer: IrAnswer, bytes: number }
|
|
226
|
+
const irCache = new Map<string, CachedIR>()
|
|
227
|
+
const irPending = new Map<string, Promise<IrAnswer>>()
|
|
228
|
+
let irBytes = 0
|
|
229
|
+
let workbenchInfo: Promise<WorkbenchAnswer> | undefined
|
|
230
|
+
const url = (path: string, document?: string, query: Record<string, string> = {}) => {
|
|
231
|
+
const address = new URL(`${base}${path}`, globalThis.document.baseURI)
|
|
232
|
+
if (document !== undefined) address.searchParams.set('document', document)
|
|
233
|
+
for (const [name, value] of Object.entries(query)) address.searchParams.set(name, value)
|
|
234
|
+
return address.href
|
|
235
|
+
}
|
|
236
|
+
const call = async <T>(address: string, init?: RequestInit, feedbackOwner?: WorkspaceRequestEvent['feedbackOwner']): Promise<T> => {
|
|
237
|
+
const response = await observedFetch(address, init, feedbackOwner)
|
|
238
|
+
const data = await response.json().catch(() => ({})) as T & ErrorAnswer
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
const error = new WorkspaceServerError(response.status, data)
|
|
241
|
+
if (response.status >= 500 && !isRetryLater(response)) observedFailures.add(error)
|
|
242
|
+
throw error
|
|
243
|
+
}
|
|
244
|
+
return data
|
|
245
|
+
}
|
|
246
|
+
const post = <T>(address: string, body: unknown, signal?: AbortSignal) => call<T>(address, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal })
|
|
247
|
+
const startVoiceover: WorkspaceClient['startVoiceover'] = async (document, request, signal) => {
|
|
248
|
+
signal?.throwIfAborted()
|
|
249
|
+
const intent = request.intent ?? Array.from(crypto.getRandomValues(new Uint32Array(4)), value => value.toString(16).padStart(8, '0')).join('')
|
|
250
|
+
let cancellation: Promise<VoiceoverCancellation> | undefined
|
|
251
|
+
const cancel = () => cancellation ??= post<VoiceoverCancellation>(url('voiceover/cancel', document), { intent }, AbortSignal.timeout(5000))
|
|
252
|
+
const aborted = () => { void cancel().catch(() => {}) }
|
|
253
|
+
signal?.addEventListener('abort', aborted, { once: true })
|
|
254
|
+
try {
|
|
255
|
+
const job = await post<VoiceoverJob>(url('voiceover', document), { ...request, intent }, signal)
|
|
256
|
+
signal?.throwIfAborted()
|
|
257
|
+
if (job.intent !== intent) throw new Error('voice-intent-changed')
|
|
258
|
+
return job
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// The server may have admitted a job before the response was lost.
|
|
261
|
+
// Cancellation uses a fresh signal; the owned observer reports transport failures.
|
|
262
|
+
if (signal?.aborted || !(error instanceof WorkspaceServerError) || error.status >= 500) await cancel().catch(() => {})
|
|
263
|
+
throw error
|
|
264
|
+
} finally { signal?.removeEventListener('abort', aborted) }
|
|
265
|
+
}
|
|
266
|
+
const rememberIR = (document: string, value?: CachedIR) => {
|
|
267
|
+
const old = irCache.get(document)
|
|
268
|
+
if (old) irBytes -= old.bytes
|
|
269
|
+
irCache.delete(document)
|
|
270
|
+
if (value) {
|
|
271
|
+
irCache.set(document, value)
|
|
272
|
+
irBytes += value.bytes
|
|
273
|
+
}
|
|
274
|
+
while (irCache.size > 2 || irBytes > irCacheLimit) {
|
|
275
|
+
const first = irCache.keys().next().value!
|
|
276
|
+
irBytes -= irCache.get(first)!.bytes
|
|
277
|
+
irCache.delete(first)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const fetchIR = async (document: string): Promise<IrAnswer> => {
|
|
281
|
+
const known = irCache.get(document)
|
|
282
|
+
const response = await observedFetch(url('ir', document), {
|
|
283
|
+
credentials: 'same-origin',
|
|
284
|
+
cache: 'no-store',
|
|
285
|
+
...known ? { headers: { 'If-None-Match': known.etag } } : {},
|
|
286
|
+
})
|
|
287
|
+
if (response.status === 304 && known) {
|
|
288
|
+
rememberIR(document, known)
|
|
289
|
+
return known.answer
|
|
290
|
+
}
|
|
291
|
+
const text = await response.text().catch(() => '')
|
|
292
|
+
let data: IrAnswer & ErrorAnswer
|
|
293
|
+
try {
|
|
294
|
+
data = JSON.parse(text) as IrAnswer & ErrorAnswer
|
|
295
|
+
} catch {
|
|
296
|
+
data = {} as IrAnswer & ErrorAnswer
|
|
297
|
+
}
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
const error = new WorkspaceServerError(response.status, data)
|
|
300
|
+
if (response.status >= 500) observedFailures.add(error)
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
const etag = response.headers.get('ETag')
|
|
304
|
+
// Account for UTF-16 response text; the parsed object's heap size is a soft limit.
|
|
305
|
+
const bytes = text.length * 2
|
|
306
|
+
rememberIR(document, response.status === 200 && etag && bytes <= irCacheLimit ? { etag, answer: data, bytes } : undefined)
|
|
307
|
+
return data
|
|
308
|
+
}
|
|
309
|
+
const readIr = (document: string): Promise<IrAnswer> => {
|
|
310
|
+
const pending = irPending.get(document)
|
|
311
|
+
if (pending) return pending
|
|
312
|
+
const request = fetchIR(document).finally(() => {
|
|
313
|
+
if (irPending.get(document) === request) irPending.delete(document)
|
|
314
|
+
})
|
|
315
|
+
irPending.set(document, request)
|
|
316
|
+
return request
|
|
317
|
+
}
|
|
318
|
+
const subscribe = <T>(address: string, name: string, on: (data: T) => void, failed?: () => void, ready?: () => void) => {
|
|
319
|
+
const received = (data: string) => {
|
|
320
|
+
try { on(JSON.parse(data) as T) } catch { failed?.() }
|
|
321
|
+
}
|
|
322
|
+
if (subscription) return subscription(address, name, received, failed, ready)
|
|
323
|
+
const source = new EventSource(address)
|
|
324
|
+
source.addEventListener(name, event => received((event as MessageEvent<string>).data))
|
|
325
|
+
if (failed) source.addEventListener('error', failed)
|
|
326
|
+
if (ready) source.addEventListener('open', ready)
|
|
327
|
+
return () => source.close()
|
|
328
|
+
}
|
|
329
|
+
const projectIntents = new Map<string, { id: string, plan?: ProjectPlan }>()
|
|
330
|
+
return client = {
|
|
331
|
+
...subscription?.openAuthoringClient ? { openAuthoringClient: subscription.openAuthoringClient } : {},
|
|
332
|
+
...subscription ? { subscribePreviewEvents: ((target, received) => {
|
|
333
|
+
const address = new URL(base, globalThis.document.baseURI)
|
|
334
|
+
for (const [key, value] of Object.entries(target)) if (key !== 'topic') address.searchParams.set(key, value)
|
|
335
|
+
return subscription(address.href, target.topic, (data, event) => received({ type: 'event', event: event ?? '', data }), () => received({ type: 'error' }), () => received({ type: 'open' }))
|
|
336
|
+
}) satisfies PreviewEventSubscription } : {},
|
|
337
|
+
subscribeRequests: listener => { requestListeners.add(listener); return () => { requestListeners.delete(listener) } },
|
|
338
|
+
readDocuments: document => call(url('documents', document)),
|
|
339
|
+
planProjectChange: request => post(url('project/plan'), request),
|
|
340
|
+
async operateDocument(operation, planned) {
|
|
341
|
+
if (operation.op === 'delete' && (!planned || JSON.stringify(planned.operation) !== JSON.stringify(operation))) throw new Error('project-deletion-plan-required')
|
|
342
|
+
const key = JSON.stringify(operation)
|
|
343
|
+
let intent = projectIntents.get(key)
|
|
344
|
+
if (!intent) { intent = { id: crypto.randomUUID() }; projectIntents.set(key, intent) }
|
|
345
|
+
while (projectIntents.size > 64) projectIntents.delete(projectIntents.keys().next().value!)
|
|
346
|
+
try {
|
|
347
|
+
intent.plan ??= planned
|
|
348
|
+
intent.plan ??= await post<ProjectPlan>(url('project/plan'), { id: intent.id, operation })
|
|
349
|
+
const answer = await post<OperationAnswer>(url('project/apply'), { planId: intent.plan.planId, digest: intent.plan.digest, ...operation.op === 'delete' ? { confirmDeletion: true } : {} })
|
|
350
|
+
projectIntents.delete(key)
|
|
351
|
+
return answer
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error instanceof WorkspaceServerError && error.status < 500) projectIntents.delete(key)
|
|
354
|
+
throw error
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
readSource: (document, path) => call(url('source', document, { path })),
|
|
358
|
+
saveSource: (document, path, content, base, message) => post(url('source', document), { path, content, base, ...message ? { message } : {} }),
|
|
359
|
+
saveSourceTransaction: (document, changes, message) => post(url('source/transaction', document), { changes, ...message ? { message } : {} }),
|
|
360
|
+
readIr,
|
|
361
|
+
readDocument: document => call(url('document', document)),
|
|
362
|
+
readAuthoringCapabilities: (document, signal) => call(url('document/capabilities', document), { signal }),
|
|
363
|
+
planDocumentAction: (document, request, signal) => post(url('document/action/plan', document), request, signal),
|
|
364
|
+
readDocumentActionPlan: (document, reference, signal) => call(url('document/action/plan', document, { planId: reference.planId, digest: reference.digest }), { signal }),
|
|
365
|
+
applyDocumentAction: (document, reference, signal) => post(url('document/action/apply', document), reference, signal),
|
|
366
|
+
documentAssetURL: (document, asset) => url('document/asset', document, { asset }),
|
|
367
|
+
changeDocument: (document, change) => post(url('document/change', document), change),
|
|
368
|
+
importDocumentImage: (document, file, change, signal) => {
|
|
369
|
+
const body = new FormData()
|
|
370
|
+
body.set('change', JSON.stringify(change)); body.set('file', file)
|
|
371
|
+
return call(url('document/image', document), { method: 'POST', body, signal })
|
|
372
|
+
},
|
|
373
|
+
readDocumentAudio: (document, assetId, baseDocumentVersion, signal) => call(url('document/audio', document, { assetId, baseDocumentVersion }), { signal }),
|
|
374
|
+
importDocumentAudio: (document, file, change, signal) => {
|
|
375
|
+
const body = new FormData()
|
|
376
|
+
body.set('change', JSON.stringify(change)); body.set('file', file)
|
|
377
|
+
return call(url('document/audio', document), { method: 'POST', body, signal })
|
|
378
|
+
},
|
|
379
|
+
adoptRecording: (document, request, signal) => post(url('document/recording/adopt', document), request, signal),
|
|
380
|
+
changeDocumentMediaClip: (document, change, signal) => post(url('document/media-clip', document), change, signal),
|
|
381
|
+
readTranscriptionCapabilities: (document, signal) => call(url('document/transcription/capabilities', document), { signal }),
|
|
382
|
+
readTranscriptions: (document, signal) => call(url('document/transcription', document), { signal }),
|
|
383
|
+
startTranscription: (document, request, signal) => post(url('document/transcription', document), request, signal),
|
|
384
|
+
readTranscription: (document, query, signal) => call(url('document/transcription', document, query), { signal }),
|
|
385
|
+
readTranscriptionResult: (document, id, revision, signal) => call(url('document/transcription/result', document, { id, revision }), { signal }),
|
|
386
|
+
cancelTranscription: (document, query, signal) => post(url('document/transcription/cancel', document), query, signal),
|
|
387
|
+
planTranscriptionAdoption: (document, draft, signal) => post(url('document/transcription/adoption/plan', document), draft, signal),
|
|
388
|
+
applyTranscriptionAdoption: (document, request, signal) => post(url('document/transcription/adoption/apply', document), request, signal),
|
|
389
|
+
subscribeDocument: (document, changed, failed) => subscribe(url('document/events', document), 'document', changed, failed),
|
|
390
|
+
readPageDeletion: (document, section) => call(url('pages/delete', document, { section })),
|
|
391
|
+
deletePage: (document, request) => post(url('pages/delete', document), request),
|
|
392
|
+
readSpeechSettings: signal => call(url('speech-settings'), { signal }),
|
|
393
|
+
changeSpeechSettings: (change, signal) => post(url('speech-settings'), change, signal),
|
|
394
|
+
readVoiceCatalog: (document, engine, lang, signal) => call(url('voice-catalog', document, { ...engine ? { engine } : {}, ...lang ? { lang } : {} }), { signal }),
|
|
395
|
+
cloneVoice: (document, request, signal) => {
|
|
396
|
+
const body = new FormData()
|
|
397
|
+
for (const [name, value] of [['engine', request.engine], ['voice', request.voice], ['lang', request.lang], ['speaker', request.speaker], ['scope', request.scope]] as const) if (value) body.set(name, value)
|
|
398
|
+
body.set('confirmed', '1')
|
|
399
|
+
body.set('sample', request.sample, 'sample.wav')
|
|
400
|
+
body.set('statement', request.statement, 'statement.wav')
|
|
401
|
+
return call(url('voice-clone', document), { method: 'POST', body, signal })
|
|
402
|
+
},
|
|
403
|
+
readCloneStatus: (document, voice, engine, signal) => call(url('voice-clone/status', document, { voice, ...engine ? { engine } : {} }), { signal }),
|
|
404
|
+
readVoiceover: (document, signal) => call(url('voiceover', document), { signal }),
|
|
405
|
+
readVoiceoverJobs: (document, signal) => call(url('voiceover/jobs', document), { signal }),
|
|
406
|
+
readProductionPlan: (document, query, signal) => call(url('production-plan', document, { source: JSON.stringify(query.source), irRevision: query.irRevision }), { signal }),
|
|
407
|
+
readProductionAsset: (document, query, signal) => call(url('production-asset', document, { source: JSON.stringify(query.source), irRevision: query.irRevision, assetId: query.assetId }), { signal }),
|
|
408
|
+
async readProductionMedia(document, query, signal) {
|
|
409
|
+
const response = await observedFetch(url('production-media', document, { source: JSON.stringify(query.source), irRevision: query.irRevision, planId: query.planId, clipId: query.clipId }), { signal })
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
const error = new WorkspaceServerError(response.status, await response.json().catch(() => ({})))
|
|
412
|
+
if (response.status >= 500) observedFailures.add(error)
|
|
413
|
+
throw error
|
|
414
|
+
}
|
|
415
|
+
return response.blob()
|
|
416
|
+
},
|
|
417
|
+
startVoiceover,
|
|
418
|
+
cancelVoiceover: (document, id, signal) => post(url('voiceover/cancel', document), { id }, signal),
|
|
419
|
+
cancelVoiceoverIntent: (document, intent, signal) => post(url('voiceover/cancel', document), { intent }, signal),
|
|
420
|
+
previewVoice: (document, request, signal) => post(url('voice-preview', document), request, signal),
|
|
421
|
+
readVoicePreview: (document, query, signal) => call(url('voice-preview', document, { revision: query.revision, voice: JSON.stringify(query.voice), mode: query.mode, ...query.target ? { target: JSON.stringify(query.target) } : {} }), { signal }),
|
|
422
|
+
subscribeIr: (document, revision, failed) => subscribe(url('ir/events', document), 'revision', revision, failed),
|
|
423
|
+
readDiagnostics: document => call(url('diagnostics', document)),
|
|
424
|
+
subscribeDiagnostics: (document, snapshot, failed) => subscribe(url('diagnostics/events', document), 'diagnostics', snapshot, failed),
|
|
425
|
+
reportDiagnostics: report => post(url('diagnostics'), report),
|
|
426
|
+
reportAuthoringDiagnostics: report => post(url('diagnostics/authoring'), report),
|
|
427
|
+
readPreview: (document, renderer, signal, feedbackOwner) => call(url('preview', document, renderer ? { view: renderer } : {}), { signal }, feedbackOwner),
|
|
428
|
+
prefetchPreview: (document, renderer) => call(url('preview', document, { view: renderer, prefetch: '1' })),
|
|
429
|
+
readWorkbenchInfo: () => {
|
|
430
|
+
workbenchInfo ??= call<WorkbenchAnswer>(url('workbench')).catch((error: unknown) => {
|
|
431
|
+
workbenchInfo = undefined
|
|
432
|
+
throw error
|
|
433
|
+
})
|
|
434
|
+
return workbenchInfo
|
|
435
|
+
},
|
|
436
|
+
readFocus: document => call(url('focus', document)),
|
|
437
|
+
writeFocus: (document, focus) => post(url('focus'), { document, focus }),
|
|
438
|
+
subscribeReview: (document, update) => subscribe(url('review', document), 'review', update),
|
|
439
|
+
acceptReview: (document, body) => post(url('review/accept', document), body),
|
|
440
|
+
rejectReview: (document, paths) => post(url('review/reject', document), paths ? { paths } : {}),
|
|
441
|
+
restoreReview: (document, step) => post(url('review/restore', document), { step }),
|
|
442
|
+
readChanges: document => call(url('changes', document)),
|
|
443
|
+
commitChanges: (document, message, paths) => post(url('changes/commit', document), { message, ...paths ? { paths } : {} }),
|
|
444
|
+
discardChanges: (document, paths) => post(url('changes/discard', document), paths ? { paths } : {}),
|
|
445
|
+
readJobs: document => call(url('jobs', document)),
|
|
446
|
+
startJob: (document, target, source) => post(url('jobs', document), { target, source } satisfies StartJobRequest),
|
|
447
|
+
cancelJob: (document, id) => post(url('jobs/cancel', document), { id }),
|
|
448
|
+
subscribeJobs: (document, changed, failed, ready) => subscribe(url('jobs/events', document), 'job', changed, failed, ready),
|
|
449
|
+
readRevisions: (document, query = {}) => call(url('revisions', document, { query: JSON.stringify(query) })),
|
|
450
|
+
readHistoryFile: (document, query) => call(url('history/file', document, { query: JSON.stringify(query) })),
|
|
451
|
+
revertRevision: (document, request) => post(url('revisions/revert', document), request),
|
|
452
|
+
undo: (document, request) => post(url('history/undo', document), request),
|
|
453
|
+
redo: (document, request) => post(url('history/redo', document), request),
|
|
454
|
+
subscribeApprovals(changed, failed) {
|
|
455
|
+
const address = url('control', '', { observe: 'approvals' })
|
|
456
|
+
const received = (data: string, event?: string) => {
|
|
457
|
+
let value: { id?: unknown, request?: unknown }
|
|
458
|
+
try { value = JSON.parse(data) } catch { return }
|
|
459
|
+
if (typeof value.id !== 'string') return
|
|
460
|
+
if (event === 'approval' && value.request && typeof value.request === 'object') changed({ type: 'approval', id: value.id, request: value.request as ApprovalRequest })
|
|
461
|
+
else if (event === 'settled') changed({ type: 'settled', id: value.id })
|
|
462
|
+
}
|
|
463
|
+
const lost = () => { changed({ type: 'reset' }); failed?.() }
|
|
464
|
+
if (subscription) return subscription(address, 'approvals', received, lost, () => changed({ type: 'reset' }))
|
|
465
|
+
const source = new EventSource(address)
|
|
466
|
+
for (const name of ['approval', 'settled']) source.addEventListener(name, event => received((event as MessageEvent<string>).data, name))
|
|
467
|
+
source.addEventListener('open', () => changed({ type: 'reset' }))
|
|
468
|
+
source.addEventListener('error', lost)
|
|
469
|
+
return () => source.close()
|
|
470
|
+
},
|
|
471
|
+
answerApproval: async (id, allow) => { await post(url('control'), { type: 'answer', id, allow }) },
|
|
472
|
+
readTelemetry: () => call(url('telemetry')),
|
|
473
|
+
sendTelemetry: async (batch, beacon) => {
|
|
474
|
+
const body = JSON.stringify(batch)
|
|
475
|
+
if (beacon && navigator.sendBeacon?.(url('telemetry'), new Blob([body], { type: 'application/json' }))) return
|
|
476
|
+
const response = await fetch(url('telemetry'), { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body, keepalive: Boolean(beacon) })
|
|
477
|
+
if (!response.ok) throw new Error(`the operations were not taken: HTTP ${response.status}`)
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
}
|
package/src/api.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type { Finding, Focus, IRDocument, ReviewChange, ReviewStep, Revision } from '@lincyaw/kernel'
|
|
2
|
-
|
|
3
|
-
export interface WorkspaceDocument {
|
|
4
|
-
dir: string
|
|
5
|
-
title: string
|
|
6
|
-
lang?: string
|
|
7
|
-
tags?: string[]
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface DocumentsAnswer {
|
|
11
|
-
title: string
|
|
12
|
-
documents: WorkspaceDocument[]
|
|
13
|
-
templates: string[]
|
|
14
|
-
git: boolean
|
|
15
|
-
branch?: string
|
|
16
|
-
uncommitted?: number
|
|
17
|
-
single: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type DocumentOperation =
|
|
21
|
-
| { op: 'create', dir: string, title?: string, template?: string }
|
|
22
|
-
| { op: 'rename', dir: string, to: string }
|
|
23
|
-
| { op: 'copy', dir: string, to: string }
|
|
24
|
-
| { op: 'delete', dir: string }
|
|
25
|
-
|
|
26
|
-
export interface OperationAnswer {
|
|
27
|
-
revision?: { id: string, summary?: string }
|
|
28
|
-
dir?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface SourceFile {
|
|
32
|
-
path: string
|
|
33
|
-
binary?: true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface FilesAnswer {
|
|
37
|
-
document: string
|
|
38
|
-
home: boolean
|
|
39
|
-
documents?: string[]
|
|
40
|
-
files: SourceFile[]
|
|
41
|
-
truncated?: true
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SourceAnswer {
|
|
45
|
-
path: string
|
|
46
|
-
content: string
|
|
47
|
-
version: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SavedAnswer {
|
|
51
|
-
version: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface IrAnswer {
|
|
55
|
-
document: string
|
|
56
|
-
revision: string
|
|
57
|
-
doc: IRDocument
|
|
58
|
-
diagnostics: Finding[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface IrEvent {
|
|
62
|
-
document: string
|
|
63
|
-
revision: string
|
|
64
|
-
diagnostics: { errors: number, warnings: number }
|
|
65
|
-
files: string[]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PreviewAnswer {
|
|
69
|
-
document: string
|
|
70
|
-
web: string
|
|
71
|
-
slides?: string
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface ErrorAnswer {
|
|
75
|
-
error?: string
|
|
76
|
-
detail?: string
|
|
77
|
-
title?: string
|
|
78
|
-
conflict?: { version: string, content: string }
|
|
79
|
-
locked?: { document: string, holder: string }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface WorkbenchAnswer {
|
|
83
|
-
mcp: string
|
|
84
|
-
root?: string
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface FocusAnswer {
|
|
88
|
-
document?: string
|
|
89
|
-
focus: Focus[] | null
|
|
90
|
-
reported?: string
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface ReviewView {
|
|
94
|
-
id: string
|
|
95
|
-
session: string
|
|
96
|
-
turns: { from: number, to: number }
|
|
97
|
-
running: boolean
|
|
98
|
-
mode: 'required' | 'auto'
|
|
99
|
-
steps: ReviewStep[]
|
|
100
|
-
changes: ReviewChange[]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface ReviewEnd {
|
|
104
|
-
outcome: 'accepted' | 'rejected' | 'rolled-back' | 'empty'
|
|
105
|
-
revision?: Revision
|
|
106
|
-
files: string[]
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export interface ReviewFollowed {
|
|
110
|
-
document: string
|
|
111
|
-
review?: ReviewView
|
|
112
|
-
ended?: ReviewEnd
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface UncommittedFile {
|
|
116
|
-
path: string
|
|
117
|
-
kind: 'added' | 'modified' | 'deleted'
|
|
118
|
-
added?: number
|
|
119
|
-
removed?: number
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface HistoryRevision extends Revision {
|
|
123
|
-
reverts?: string
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export type RevertAnswer = { ok: true, revision: Revision } | { ok: false, conflicts: string[] }
|