agent-canvas 0.2.1 → 0.6.0

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 (99) hide show
  1. package/README.md +42 -17
  2. package/bin/agent-canvas.js +7 -7
  3. package/bin/agent-canvas.ts +48 -37
  4. package/bin/lib/commands/daemon.ts +46 -43
  5. package/bin/lib/commands/exec.ts +100 -100
  6. package/bin/lib/commands/fetch.ts +15 -15
  7. package/bin/lib/commands/install.ts +36 -36
  8. package/bin/lib/commands/instructions.ts +51 -51
  9. package/bin/lib/commands/push.ts +115 -97
  10. package/bin/lib/commands/review.ts +190 -0
  11. package/bin/lib/commands/watch.ts +24 -24
  12. package/bin/lib/config.ts +12 -12
  13. package/bin/lib/daemon-lifecycle.ts +47 -51
  14. package/bin/lib/helpers.ts +82 -76
  15. package/bin/lib/instructions/app-review.md +88 -0
  16. package/bin/lib/instructions/component-checklist.md +7 -5
  17. package/bin/lib/instructions/component-custom.md +20 -14
  18. package/bin/lib/instructions/component-diff.md +4 -3
  19. package/bin/lib/instructions/component-item.md +18 -8
  20. package/bin/lib/instructions/component-markdown.md +4 -2
  21. package/bin/lib/instructions/component-mermaid.md +12 -6
  22. package/bin/lib/instructions/component-multichoice.md +6 -2
  23. package/bin/lib/instructions/component-note.md +1 -1
  24. package/bin/lib/instructions/component-priority.md +1 -1
  25. package/bin/lib/instructions/component-rangeinput.md +9 -2
  26. package/bin/lib/instructions/component-secretinput.md +12 -11
  27. package/bin/lib/instructions/component-svg.md +62 -16
  28. package/bin/lib/instructions/component-table.md +6 -6
  29. package/bin/lib/instructions/component-usefeedback.md +8 -8
  30. package/bin/lib/instructions/component-userinput.md +6 -2
  31. package/bin/lib/instructions/flow-feature.md +22 -5
  32. package/bin/lib/instructions/flow-plan.md +5 -5
  33. package/bin/lib/instructions/flow-runbook.md +28 -25
  34. package/bin/lib/instructions/overview.md +27 -18
  35. package/bin/lib/instructions/theming.md +5 -0
  36. package/daemon/build.ts +171 -152
  37. package/daemon/client/App.tsx +1205 -1077
  38. package/daemon/client/CompareView.tsx +478 -458
  39. package/daemon/client/FileBrowser.tsx +241 -203
  40. package/daemon/client/FileViewer.tsx +220 -211
  41. package/daemon/client/OverviewView.tsx +283 -267
  42. package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
  43. package/daemon/client/RevisionSelect.tsx +111 -95
  44. package/daemon/client/SessionSwitcher.tsx +81 -81
  45. package/daemon/client/ShareDialog.tsx +211 -212
  46. package/daemon/client/apiSchemas.ts +78 -0
  47. package/daemon/client/canvas-runtime.ts +1 -1
  48. package/daemon/client/clientApi.ts +194 -206
  49. package/daemon/client/components/SecretInput.tsx +138 -138
  50. package/daemon/client/components/index.ts +2 -2
  51. package/daemon/client/domDiff.ts +341 -333
  52. package/daemon/client/errorMessage.ts +3 -0
  53. package/daemon/client/exportMarkdown.ts +222 -201
  54. package/daemon/client/lcs.ts +19 -14
  55. package/daemon/client/shareCrypto.ts +57 -57
  56. package/daemon/client/unifiedDiff.ts +660 -649
  57. package/daemon/dist/annotations-runtime.js +1 -1
  58. package/daemon/dist/client.css +2 -2
  59. package/daemon/dist/client.js +77 -80
  60. package/daemon/dist/components.js +59 -59
  61. package/daemon/dist/preact-compat.js +1 -1
  62. package/daemon/dist/review.js +361 -0
  63. package/daemon/dist/runtime.js +1 -1
  64. package/daemon/langMap.ts +24 -6
  65. package/daemon/review-widget/anchor.ts +187 -0
  66. package/daemon/review-widget/client.ts +95 -0
  67. package/daemon/review-widget/index.tsx +36 -0
  68. package/daemon/review-widget/markup.ts +71 -0
  69. package/daemon/review-widget/screenshot.ts +91 -0
  70. package/daemon/review-widget/source.ts +103 -0
  71. package/daemon/review-widget/styles.css +359 -0
  72. package/daemon/review-widget/text-modules.d.ts +4 -0
  73. package/daemon/review-widget/tsconfig.json +16 -0
  74. package/daemon/review-widget/widget.tsx +436 -0
  75. package/daemon/src/components.ts +3 -3
  76. package/daemon/src/handlers/api.ts +330 -328
  77. package/daemon/src/handlers/files.ts +112 -106
  78. package/daemon/src/handlers/static.ts +47 -46
  79. package/daemon/src/handlers/uploads.ts +61 -56
  80. package/daemon/src/paths.ts +13 -10
  81. package/daemon/src/remote-feedback.ts +192 -192
  82. package/daemon/src/review/markdown.ts +52 -0
  83. package/daemon/src/review/model.ts +132 -0
  84. package/daemon/src/review/proxy.ts +177 -0
  85. package/daemon/src/review/server.ts +199 -0
  86. package/daemon/src/review/store.ts +151 -0
  87. package/daemon/src/server.ts +127 -86
  88. package/daemon/src/share.ts +163 -169
  89. package/daemon/src/stubFetch.ts +7 -0
  90. package/daemon/tsconfig.json +19 -19
  91. package/package.json +47 -47
  92. package/skills/canvas/SKILL.md +5 -1
  93. package/bin/lib/commands/exec.test.ts +0 -27
  94. package/bin/lib/helpers.test.ts +0 -79
  95. package/daemon/src/files-api.test.ts +0 -78
  96. package/daemon/src/remote-feedback.test.ts +0 -165
  97. package/daemon/src/secrets-api.test.ts +0 -123
  98. package/daemon/src/share-e2e.test.ts +0 -277
  99. package/daemon/src/share.test.ts +0 -289
@@ -1,27 +1,16 @@
1
- import React, { useState, useEffect, useCallback } from "react";
2
-
3
- export interface ShareEntry {
4
- shareId: string;
5
- url: string;
6
- revision: number;
7
- createdAt: string;
8
- ownerToken?: string;
9
- expiresAt?: string;
10
- lastFeedbackAt?: string;
11
- /** Present iff the share is end-to-end encrypted; the URL itself
12
- * carries the key in the `#k=...` fragment. */
13
- encryptionKey?: string;
14
- }
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { shareCreatedResponseSchema, type ShareEntry, shareRevokedResponseSchema } from './apiSchemas'
3
+ import { errorMessage } from './errorMessage'
15
4
 
16
5
  interface ShareDialogProps {
17
- sessionId: string;
18
- revision: number;
19
- open: boolean;
20
- shareEnabled: boolean;
21
- existingShares: ShareEntry[];
22
- onClose: () => void;
23
- onShareCreated: (share: ShareEntry) => void;
24
- onShareRevoked: (shareId: string) => void;
6
+ sessionId: string
7
+ revision: number
8
+ open: boolean
9
+ shareEnabled: boolean
10
+ existingShares: ShareEntry[]
11
+ onClose: () => void
12
+ onShareCreated: (share: ShareEntry) => void
13
+ onShareRevoked: (shareId: string) => void
25
14
  }
26
15
 
27
16
  /**
@@ -36,207 +25,217 @@ interface ShareDialogProps {
36
25
  * stored ownerToken.
37
26
  */
38
27
  export function ShareDialog({
39
- sessionId,
40
- revision,
41
- open,
42
- shareEnabled,
43
- existingShares,
44
- onClose,
45
- onShareCreated,
46
- onShareRevoked,
28
+ sessionId,
29
+ revision,
30
+ open,
31
+ shareEnabled,
32
+ existingShares,
33
+ onClose,
34
+ onShareCreated,
35
+ onShareRevoked,
47
36
  }: ShareDialogProps) {
48
- const [loading, setLoading] = useState(false);
49
- const [revoking, setRevoking] = useState(false);
50
- const [error, setError] = useState<string | null>(null);
51
- const [copied, setCopied] = useState(false);
37
+ const [loading, setLoading] = useState(false)
38
+ const [revoking, setRevoking] = useState(false)
39
+ const [error, setError] = useState<string | null>(null)
40
+ const [copied, setCopied] = useState(false)
52
41
 
53
- const shareForRev = existingShares.find((s) => s.revision === revision);
42
+ const shareForRev = existingShares.find((s) => s.revision === revision)
54
43
 
55
- useEffect(() => {
56
- if (!open) {
57
- setLoading(false);
58
- setRevoking(false);
59
- setError(null);
60
- setCopied(false);
61
- }
62
- }, [open]);
44
+ useEffect(() => {
45
+ if (!open) {
46
+ setLoading(false)
47
+ setRevoking(false)
48
+ setError(null)
49
+ setCopied(false)
50
+ }
51
+ }, [open])
63
52
 
64
- const createShare = useCallback(async () => {
65
- setLoading(true);
66
- setError(null);
67
- try {
68
- const res = await fetch(`/api/session/${sessionId}/revision/${revision}/share`, {
69
- method: "POST",
70
- });
71
- const data = await res.json() as any;
72
- if (!res.ok || !data.ok) {
73
- throw new Error(data.error || `HTTP ${res.status}`);
74
- }
75
- onShareCreated(data.share as ShareEntry);
76
- } catch (e: any) {
77
- setError(e.message || String(e));
78
- } finally {
79
- setLoading(false);
80
- }
81
- }, [sessionId, revision, onShareCreated]);
53
+ const createShare = useCallback(async () => {
54
+ setLoading(true)
55
+ setError(null)
56
+ try {
57
+ const res = await fetch(`/api/session/${sessionId}/revision/${revision}/share`, {
58
+ method: 'POST',
59
+ })
60
+ const data = shareCreatedResponseSchema.parse(await res.json())
61
+ if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
62
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
63
+ onShareCreated(data.share)
64
+ } catch (e) {
65
+ setError(errorMessage(e))
66
+ } finally {
67
+ setLoading(false)
68
+ }
69
+ }, [sessionId, revision, onShareCreated])
82
70
 
83
- const revoke = useCallback(async (shareId: string) => {
84
- if (!window.confirm("Revoke this share link? Reviewers will no longer be able to view or comment.")) return;
85
- setRevoking(true);
86
- setError(null);
87
- try {
88
- const res = await fetch(`/api/session/${sessionId}/shares/${shareId}/revoke`, {
89
- method: "POST",
90
- });
91
- const data = await res.json() as any;
92
- if (!res.ok || !data.ok) {
93
- throw new Error(data.error || `HTTP ${res.status}`);
94
- }
95
- onShareRevoked(shareId);
96
- } catch (e: any) {
97
- setError(e.message || String(e));
98
- } finally {
99
- setRevoking(false);
100
- }
101
- }, [sessionId, onShareRevoked]);
71
+ const revoke = useCallback(async (shareId: string) => {
72
+ if (!window.confirm('Revoke this share link? Reviewers will no longer be able to view or comment.')) return
73
+ setRevoking(true)
74
+ setError(null)
75
+ try {
76
+ const res = await fetch(`/api/session/${sessionId}/shares/${shareId}/revoke`, {
77
+ method: 'POST',
78
+ })
79
+ const data = shareRevokedResponseSchema.parse(await res.json())
80
+ if (!data.ok) throw new Error(data.error || `HTTP ${res.status}`)
81
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
82
+ onShareRevoked(shareId)
83
+ } catch (e) {
84
+ setError(errorMessage(e))
85
+ } finally {
86
+ setRevoking(false)
87
+ }
88
+ }, [sessionId, onShareRevoked])
102
89
 
103
- const copyUrl = useCallback((url: string) => {
104
- navigator.clipboard.writeText(url).then(() => {
105
- setCopied(true);
106
- setTimeout(() => setCopied(false), 1500);
107
- });
108
- }, []);
90
+ const copyUrl = useCallback((url: string) => {
91
+ navigator.clipboard.writeText(url).then(() => {
92
+ setCopied(true)
93
+ setTimeout(() => setCopied(false), 1500)
94
+ })
95
+ }, [])
109
96
 
110
- if (!open) return null;
97
+ if (!open) return null
111
98
 
112
- return (
113
- <div
114
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
115
- onClick={onClose}
116
- >
117
- <div
118
- className="bg-bg-surface border border-border-medium rounded-lg shadow-xl w-full max-w-md mx-4 p-5"
119
- onClick={(e) => e.stopPropagation()}
120
- >
121
- <div className="flex items-center justify-between mb-3">
122
- <h2 className="text-sm font-medium text-text-primary font-body">
123
- Share this revision
124
- </h2>
125
- <button
126
- onClick={onClose}
127
- className="w-6 h-6 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated transition-colors"
128
- >
129
- <span className="text-xs">&#x2715;</span>
130
- </button>
131
- </div>
99
+ return (
100
+ <div
101
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
102
+ onClick={onClose}
103
+ >
104
+ <div
105
+ className="bg-bg-surface border border-border-medium rounded-lg shadow-xl w-full max-w-md mx-4 p-5"
106
+ onClick={(e) => e.stopPropagation()}
107
+ >
108
+ <div className="flex items-center justify-between mb-3">
109
+ <h2 className="text-sm font-medium text-text-primary font-body">
110
+ Share this revision
111
+ </h2>
112
+ <button
113
+ onClick={onClose}
114
+ className="w-6 h-6 flex items-center justify-center rounded text-text-tertiary hover:text-text-secondary hover:bg-bg-elevated transition-colors"
115
+ >
116
+ <span className="text-xs">&#x2715;</span>
117
+ </button>
118
+ </div>
132
119
 
133
- {!shareEnabled ? (
134
- <div className="text-[13px] text-text-secondary font-body space-y-2">
135
- <p>Canvas sharing is not configured.</p>
136
- <p className="text-text-tertiary">
137
- Set <code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]">CANVAS_SHARE_ENDPOINT</code> to the URL of a deployed{" "}
138
- <code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]">canvas-share</code> worker and restart the daemon.
139
- </p>
140
- </div>
141
- ) : shareForRev ? (
142
- <div className="space-y-3">
143
- <p className="text-[13px] text-text-secondary font-body">
144
- This revision is shared:
145
- </p>
146
- <div className="flex gap-2">
147
- <input
148
- type="text"
149
- readOnly
150
- value={shareForRev.url}
151
- className="flex-1 px-2 py-1.5 text-[12px] font-mono bg-bg-input border border-border-medium rounded text-text-primary"
152
- onFocus={(e) => e.currentTarget.select()}
153
- />
154
- <button
155
- onClick={() => copyUrl(shareForRev.url)}
156
- className="px-3 py-1.5 text-[12px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity whitespace-nowrap"
157
- >
158
- {copied ? "Copied!" : "Copy"}
159
- </button>
160
- </div>
161
- {shareForRev.expiresAt && (
162
- <p className="text-[11px] text-text-tertiary font-body">
163
- Expires {new Date(shareForRev.expiresAt).toLocaleDateString()}.
164
- </p>
165
- )}
166
- {shareForRev.encryptionKey && (
167
- <p className="text-[11px] text-accent-green font-body flex items-center gap-1.5">
168
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
169
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
170
- <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
171
- </svg>
172
- End-to-end encrypted — the key is part of the link and never leaves your browser.
173
- </p>
174
- )}
175
- <p className="text-[11px] text-text-tertiary font-body">
176
- Anyone with this link can view and leave feedback. Reviewer
177
- feedback will appear in your annotation sidebar within seconds.
178
- </p>
179
- <div className="flex items-center justify-between pt-1">
180
- <button
181
- onClick={createShare}
182
- disabled={loading || revoking}
183
- className="text-[11px] text-text-tertiary hover:text-text-secondary font-body underline disabled:opacity-50"
184
- >
185
- {loading ? "Creating..." : "Create a new link"}
186
- </button>
187
- {shareForRev.ownerToken && (
188
- <button
189
- onClick={() => revoke(shareForRev.shareId)}
190
- disabled={loading || revoking}
191
- className="text-[11px] text-accent-red hover:opacity-80 font-body underline disabled:opacity-50"
192
- >
193
- {revoking ? "Revoking..." : "Revoke link"}
194
- </button>
195
- )}
196
- </div>
197
- </div>
198
- ) : (
199
- <div className="space-y-3">
200
- <p className="text-[13px] text-text-secondary font-body">
201
- Create a public link to this revision. Anyone with the link can
202
- view the canvas and leave feedback. Reviewer feedback will sync
203
- back here automatically.
204
- </p>
205
- <button
206
- onClick={createShare}
207
- disabled={loading}
208
- className="w-full px-3 py-2 text-[13px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity disabled:opacity-50"
209
- >
210
- {loading ? "Creating share link..." : "Create share link"}
211
- </button>
212
- </div>
213
- )}
120
+ {!shareEnabled
121
+ ? (
122
+ <div className="text-[13px] text-text-secondary font-body space-y-2">
123
+ <p>Canvas sharing is not configured.</p>
124
+ <p className="text-text-tertiary">
125
+ Set <code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]">CANVAS_SHARE_ENDPOINT</code> to the URL of a deployed
126
+ {' '}
127
+ <code className="px-1 py-0.5 rounded bg-bg-elevated text-text-primary text-[11px]">canvas-share</code> worker and restart the daemon.
128
+ </p>
129
+ </div>
130
+ )
131
+ : shareForRev
132
+ ? (
133
+ <div className="space-y-3">
134
+ <p className="text-[13px] text-text-secondary font-body">
135
+ This revision is shared:
136
+ </p>
137
+ <div className="flex gap-2">
138
+ <input
139
+ type="text"
140
+ readOnly
141
+ value={shareForRev.url}
142
+ className="flex-1 px-2 py-1.5 text-[12px] font-mono bg-bg-input border border-border-medium rounded text-text-primary"
143
+ onFocus={(e) => e.currentTarget.select()}
144
+ />
145
+ <button
146
+ onClick={() => copyUrl(shareForRev.url)}
147
+ className="px-3 py-1.5 text-[12px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity whitespace-nowrap"
148
+ >
149
+ {copied ? 'Copied!' : 'Copy'}
150
+ </button>
151
+ </div>
152
+ {shareForRev.expiresAt && (
153
+ <p className="text-[11px] text-text-tertiary font-body">
154
+ Expires {new Date(shareForRev.expiresAt).toLocaleDateString()}.
155
+ </p>
156
+ )}
157
+ {shareForRev.encryptionKey && (
158
+ <p className="text-[11px] text-accent-green font-body flex items-center gap-1.5">
159
+ <svg
160
+ width="11"
161
+ height="11"
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ strokeWidth="2"
166
+ strokeLinecap="round"
167
+ strokeLinejoin="round"
168
+ >
169
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
170
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
171
+ </svg>
172
+ End-to-end encrypted — the key is part of the link and never leaves your browser.
173
+ </p>
174
+ )}
175
+ <p className="text-[11px] text-text-tertiary font-body">
176
+ Anyone with this link can view and leave feedback. Reviewer feedback will appear in your annotation sidebar within seconds.
177
+ </p>
178
+ <div className="flex items-center justify-between pt-1">
179
+ <button
180
+ onClick={createShare}
181
+ disabled={loading || revoking}
182
+ className="text-[11px] text-text-tertiary hover:text-text-secondary font-body underline disabled:opacity-50"
183
+ >
184
+ {loading ? 'Creating...' : 'Create a new link'}
185
+ </button>
186
+ {shareForRev.ownerToken && (
187
+ <button
188
+ onClick={() => revoke(shareForRev.shareId)}
189
+ disabled={loading || revoking}
190
+ className="text-[11px] text-accent-red hover:opacity-80 font-body underline disabled:opacity-50"
191
+ >
192
+ {revoking ? 'Revoking...' : 'Revoke link'}
193
+ </button>
194
+ )}
195
+ </div>
196
+ </div>
197
+ )
198
+ : (
199
+ <div className="space-y-3">
200
+ <p className="text-[13px] text-text-secondary font-body">
201
+ Create a public link to this revision. Anyone with the link can view the canvas and leave feedback. Reviewer feedback will sync back here
202
+ automatically.
203
+ </p>
204
+ <button
205
+ onClick={createShare}
206
+ disabled={loading}
207
+ className="w-full px-3 py-2 text-[13px] font-body font-medium rounded bg-accent-blue text-white hover:opacity-90 transition-opacity disabled:opacity-50"
208
+ >
209
+ {loading ? 'Creating share link...' : 'Create share link'}
210
+ </button>
211
+ </div>
212
+ )}
214
213
 
215
- {error && (
216
- <div className="mt-3 px-3 py-2 rounded bg-accent-red/10 border border-accent-red/20 text-[12px] text-accent-red font-body">
217
- {error}
218
- </div>
219
- )}
220
- </div>
221
- </div>
222
- );
214
+ {error && (
215
+ <div className="mt-3 px-3 py-2 rounded bg-accent-red/10 border border-accent-red/20 text-[12px] text-accent-red font-body">
216
+ {error}
217
+ </div>
218
+ )}
219
+ </div>
220
+ </div>
221
+ )
223
222
  }
224
223
 
225
224
  /** Small share icon button for toolbar use. */
226
- export function ShareButton({ onClick, title = "Share this revision", hasShare }: { onClick: () => void; title?: string; hasShare?: boolean }) {
227
- return (
228
- <button
229
- onClick={onClick}
230
- className={`group p-1.5 transition-colors ${hasShare ? "text-accent-blue hover:opacity-80" : "text-text-tertiary hover:text-text-secondary"}`}
231
- title={title}
232
- >
233
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
234
- <circle cx="18" cy="5" r="3"/>
235
- <circle cx="6" cy="12" r="3"/>
236
- <circle cx="18" cy="19" r="3"/>
237
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
238
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
239
- </svg>
240
- </button>
241
- );
225
+ export function ShareButton({ onClick, title = 'Share this revision', hasShare }: { onClick: () => void; title?: string; hasShare?: boolean }) {
226
+ return (
227
+ <button
228
+ onClick={onClick}
229
+ className={`group p-1.5 transition-colors ${hasShare ? 'text-accent-blue hover:opacity-80' : 'text-text-tertiary hover:text-text-secondary'}`}
230
+ title={title}
231
+ >
232
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
233
+ <circle cx="18" cy="5" r="3" />
234
+ <circle cx="6" cy="12" r="3" />
235
+ <circle cx="18" cy="19" r="3" />
236
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
237
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
238
+ </svg>
239
+ </button>
240
+ )
242
241
  }
@@ -0,0 +1,78 @@
1
+ import { annotationAuthorSchema, annotationSchema } from '@fabrika/annotations/schemas'
2
+ import { z } from 'zod'
3
+
4
+ export const shareEntrySchema = z.object({
5
+ shareId: z.string(),
6
+ url: z.string(),
7
+ revision: z.number(),
8
+ createdAt: z.string(),
9
+ ownerToken: z.string().optional(),
10
+ expiresAt: z.string().optional(),
11
+ lastFeedbackAt: z.string().optional(),
12
+ /** Present iff the share is end-to-end encrypted; the URL itself
13
+ * carries the key in the `#k=...` fragment. */
14
+ encryptionKey: z.string().optional(),
15
+ })
16
+
17
+ export type ShareEntry = z.infer<typeof shareEntrySchema>
18
+
19
+ const revisionInfoSchema = z.object({
20
+ revision: z.number(),
21
+ label: z.string().optional(),
22
+ canvasFiles: z.array(z.object({
23
+ filename: z.string(),
24
+ diffStats: z.object({ added: z.number(), removed: z.number() }).optional(),
25
+ })),
26
+ createdAt: z.string(),
27
+ hasFeedback: z.boolean(),
28
+ feedbackConsumed: z.boolean(),
29
+ response: z.string().optional(),
30
+ }).passthrough()
31
+
32
+ /** Served by the daemon for a session and by the share worker for a share, which leaves `projectRoot` out. */
33
+ export const sessionMetaSchema = z.object({
34
+ encryption: z.object({ alg: z.literal('AES-GCM'), v: z.literal(1) }).optional(),
35
+ origin: z.object({
36
+ sessionId: z.string().optional(),
37
+ label: z.string().optional(),
38
+ revision: z.number().optional(),
39
+ createdAt: z.string().optional(),
40
+ }).optional(),
41
+ currentRevision: z.number().optional(),
42
+ revisions: z.array(revisionInfoSchema).optional(),
43
+ canvasFiles: z.array(z.string()).optional(),
44
+ projectRoot: z.string().optional(),
45
+ shares: z.array(shareEntrySchema).optional(),
46
+ shareEnabled: z.boolean().optional(),
47
+ }).passthrough()
48
+
49
+ export type SessionMeta = z.infer<typeof sessionMetaSchema>
50
+
51
+ export const treeResponseSchema = z.object({
52
+ entries: z.array(z.object({ name: z.string(), type: z.enum(['file', 'dir']) })),
53
+ })
54
+
55
+ /** Mirrors the daemon's RemoteFeedbackEntry. */
56
+ export const remoteFeedbackEntrySchema = z.object({
57
+ id: z.string(),
58
+ shareId: z.string(),
59
+ revision: z.number(),
60
+ submittedAt: z.string(),
61
+ author: annotationAuthorSchema,
62
+ annotations: z.array(annotationSchema.omit({ source: true, author: true })),
63
+ generalNote: z.string().optional(),
64
+ })
65
+
66
+ export type RemoteFeedbackEntry = z.infer<typeof remoteFeedbackEntrySchema>
67
+
68
+ export const remoteFeedbackResponseSchema = z.object({ entries: z.array(remoteFeedbackEntrySchema) })
69
+
70
+ export const shareCreatedResponseSchema = z.union([
71
+ z.object({ ok: z.literal(true), share: shareEntrySchema }),
72
+ z.object({ ok: z.literal(false).optional(), error: z.string().optional() }),
73
+ ])
74
+
75
+ export const shareRevokedResponseSchema = z.union([
76
+ z.object({ ok: z.literal(true) }),
77
+ z.object({ ok: z.literal(false).optional(), error: z.string().optional() }),
78
+ ])
@@ -1 +1 @@
1
- export * from "@fabrika/canvas-kernel/runtime";
1
+ export * from '@fabrika/canvas-kernel/runtime'