@jxsuite/studio 0.35.0 → 0.37.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.
- package/dist/iframe-entry.js +18 -9
- package/dist/iframe-entry.js.map +4 -4
- package/dist/studio.js +12278 -2990
- package/dist/studio.js.map +73 -21
- package/package.json +10 -5
- package/src/canvas/canvas-render.ts +85 -12
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-overlay.ts +30 -1
- package/src/collab/collab-session.ts +903 -0
- package/src/collab/collab-state.ts +71 -0
- package/src/collab/monaco-cursors.ts +69 -0
- package/src/collab/presence-chips.ts +58 -0
- package/src/files/file-ops.ts +6 -0
- package/src/files/files.ts +6 -0
- package/src/panels/ai-panel.ts +25 -3
- package/src/panels/git-panel.ts +5 -0
- package/src/panels/toolbar.ts +15 -3
- package/src/panels/welcome-screen.ts +26 -0
- package/src/platforms/cloud.ts +805 -0
- package/src/platforms/devserver.ts +107 -0
- package/src/project-list.ts +38 -0
- package/src/publish/pages-service.ts +186 -0
- package/src/publish/publish-panel.ts +360 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/cf-settings.ts +58 -0
- package/src/services/settings-store.ts +7 -1
- package/src/studio.ts +25 -0
- package/src/tabs/doc-op-apply.ts +5 -81
- package/src/tabs/patch-ops.ts +7 -23
- package/src/tabs/transact.ts +138 -12
- package/src/types.ts +95 -174
- package/src/workspace/workspace.ts +4 -0
package/src/tabs/transact.ts
CHANGED
|
@@ -79,22 +79,64 @@ export type JxNodeValue =
|
|
|
79
79
|
|
|
80
80
|
// ─── Transactional layer ─────────────────────────────────────────────────────
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Where a transaction came from: a direct user/tool edit, the undo/redo machinery replaying
|
|
84
|
+
* recorded ops, or a remote collaborator's ops applied locally. Observers use this to avoid
|
|
85
|
+
* republishing what they themselves applied.
|
|
86
|
+
*/
|
|
87
|
+
export type TransactOrigin = "user" | "history" | "remote";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Module-level hook invoked at the end of every transactDoc (after the canvas patch apply and the
|
|
91
|
+
* dirty mark). Registered by the collab layer to mirror local edits into a shared document; at most
|
|
92
|
+
* one observer exists. `record.docOps` may be empty for un-instrumented mutations — observers must
|
|
93
|
+
* handle that (e.g. by diffing).
|
|
94
|
+
*/
|
|
95
|
+
export type TransactObserver = (
|
|
96
|
+
tab: Tab,
|
|
97
|
+
record: TransactionRecord,
|
|
98
|
+
origin: TransactOrigin,
|
|
99
|
+
) => void;
|
|
100
|
+
|
|
101
|
+
let _transactObserver: TransactObserver | null = null;
|
|
102
|
+
|
|
103
|
+
export function setTransactObserver(fn: TransactObserver | null) {
|
|
104
|
+
_transactObserver = fn;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Module-level gate consulted at transactDoc entry: return a refusal reason to block the mutation
|
|
109
|
+
* (the collab layer soft-freezes structural editing while a peer holds source-canonical), or null
|
|
110
|
+
* to proceed. Remote-origin transactions always pass — they ARE the frozen representation's
|
|
111
|
+
* mirror.
|
|
112
|
+
*/
|
|
113
|
+
export type TransactGate = (tab: Tab, origin: TransactOrigin) => string | null;
|
|
114
|
+
|
|
115
|
+
let _transactGate: TransactGate | null = null;
|
|
116
|
+
|
|
117
|
+
export function setTransactGate(fn: TransactGate | null) {
|
|
118
|
+
_transactGate = fn;
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
/**
|
|
83
122
|
* Apply a document mutation transactionally: push to history and mark dirty. The mutationFn
|
|
84
123
|
* receives the tab and should mutate tab.doc.document in place.
|
|
85
124
|
*
|
|
86
125
|
* @param {Tab | null} tab
|
|
87
126
|
* @param {(tab: Tab) => void} mutationFn
|
|
88
|
-
* @param {{ skipHistory?: boolean }} [opts]
|
|
127
|
+
* @param {{ skipHistory?: boolean; origin?: TransactOrigin }} [opts]
|
|
89
128
|
*/
|
|
90
129
|
export function transactDoc(
|
|
91
130
|
tab: Tab | null,
|
|
92
131
|
mutationFn: (tab: Tab) => void,
|
|
93
|
-
{ skipHistory = false }: { skipHistory?: boolean } = {},
|
|
132
|
+
{ skipHistory = false, origin = "user" }: { skipHistory?: boolean; origin?: TransactOrigin } = {},
|
|
94
133
|
) {
|
|
95
134
|
if (!tab) {
|
|
96
135
|
return;
|
|
97
136
|
}
|
|
137
|
+
if (origin !== "remote" && _transactGate?.(tab, origin)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
98
140
|
const selectionBefore = tab.session.selection ? [...tab.session.selection] : null;
|
|
99
141
|
beginRecording();
|
|
100
142
|
let record: TransactionRecord;
|
|
@@ -141,6 +183,8 @@ export function transactDoc(
|
|
|
141
183
|
}
|
|
142
184
|
|
|
143
185
|
tab.doc.dirty = true;
|
|
186
|
+
|
|
187
|
+
_transactObserver?.(tab, record, origin);
|
|
144
188
|
}
|
|
145
189
|
|
|
146
190
|
/**
|
|
@@ -215,16 +259,38 @@ function materializeState(
|
|
|
215
259
|
*
|
|
216
260
|
* @param {Tab | null} tab
|
|
217
261
|
* @param {(doc: JxMutableNode) => void} fn
|
|
218
|
-
* @param {{ skipHistory?: boolean }} [opts]
|
|
262
|
+
* @param {{ skipHistory?: boolean; origin?: TransactOrigin }} [opts]
|
|
219
263
|
*/
|
|
220
264
|
export function transact(
|
|
221
265
|
tab: Tab | null,
|
|
222
266
|
fn: (doc: JxMutableNode) => void,
|
|
223
|
-
opts?: { skipHistory?: boolean },
|
|
267
|
+
opts?: { skipHistory?: boolean; origin?: TransactOrigin },
|
|
224
268
|
) {
|
|
225
269
|
transactDoc(tab, (t) => fn(t.doc.document), opts);
|
|
226
270
|
}
|
|
227
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Apply externally-produced doc ops (a remote collaborator's edits) through the normal transaction
|
|
274
|
+
* pipeline: the canvas patches surgically exactly as it does for undo/redo replay, panels' effects
|
|
275
|
+
* fire, and — because the origin is "remote" — the transact observer will not republish them.
|
|
276
|
+
* History is skipped; while a collab session is attached, undo is delegated (see
|
|
277
|
+
* setHistoryDelegate) and local-only.
|
|
278
|
+
*
|
|
279
|
+
* @param {Tab} tab
|
|
280
|
+
* @param {JxDocOp[]} ops
|
|
281
|
+
*/
|
|
282
|
+
export function applyExternalDocOps(tab: Tab, ops: JxDocOp[]) {
|
|
283
|
+
transactDoc(
|
|
284
|
+
tab,
|
|
285
|
+
(t) => {
|
|
286
|
+
for (const op of ops) {
|
|
287
|
+
applyDocOp(t, op);
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{ origin: "remote", skipHistory: true },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
228
294
|
// ─── Document-op application ─────────────────────────────────────────────────
|
|
229
295
|
|
|
230
296
|
/** Document-level keys whose changes require a full scope/panel rebuild on the canvas. */
|
|
@@ -289,26 +355,40 @@ function applyDocOp(tab: Tab, op: JxDocOp) {
|
|
|
289
355
|
|
|
290
356
|
let _batchTab: Tab | null = null;
|
|
291
357
|
|
|
358
|
+
/** Module-level hook invoked when a batch closes (collab flushes its buffered ops as one step). */
|
|
359
|
+
export type BatchEndNotifier = (tab: Tab) => void;
|
|
360
|
+
|
|
361
|
+
let _batchEndNotifier: BatchEndNotifier | null = null;
|
|
362
|
+
|
|
363
|
+
export function setBatchEndNotifier(fn: BatchEndNotifier | null) {
|
|
364
|
+
_batchEndNotifier = fn;
|
|
365
|
+
}
|
|
366
|
+
|
|
292
367
|
export function beginBatch(tab: Tab | null) {
|
|
293
368
|
_batchTab = tab;
|
|
294
369
|
}
|
|
295
370
|
|
|
296
371
|
export function endBatch() {
|
|
297
|
-
|
|
298
|
-
|
|
372
|
+
const tab = _batchTab;
|
|
373
|
+
// A history delegate owns undo grouping while registered; the snapshot push would be dead weight.
|
|
374
|
+
if (tab && !_historyDelegates.has(tab)) {
|
|
375
|
+
const raw = toRaw(tab.doc.document);
|
|
299
376
|
const snapshot = {
|
|
300
377
|
document: jsonClone(raw),
|
|
301
|
-
selection:
|
|
378
|
+
selection: tab.session.selection ? [...tab.session.selection] : null,
|
|
302
379
|
};
|
|
303
|
-
const truncated =
|
|
380
|
+
const truncated = tab.history.snapshots.slice(0, tab.history.index + 1);
|
|
304
381
|
truncated.push(snapshot);
|
|
305
382
|
if (truncated.length > HISTORY_LIMIT) {
|
|
306
383
|
truncated.shift();
|
|
307
384
|
}
|
|
308
|
-
|
|
309
|
-
|
|
385
|
+
tab.history.snapshots = truncated;
|
|
386
|
+
tab.history.index = truncated.length - 1;
|
|
310
387
|
}
|
|
311
388
|
_batchTab = null;
|
|
389
|
+
if (tab) {
|
|
390
|
+
_batchEndNotifier?.(tab);
|
|
391
|
+
}
|
|
312
392
|
}
|
|
313
393
|
|
|
314
394
|
export function isBatching(): boolean {
|
|
@@ -317,6 +397,42 @@ export function isBatching(): boolean {
|
|
|
317
397
|
|
|
318
398
|
// ─── Undo / Redo ─────────────────────────────────────────────────────────────
|
|
319
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Per-tab replacement for the built-in op-log history. While a delegate is registered (a collab
|
|
402
|
+
* session's Y.UndoManager), undo/redo route to it and the snapshot history is bypassed — keeping
|
|
403
|
+
* this module free of any yjs knowledge.
|
|
404
|
+
*/
|
|
405
|
+
export interface HistoryDelegate {
|
|
406
|
+
undo: (tab: Tab) => void;
|
|
407
|
+
redo: (tab: Tab) => void;
|
|
408
|
+
canUndo: (tab: Tab) => boolean;
|
|
409
|
+
canRedo: (tab: Tab) => boolean;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const _historyDelegates = new WeakMap<Tab, HistoryDelegate>();
|
|
413
|
+
|
|
414
|
+
export function setHistoryDelegate(tab: Tab, delegate: HistoryDelegate | null) {
|
|
415
|
+
if (delegate) {
|
|
416
|
+
_historyDelegates.set(tab, delegate);
|
|
417
|
+
} else {
|
|
418
|
+
_historyDelegates.delete(tab);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function getHistoryDelegate(tab: Tab): HistoryDelegate | null {
|
|
423
|
+
return _historyDelegates.get(tab) ?? null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function canUndo(tab: Tab): boolean {
|
|
427
|
+
const delegate = _historyDelegates.get(tab);
|
|
428
|
+
return delegate ? delegate.canUndo(tab) : tab.history.index > 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function canRedo(tab: Tab): boolean {
|
|
432
|
+
const delegate = _historyDelegates.get(tab);
|
|
433
|
+
return delegate ? delegate.canRedo(tab) : tab.history.index < tab.history.snapshots.length - 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
320
436
|
/** Restore a materialized snapshot state (full-render path). */
|
|
321
437
|
function restoreState(tab: Tab, index: number) {
|
|
322
438
|
const { snapshots } = tab.history;
|
|
@@ -345,6 +461,11 @@ function assertHistoryConsistency(tab: Tab, index: number) {
|
|
|
345
461
|
|
|
346
462
|
/** @param {Tab} tab */
|
|
347
463
|
export function undo(tab: Tab) {
|
|
464
|
+
const delegate = _historyDelegates.get(tab);
|
|
465
|
+
if (delegate) {
|
|
466
|
+
delegate.undo(tab);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
348
469
|
if (tab.history.index <= 0) {
|
|
349
470
|
return;
|
|
350
471
|
}
|
|
@@ -360,7 +481,7 @@ export function undo(tab: Tab) {
|
|
|
360
481
|
applyDocOp(t, toRaw(inverseOps[i]) as JxDocOp);
|
|
361
482
|
}
|
|
362
483
|
},
|
|
363
|
-
{ skipHistory: true },
|
|
484
|
+
{ origin: "history", skipHistory: true },
|
|
364
485
|
);
|
|
365
486
|
tab.session.selection = entry.selectionBefore ? [...toRaw(entry.selectionBefore)] : null;
|
|
366
487
|
tab.history.index -= 1;
|
|
@@ -374,6 +495,11 @@ export function undo(tab: Tab) {
|
|
|
374
495
|
|
|
375
496
|
/** @param {Tab} tab */
|
|
376
497
|
export function redo(tab: Tab) {
|
|
498
|
+
const delegate = _historyDelegates.get(tab);
|
|
499
|
+
if (delegate) {
|
|
500
|
+
delegate.redo(tab);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
377
503
|
if (tab.history.index >= tab.history.snapshots.length - 1) {
|
|
378
504
|
return;
|
|
379
505
|
}
|
|
@@ -387,7 +513,7 @@ export function redo(tab: Tab) {
|
|
|
387
513
|
applyDocOp(t, toRaw(op) as JxDocOp);
|
|
388
514
|
}
|
|
389
515
|
},
|
|
390
|
-
{ skipHistory: true },
|
|
516
|
+
{ origin: "history", skipHistory: true },
|
|
391
517
|
);
|
|
392
518
|
tab.session.selection = entry.selection ? [...toRaw(entry.selection)] : null;
|
|
393
519
|
tab.history.index += 1;
|
package/src/types.ts
CHANGED
|
@@ -1,168 +1,59 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
JxMutableNode,
|
|
5
|
-
JxPath,
|
|
6
|
-
ProjectConfig,
|
|
7
|
-
} from "@jxsuite/schema/types";
|
|
8
|
-
|
|
9
|
-
// ─── Git & Platform Types ───────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export interface GitFileStatus {
|
|
12
|
-
status: string;
|
|
13
|
-
path: string;
|
|
14
|
-
staged?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface GitStatusResult {
|
|
18
|
-
branch: string;
|
|
19
|
-
files: GitFileStatus[];
|
|
20
|
-
ahead: number;
|
|
21
|
-
behind: number;
|
|
22
|
-
isRepo: boolean;
|
|
23
|
-
remotes: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface GitBranchesResult {
|
|
27
|
-
current: string;
|
|
28
|
-
branches: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface GitLogEntry {
|
|
32
|
-
hash: string;
|
|
33
|
-
message: string;
|
|
34
|
-
author: string;
|
|
35
|
-
date: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface ComponentSlotMeta {
|
|
39
|
-
name: string;
|
|
40
|
-
description?: string;
|
|
41
|
-
fallback?: (JxMutableNode | string)[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ComponentMeta {
|
|
45
|
-
tagName: string;
|
|
46
|
-
$id?: string | null;
|
|
47
|
-
path: string;
|
|
48
|
-
props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
|
|
49
|
-
slots?: ComponentSlotMeta[];
|
|
50
|
-
hasElements?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface PackageInfo {
|
|
54
|
-
name: string;
|
|
55
|
-
version: string;
|
|
56
|
-
/** True when the dependency lives in `devDependencies` rather than `dependencies`. */
|
|
57
|
-
dev?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
|
|
61
|
-
export interface OutdatedInfo {
|
|
62
|
-
name: string;
|
|
63
|
-
/** The version range pinned in package.json (e.g. "^0.19.0"). */
|
|
64
|
-
current: string;
|
|
65
|
-
/** The newest published version. */
|
|
66
|
-
latest: string;
|
|
67
|
-
/** The newest version satisfying the current range, if known. */
|
|
68
|
-
wanted?: string;
|
|
69
|
-
dev?: boolean;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Result of a package mutation that runs `bun install` (install / set-versions). */
|
|
73
|
-
export interface PackageOpResult {
|
|
74
|
-
ok: boolean;
|
|
75
|
-
/** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
|
|
76
|
-
log?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Desktop app/build info surfaced in the About screen. */
|
|
80
|
-
export interface AppInfo {
|
|
81
|
-
version: string;
|
|
82
|
-
channel: string;
|
|
83
|
-
hash: string;
|
|
84
|
-
/** Human-readable update status (e.g. "Up to date", "Update available"), if known. */
|
|
85
|
-
updateStatus?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface CodeServiceResult {
|
|
89
|
-
code?: string;
|
|
90
|
-
diagnostics?: unknown[];
|
|
91
|
-
[key: string]: unknown;
|
|
92
|
-
}
|
|
2
|
+
import type { CollabHandle } from "@jxsuite/collab/provider";
|
|
3
|
+
import type { JxMutableNode, JxPath, ProjectConfig } from "@jxsuite/schema/types";
|
|
93
4
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
type: "file" | "directory";
|
|
98
|
-
size?: number;
|
|
99
|
-
modified?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
|
|
103
|
-
export interface FsEvent {
|
|
104
|
-
type: "add" | "change" | "unlink" | "addDir" | "unlinkDir";
|
|
105
|
-
path: string;
|
|
106
|
-
isDir: boolean;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Result of a rename, including the references rewritten across the project (refactor report). */
|
|
110
|
-
export interface RenameResult {
|
|
111
|
-
ok: boolean;
|
|
112
|
-
from: string;
|
|
113
|
-
to: string;
|
|
114
|
-
isDir?: boolean;
|
|
115
|
-
references?: {
|
|
116
|
-
filesChanged: number;
|
|
117
|
-
refsUpdated: number;
|
|
118
|
-
files: { path: string; count: number }[];
|
|
119
|
-
};
|
|
120
|
-
errors?: { path: string; error: string }[];
|
|
121
|
-
tag?: { from: string; to: string; filesChanged: number; refsUpdated: number };
|
|
122
|
-
tagSkipped?: string;
|
|
123
|
-
error?: string;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
|
|
127
|
-
export interface StarterInfo {
|
|
128
|
-
id: string;
|
|
129
|
-
name: string;
|
|
130
|
-
industry: string;
|
|
131
|
-
tagline: string;
|
|
132
|
-
description: string;
|
|
133
|
-
features: string[];
|
|
134
|
-
accent: string;
|
|
135
|
-
/** Preview image as a self-contained `data:` URI. */
|
|
136
|
-
thumbnail: string;
|
|
137
|
-
}
|
|
5
|
+
// ─── Wire types (the Studio Backend Protocol) ───────────────────────────────
|
|
6
|
+
/* The request/response shapes every backend serves live in @jxsuite/protocol;
|
|
7
|
+
re-exported here so existing `../types` imports keep working. */
|
|
138
8
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
9
|
+
import type {
|
|
10
|
+
AppInfo,
|
|
11
|
+
CfConnection,
|
|
12
|
+
CodeServiceResult,
|
|
13
|
+
ComponentMeta,
|
|
14
|
+
DirEntry,
|
|
15
|
+
FsEvent,
|
|
16
|
+
GitBranchesResult,
|
|
17
|
+
GitLogEntry,
|
|
18
|
+
GitStatusResult,
|
|
19
|
+
ImportProgressEvent,
|
|
20
|
+
ImportSiteOptions,
|
|
21
|
+
OutdatedInfo,
|
|
22
|
+
PackageInfo,
|
|
23
|
+
PackageOpResult,
|
|
24
|
+
ProjectListEntry,
|
|
25
|
+
RecentProjectEntry,
|
|
26
|
+
RenameResult,
|
|
27
|
+
StarterInfo,
|
|
28
|
+
} from "@jxsuite/protocol";
|
|
146
29
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
30
|
+
export type {
|
|
31
|
+
AiModelInfo,
|
|
32
|
+
AiModelsResponse,
|
|
33
|
+
AppInfo,
|
|
34
|
+
CfConnection,
|
|
35
|
+
CodeServiceResult,
|
|
36
|
+
ComponentMeta,
|
|
37
|
+
ComponentSlotMeta,
|
|
38
|
+
DirEntry,
|
|
39
|
+
ErrorBody,
|
|
40
|
+
FsEvent,
|
|
41
|
+
GitBranchesResult,
|
|
42
|
+
GitFileStatus,
|
|
43
|
+
GitLogEntry,
|
|
44
|
+
GitStatusResult,
|
|
45
|
+
ImportProgressEvent,
|
|
46
|
+
ImportSiteOptions,
|
|
47
|
+
JsonValue,
|
|
48
|
+
OutdatedInfo,
|
|
49
|
+
PackageInfo,
|
|
50
|
+
PackageOpResult,
|
|
51
|
+
ProjectListEntry,
|
|
52
|
+
PullRequestInfo,
|
|
53
|
+
RecentProjectEntry,
|
|
54
|
+
RenameResult,
|
|
55
|
+
StarterInfo,
|
|
56
|
+
} from "@jxsuite/protocol";
|
|
166
57
|
|
|
167
58
|
export interface StudioPlatform {
|
|
168
59
|
id: string;
|
|
@@ -326,6 +217,49 @@ export interface StudioPlatform {
|
|
|
326
217
|
getRecentProjects?: () => Promise<RecentProjectEntry[]>;
|
|
327
218
|
/** Persist the full recent-projects list to the backend store. */
|
|
328
219
|
saveRecentProjects?: (projects: RecentProjectEntry[]) => Promise<void>;
|
|
220
|
+
// ─── Project catalogue (platforms that can enumerate openable projects) ─────
|
|
221
|
+
/**
|
|
222
|
+
* Enumerate every project this platform can open — the dev server's sites under its root, a cloud
|
|
223
|
+
* platform's remote projects. Entry `root` values re-open through the same paths as recent
|
|
224
|
+
* projects (openRecentProject). Absent on desktop, where the OS file system is the catalogue and
|
|
225
|
+
* projects are found via the native picker instead.
|
|
226
|
+
*/
|
|
227
|
+
listProjects?: () => Promise<ProjectListEntry[]>;
|
|
228
|
+
/**
|
|
229
|
+
* Open a realtime co-editing session for a project-relative document path. Optional: platforms
|
|
230
|
+
* without a collab backend omit it and Studio edits solo with file-level saves. Resolves null
|
|
231
|
+
* when the backend refuses a room for this doc (binary, oversized). The returned handle's Y.Doc
|
|
232
|
+
* starts empty and fills from the provider — see `@jxsuite/collab/provider` for the contract.
|
|
233
|
+
*/
|
|
234
|
+
collab?: (docPath: string) => Promise<CollabHandle | null>;
|
|
235
|
+
// ─── Identity & hosting connections (publish surface) ───────────────────────
|
|
236
|
+
/** The signed-in user's identity, when the platform has one (cloud). */
|
|
237
|
+
getUser?: () => Promise<{ login: string; name?: string; avatarUrl?: string } | null>;
|
|
238
|
+
/**
|
|
239
|
+
* Open a pull request for the current branch. Cloud platforms implement it against their session;
|
|
240
|
+
* local platforms omit it and Studio falls back to a direct GitHub API call with the user's
|
|
241
|
+
* stored token.
|
|
242
|
+
*/
|
|
243
|
+
createPullRequest?: (opts: {
|
|
244
|
+
title: string;
|
|
245
|
+
body?: string;
|
|
246
|
+
head?: string;
|
|
247
|
+
base?: string;
|
|
248
|
+
}) => Promise<{ url: string; number: number }>;
|
|
249
|
+
/** Current Cloudflare connection state, when the platform can broker one. */
|
|
250
|
+
cfConnection?: () => Promise<CfConnection | null>;
|
|
251
|
+
/**
|
|
252
|
+
* Interactively connect a Cloudflare account (hosted OAuth on the cloud platform). Local
|
|
253
|
+
* platforms omit it — the publish UI collects an API token instead and verifies via
|
|
254
|
+
* cfConnection.
|
|
255
|
+
*/
|
|
256
|
+
cfConnect?: () => Promise<CfConnection | null>;
|
|
257
|
+
/**
|
|
258
|
+
* Allowlisted Cloudflare API passthrough (accounts, Pages projects and deployments). The backend
|
|
259
|
+
* injects credentials — an OAuth token on the cloud platform, the user's pasted API token locally
|
|
260
|
+
* — so no secret ever rides in the request body.
|
|
261
|
+
*/
|
|
262
|
+
cfApi?: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>;
|
|
329
263
|
// ─── User settings (backend-persisted; undefined on dev-server) ─────────────
|
|
330
264
|
/**
|
|
331
265
|
* Read the user-level settings map (e.g. the AI connection parameters) from a backend store
|
|
@@ -337,13 +271,6 @@ export interface StudioPlatform {
|
|
|
337
271
|
saveSettings?: (settings: Record<string, string>) => Promise<void>;
|
|
338
272
|
}
|
|
339
273
|
|
|
340
|
-
/** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
|
|
341
|
-
export interface RecentProjectEntry {
|
|
342
|
-
name: string;
|
|
343
|
-
root: string;
|
|
344
|
-
timestamp: number;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
274
|
// ─── Studio Types ───────────────────────────────────────────────────────────
|
|
348
275
|
|
|
349
276
|
export interface CanvasPanel {
|
|
@@ -409,9 +336,3 @@ export interface ProjectState {
|
|
|
409
336
|
projectDirs?: string[];
|
|
410
337
|
[key: string]: unknown;
|
|
411
338
|
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* A JSON document value, or `undefined` to signal property removal in the mutators. Re-uses the
|
|
415
|
-
* schema's precise recursive JSON model.
|
|
416
|
-
*/
|
|
417
|
-
export type JsonValue = SchemaJsonValue | undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { computed, reactive, toRaw } from "../reactivity";
|
|
2
|
+
import { ensureCollab, rekeyCollab } from "../collab/collab-session";
|
|
2
3
|
import { createTab, disposeTab } from "../tabs/tab";
|
|
3
4
|
import type { Tab } from "../tabs/tab";
|
|
4
5
|
|
|
@@ -96,6 +97,7 @@ export function openTab(opts: {
|
|
|
96
97
|
workspace.tabs.set(tab.id, tab);
|
|
97
98
|
workspace.tabOrder.push(tab.id);
|
|
98
99
|
workspace.activeTabId = tab.id;
|
|
100
|
+
ensureCollab(tab);
|
|
99
101
|
return tab;
|
|
100
102
|
}
|
|
101
103
|
|
|
@@ -155,6 +157,7 @@ export function replaceAllTabs(newTabOpts: {
|
|
|
155
157
|
workspace.tabs.set(newTab.id, newTab);
|
|
156
158
|
workspace.activeTabId = newTab.id;
|
|
157
159
|
workspace.tabOrder = [newTab.id];
|
|
160
|
+
ensureCollab(newTab);
|
|
158
161
|
|
|
159
162
|
for (const id of oldIds) {
|
|
160
163
|
if (id === newTab.id) {
|
|
@@ -201,4 +204,5 @@ export function renameTab(oldId: string, newId: string, newDocumentPath: string)
|
|
|
201
204
|
if (workspace.activeTabId === oldId) {
|
|
202
205
|
workspace.activeTabId = newId;
|
|
203
206
|
}
|
|
207
|
+
rekeyCollab(tab);
|
|
204
208
|
}
|