@jxsuite/studio 5.5.0 → 5.7.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/LICENSE +1 -1
- package/dist/chunks/{2020-g8mwpbkq.js → 2020-cs2sb9t1.js} +3 -3
- package/dist/chunks/adapter-y5gdq19c.js +4 -0
- package/dist/chunks/cf-account-picker-882a7ecm.js +4 -0
- package/dist/chunks/cf-settings-20dznc3j.js +4 -0
- package/dist/chunks/components-q037dya7.js +4 -0
- package/dist/chunks/content-source-37x3vsap.js +4 -0
- package/dist/chunks/convert-to-repeater-grfep2w8.js +4 -0
- package/dist/chunks/editor-j5sq149q.js +4 -0
- package/dist/chunks/entry-commands-c5h3nqck.js +4 -0
- package/dist/chunks/extension-sections-dp8endhx.js +4 -0
- package/dist/chunks/file-ops-937cvqk6.js +4 -0
- package/dist/chunks/files-tznwe1p1.js +4 -0
- package/dist/chunks/format-host-e2pjvd25.js +4 -0
- package/dist/chunks/fs-events-fv64ak3s.js +4 -0
- package/dist/chunks/git-panel-gb713ttc.js +4 -0
- package/dist/chunks/grid-view-5amhg604.js +51 -0
- package/dist/chunks/grid-view-5mqv1cbd.css +1 -0
- package/dist/chunks/iframe-host-t819mcg5.js +4 -0
- package/dist/chunks/{index-arg188jq.js → index-9ydfe04y.js} +3 -3
- package/dist/chunks/index-m0x5ztax.js +4 -0
- package/dist/chunks/{javascript-zcdmye2k.js → javascript-d9pqgc2p.js} +2 -2
- package/dist/chunks/{jsonMode-6x5e1nbf.js → jsonMode-800yvbem.js} +5 -5
- package/dist/chunks/jx-validate-zc9zp77s.js +4 -0
- package/dist/chunks/layers-4bsr82wz.js +4 -0
- package/dist/chunks/layers-panel-b6shnaax.js +4 -0
- package/dist/chunks/lit-html-cfn61dn7.js +4 -0
- package/dist/chunks/media-picker-ej0kdh1j.js +4 -0
- package/dist/chunks/menu-rtxf9chb.js +4 -0
- package/dist/chunks/{monaco-binding-4az2kh5p.js → monaco-binding-62dtmgbf.js} +3 -3
- package/dist/chunks/monaco-buffer-nn4dtx27.js +4 -0
- package/dist/chunks/{monaco-setup-d7mwkcx3.js → monaco-setup-tj13ndtm.js} +3 -3
- package/dist/chunks/notify-ahs57a2z.js +4 -0
- package/dist/chunks/platform-v9xyfkbx.js +4 -0
- package/dist/chunks/publish-panel-bq36xcmj.js +4 -0
- package/dist/chunks/quick-search-zm00h9dh.js +4 -0
- package/dist/chunks/references-99ws0b0m.js +4 -0
- package/dist/chunks/right-panel-pq2f71q3.js +4 -0
- package/dist/chunks/section-registry-b2yzp9sw.js +4 -0
- package/dist/chunks/studio-01kbbqg3.js +928 -0
- package/dist/chunks/{studio-hf4qdjc8.js → studio-01qza1n3.js} +4 -4
- package/dist/chunks/{studio-pkkw32j0.js → studio-03wwfvyt.js} +4 -4
- package/dist/chunks/{studio-7gqyhjns.js → studio-0bbxhhce.js} +4 -4
- package/dist/chunks/studio-0k9mvpgj.js +14 -0
- package/dist/chunks/studio-0scebvb9.js +6 -0
- package/dist/chunks/studio-1a99w39s.js +5 -0
- package/dist/chunks/studio-1wvfxkyy.js +5 -0
- package/dist/chunks/studio-1x8x26k3.js +5 -0
- package/dist/chunks/studio-222rqyyc.js +5 -0
- package/dist/chunks/studio-4xppwncr.js +4 -0
- package/dist/chunks/{studio-xcng8nx7.js → studio-54g077x3.js} +5 -5
- package/dist/chunks/studio-56z7evx4.js +10 -0
- package/dist/chunks/studio-5e9zsamh.js +9 -0
- package/dist/chunks/studio-65fhspdd.js +5 -0
- package/dist/chunks/studio-693nhyky.js +5 -0
- package/dist/chunks/{studio-7jazdy6y.js → studio-7pgy3xaf.js} +4 -4
- package/dist/chunks/studio-888knnkc.js +5 -0
- package/dist/chunks/studio-9mqfamps.js +5 -0
- package/dist/chunks/studio-a7xqhc3n.js +5 -0
- package/dist/chunks/studio-aje966s9.js +5 -0
- package/dist/chunks/studio-aqmph6k4.js +5 -0
- package/dist/chunks/studio-c6w3tq7t.js +5 -0
- package/dist/chunks/{studio-6j2jn55a.js → studio-c6zdm25z.js} +8 -8
- package/dist/chunks/{studio-f8jyp45j.js → studio-c9d3pxf7.js} +4 -4
- package/dist/chunks/studio-ceb34dxj.js +5 -0
- package/dist/chunks/studio-cvbkkfqb.js +5 -0
- package/dist/chunks/studio-cxt150pj.js +7 -0
- package/dist/chunks/studio-dkn3x3hz.js +5 -0
- package/dist/chunks/studio-e6re3rk1.js +5 -0
- package/dist/chunks/{studio-5n1km79v.js → studio-edbtvcd1.js} +4 -4
- package/dist/chunks/{studio-xc8ebsts.js → studio-eh0rac07.js} +4 -4
- package/dist/chunks/{studio-625ezz59.js → studio-evwgp1ym.js} +4 -4
- package/dist/chunks/{studio-he2fhrgp.js → studio-f195h2hq.js} +4 -4
- package/dist/chunks/studio-fqq890ap.js +4 -0
- package/dist/chunks/studio-frb0qbf7.js +6 -0
- package/dist/chunks/studio-gfn1kpxj.js +6 -0
- package/dist/chunks/studio-gh97gngt.js +5 -0
- package/dist/chunks/{studio-awqghqxv.js → studio-gp02nqnw.js} +4 -4
- package/dist/chunks/{studio-x9sagq2d.js → studio-hjbg8d5y.js} +7 -7
- package/dist/chunks/studio-j6gnhvaa.js +5 -0
- package/dist/chunks/studio-mwxgyn9h.js +7 -0
- package/dist/chunks/{studio-x5esgzct.js → studio-n78gjfy8.js} +4 -4
- package/dist/chunks/studio-nbxqcgvf.js +5 -0
- package/dist/chunks/{studio-p1eggxcf.js → studio-nt06yybj.js} +4 -4
- package/dist/chunks/{studio-hc9q4j24.js → studio-p7c1agz0.js} +5 -5
- package/dist/chunks/{studio-8r4y2x0z.js → studio-pwqfmt4p.js} +4 -4
- package/dist/chunks/{studio-5wfrgee9.js → studio-r3kaka4d.js} +4 -4
- package/dist/chunks/studio-semd1s8s.js +5 -0
- package/dist/chunks/studio-skzanb0k.js +5 -0
- package/dist/chunks/studio-spf2skgc.js +5 -0
- package/dist/chunks/{studio-b4epb26c.js → studio-st75kn99.js} +7 -7
- package/dist/chunks/{studio-eq27rrr0.js → studio-sxc0zvpt.js} +3 -3
- package/dist/chunks/studio-t1eedsyx.js +4 -0
- package/dist/chunks/{studio-73czgk6a.js → studio-tp642n5g.js} +4 -4
- package/dist/chunks/studio-tw7rre6s.js +5 -0
- package/dist/chunks/{studio-7by9a93k.js → studio-tzegmy4w.js} +4 -4
- package/dist/chunks/studio-vavmwshr.js +5 -0
- package/dist/chunks/studio-wma2c6g8.js +5 -0
- package/dist/chunks/studio-x92tgnee.js +64 -0
- package/dist/chunks/studio-xk0z6msk.js +5 -0
- package/dist/chunks/{studio-c4pnt1c6.js → studio-xry7tk3z.js} +5 -5
- package/dist/chunks/studio-xta2vxp6.js +9 -0
- package/dist/chunks/{studio-21enxdr8.js → studio-xy3tv3az.js} +4 -4
- package/dist/chunks/studio-y7jehjrp.js +5 -0
- package/dist/chunks/{studio-k1eaqyzm.js → studio-yr04mv2j.js} +4 -4
- package/dist/chunks/{studio-mhhc9btt.js → studio-zd4kayn2.js} +4 -4
- package/dist/chunks/studio-zpnk016a.js +16 -0
- package/dist/chunks/{tsMode-rtwt7w1a.js → tsMode-k9s9fcag.js} +5 -5
- package/dist/chunks/workspace-b69mgm0e.js +4 -0
- package/dist/chunks/ws-client-0ja08haa.js +4 -0
- package/dist/iframe-entry.js +14 -14
- package/dist/manifest.json +62 -59
- package/dist/studio.css +1 -1
- package/dist/studio.js +121 -123
- package/package.json +5 -5
- package/src/account/action-flow.ts +294 -0
- package/src/account/action-required.ts +68 -0
- package/src/account-status.ts +29 -1
- package/src/browse/library-model.ts +3 -1
- package/src/browse/library-preview.ts +6 -2
- package/src/canvas/asset-refs.ts +3 -1
- package/src/canvas/asset-resolve.ts +79 -1
- package/src/canvas/iframe-entry.ts +38 -1
- package/src/canvas/iframe-host.ts +85 -0
- package/src/canvas/iframe-protocol.ts +13 -0
- package/src/canvas/stage-scrollbar.ts +7 -7
- package/src/files/asset-versions.ts +294 -0
- package/src/files/file-ops.ts +5 -8
- package/src/files/files.ts +61 -9
- package/src/files/fs-events.ts +279 -28
- package/src/files/media-meta.ts +2 -1
- package/src/files/media-paths.ts +5 -2
- package/src/files/media-upload.ts +5 -1
- package/src/new-project/add-repo-modal.ts +12 -1
- package/src/new-project/new-project-modal.ts +12 -1
- package/src/panels/git-panel.ts +25 -2
- package/src/panels/jump-bar.ts +132 -158
- package/src/panels/pane-context.ts +8 -8
- package/src/panels/pane-grid.ts +18 -26
- package/src/panels/style-panel.ts +2 -2
- package/src/platform-errors.ts +61 -1
- package/src/platforms/cloud.ts +389 -50
- package/src/platforms/devserver.ts +17 -1
- package/src/publish/pages-service.ts +17 -7
- package/src/services/ai-models.ts +30 -6
- package/src/services/ai-system-prompt.ts +81 -51
- package/src/services/context-manager.ts +41 -70
- package/src/services/document-assistant.ts +12 -3
- package/src/services/harness/chat-reducer.ts +115 -0
- package/src/services/harness/turn-hooks.ts +50 -0
- package/src/services/tool-executor.ts +69 -288
- package/src/services/tool-outcomes.ts +12 -10
- package/src/settings/preferences-accounts.ts +42 -3
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/studio.ts +20 -6
- package/src/surfaces/block-action-bar.json +6 -1
- package/src/surfaces/dialog.ts +5 -0
- package/src/surfaces/jump-bar.json +9 -12
- package/src/surfaces/jump-bar.ts +15 -16
- package/src/surfaces/pane-context.json +2 -2
- package/src/surfaces/pane-grid.json +8 -25
- package/src/surfaces/pane-grid.ts +7 -8
- package/src/surfaces/statusbar.json +71 -3
- package/src/surfaces/statusbar.ts +66 -139
- package/src/surfaces/style-panel.json +3 -3
- package/src/surfaces/style-panel.ts +11 -1
- package/src/surfaces/welcome.json +86 -1
- package/src/surfaces/welcome.ts +61 -2
- package/src/types.ts +84 -2
- package/src/ui/ai-managed-connect.ts +41 -4
- package/src/ui/media-picker.ts +2 -1
- package/dist/chunks/adapter-bsbs78x9.js +0 -4
- package/dist/chunks/cf-account-picker-gnm7xeqb.js +0 -4
- package/dist/chunks/cf-settings-9y6jyscp.js +0 -4
- package/dist/chunks/components-s51h3b7c.js +0 -4
- package/dist/chunks/content-source-ty3be0qg.js +0 -4
- package/dist/chunks/convert-to-repeater-c3e7q61z.js +0 -4
- package/dist/chunks/editor-t002b41m.js +0 -4
- package/dist/chunks/entry-commands-sb72e9gy.js +0 -4
- package/dist/chunks/extension-sections-ktn2ntnj.js +0 -4
- package/dist/chunks/file-ops-78zeytkm.js +0 -4
- package/dist/chunks/files-gatzrz8v.js +0 -4
- package/dist/chunks/format-host-r59b1651.js +0 -4
- package/dist/chunks/fs-events-f8h91ndt.js +0 -4
- package/dist/chunks/git-panel-y12zf7nj.js +0 -4
- package/dist/chunks/grid-view-rfd8b2pg.js +0 -51
- package/dist/chunks/grid-view-tafenfm2.css +0 -1
- package/dist/chunks/iframe-host-vsrsj20g.js +0 -4
- package/dist/chunks/index-5aghyrfh.js +0 -4
- package/dist/chunks/jx-validate-653krkrf.js +0 -4
- package/dist/chunks/layers-32p8gtae.js +0 -4
- package/dist/chunks/layers-panel-rs58m105.js +0 -4
- package/dist/chunks/lit-html-54bmkwz3.js +0 -4
- package/dist/chunks/media-picker-5bb04wct.js +0 -4
- package/dist/chunks/menu-cbha7w2e.js +0 -4
- package/dist/chunks/monaco-buffer-9vbbyfr1.js +0 -4
- package/dist/chunks/notify-dc1nz5ar.js +0 -4
- package/dist/chunks/platform-2m49s1zf.js +0 -4
- package/dist/chunks/publish-panel-nx3ycmma.js +0 -4
- package/dist/chunks/quick-search-yzrzh9x3.js +0 -4
- package/dist/chunks/references-vx7mbj9f.js +0 -4
- package/dist/chunks/right-panel-7jzpbxja.js +0 -4
- package/dist/chunks/section-registry-gjryse41.js +0 -4
- package/dist/chunks/studio-3znfhcxg.js +0 -5
- package/dist/chunks/studio-58qam27v.js +0 -4
- package/dist/chunks/studio-6j59fwaw.js +0 -5
- package/dist/chunks/studio-7z69g1gx.js +0 -5
- package/dist/chunks/studio-93j3v6zx.js +0 -5
- package/dist/chunks/studio-9gf6v8zy.js +0 -5
- package/dist/chunks/studio-9rhmettf.js +0 -4
- package/dist/chunks/studio-ac5aq677.js +0 -10
- package/dist/chunks/studio-as65vbns.js +0 -5
- package/dist/chunks/studio-bkv0sga4.js +0 -6
- package/dist/chunks/studio-bttmqef4.js +0 -5
- package/dist/chunks/studio-c3y9d41k.js +0 -5
- package/dist/chunks/studio-ccegp7gz.js +0 -9
- package/dist/chunks/studio-chxabn98.js +0 -6
- package/dist/chunks/studio-cktcdsxz.js +0 -16
- package/dist/chunks/studio-cnspfke5.js +0 -5
- package/dist/chunks/studio-de3p1ktm.js +0 -5
- package/dist/chunks/studio-fsbcn530.js +0 -928
- package/dist/chunks/studio-gftga7a3.js +0 -64
- package/dist/chunks/studio-hf1rpcq0.js +0 -4
- package/dist/chunks/studio-hz02hq70.js +0 -5
- package/dist/chunks/studio-jnb285we.js +0 -5
- package/dist/chunks/studio-jsv3s3s1.js +0 -5
- package/dist/chunks/studio-ks7cksh0.js +0 -5
- package/dist/chunks/studio-nptx0dbs.js +0 -5
- package/dist/chunks/studio-pg0z7k98.js +0 -5
- package/dist/chunks/studio-pve0b2f2.js +0 -5
- package/dist/chunks/studio-q7zmjv3b.js +0 -6
- package/dist/chunks/studio-rpmvw7kj.js +0 -5
- package/dist/chunks/studio-rwhss52s.js +0 -5
- package/dist/chunks/studio-sbg3g3h0.js +0 -5
- package/dist/chunks/studio-sq1bn60g.js +0 -14
- package/dist/chunks/studio-vwz7rma2.js +0 -5
- package/dist/chunks/studio-wa55e8gf.js +0 -5
- package/dist/chunks/studio-x0mzdha2.js +0 -5
- package/dist/chunks/studio-xrgpakvc.js +0 -9
- package/dist/chunks/studio-xt3djk1w.js +0 -7
- package/dist/chunks/studio-yh3nggx1.js +0 -7
- package/dist/chunks/studio-zmecp6hq.js +0 -5
- package/dist/chunks/workspace-g6dvaete.js +0 -4
- package/dist/chunks/ws-client-shf41j5p.js +0 -4
package/src/files/fs-events.ts
CHANGED
|
@@ -6,21 +6,49 @@
|
|
|
6
6
|
* without a DOM). `startFsSync` is the only impure part: it feature-detects the platform's
|
|
7
7
|
* `subscribeFileEvents`, debounces bursts, drops the echoes of the user's own local mutations, and
|
|
8
8
|
* triggers a single re-render through the existing left-panel render path (no imperative DOM).
|
|
9
|
+
*
|
|
10
|
+
* It also answers the transport's `onResync`, which is the platform admitting that its event stream
|
|
11
|
+
* has a gap (a reconnect, or the stream's first open after listings were made) or never carried the
|
|
12
|
+
* change (a commit). Events are deltas; a gap in deltas cannot be patched by more deltas, so a
|
|
13
|
+
* resync re-reads what this window has cached.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { getPlatform } from "../platform";
|
|
12
17
|
import { reloadIgnoreCache, touchesGitignore } from "./gitignore";
|
|
13
18
|
import { invalidateUsages } from "../services/references";
|
|
19
|
+
import { forgetAllVersions, forgetVersions, notedListing } from "./asset-versions";
|
|
20
|
+
import { isCollabPath } from "../collab/collab-state";
|
|
14
21
|
import { projectState } from "../store";
|
|
15
|
-
import
|
|
22
|
+
import { workspace } from "../workspace/workspace";
|
|
23
|
+
import type { DirEntry, FsEvent, FsResyncReason, StudioPlatform } from "../types";
|
|
16
24
|
|
|
17
25
|
const RECENT_MS = 1500;
|
|
26
|
+
/**
|
|
27
|
+
* How wide each resync reason's pass is: a coalesced burst runs the widest one it saw. A reconnect
|
|
28
|
+
* re-derives everything; an open forgets the content versions and re-lists; a commit re-lists.
|
|
29
|
+
*/
|
|
30
|
+
const RESYNC_RANK: Record<FsResyncReason, number> = { commit: 0, open: 1, reconnect: 2 };
|
|
31
|
+
/**
|
|
32
|
+
* How long a resync request waits for company. A flapping network reconnects several times in a
|
|
33
|
+
* second, and a commit notice can arrive on the heels of a reconnect; each resync re-lists every
|
|
34
|
+
* loaded directory, so a burst is coalesced into one pass.
|
|
35
|
+
*/
|
|
36
|
+
const RESYNC_DEBOUNCE_MS = 250;
|
|
37
|
+
/** Listings in flight at once during a resync — a deep expanded tree is many round trips. */
|
|
38
|
+
const RELIST_CONCURRENCY = 8;
|
|
18
39
|
const recentLocal = new Map<string, number>();
|
|
19
40
|
|
|
20
41
|
const norm = (p: string) => p.replaceAll("\\", "/");
|
|
21
42
|
|
|
22
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Mark paths the user just mutated locally so the watcher's echo of them is ignored briefly.
|
|
45
|
+
*
|
|
46
|
+
* Their content versions are forgotten too, each path as a file AND as a directory (a rename or a
|
|
47
|
+
* delete may name either): the bytes are about to change, and a version that outlived them would
|
|
48
|
+
* let an immutable cache serve the old ones.
|
|
49
|
+
*/
|
|
23
50
|
export function markLocalMutation(...paths: string[]): void {
|
|
51
|
+
forgetVersions(paths, paths);
|
|
24
52
|
const expiry = Date.now() + RECENT_MS;
|
|
25
53
|
for (const p of paths) {
|
|
26
54
|
if (p) {
|
|
@@ -114,6 +142,146 @@ export function applyFsEvents(
|
|
|
114
142
|
return changedDirs;
|
|
115
143
|
}
|
|
116
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Forget the content versions every event in a batch could have invalidated: the file an `add`,
|
|
147
|
+
* `change` or `unlink` names, and everything under a removed directory.
|
|
148
|
+
*/
|
|
149
|
+
export function forgetEventVersions(events: readonly FsEvent[]): void {
|
|
150
|
+
const paths: string[] = [];
|
|
151
|
+
const dirs: string[] = [];
|
|
152
|
+
for (const event of events) {
|
|
153
|
+
if (event.type === "unlinkDir") {
|
|
154
|
+
dirs.push(event.path);
|
|
155
|
+
} else if (event.type !== "addDir") {
|
|
156
|
+
paths.push(event.path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (paths.length > 0 || dirs.length > 0) {
|
|
160
|
+
forgetVersions(paths, dirs);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether a cached listing still holds exactly the entries a snapshot of it took. */
|
|
165
|
+
function sameEntries(current: DirEntry[] | undefined, snapshot: DirEntry[]): boolean {
|
|
166
|
+
return (
|
|
167
|
+
current !== undefined &&
|
|
168
|
+
current.length === snapshot.length &&
|
|
169
|
+
current.every((entry, index) => entry === snapshot[index])
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Reads of one directory a resync makes before it lets newer changes have the last word. */
|
|
174
|
+
const RELIST_ATTEMPTS = 2;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Re-list every directory `dirs` has cached and replace its entries in place.
|
|
178
|
+
*
|
|
179
|
+
* Only directories already in the cache are read: the tree's lazy loading is the authority on what
|
|
180
|
+
* is worth fetching, and a resync that grew the cache would turn one reconnect into a crawl of the
|
|
181
|
+
* project. A directory whose listing FAILS is dropped rather than kept — after a gap in the event
|
|
182
|
+
* stream, "this directory vanished" is the likeliest reason, and a stale listing of a directory
|
|
183
|
+
* that no longer exists is the very thing a resync exists to remove. Expanding it again re-reads
|
|
184
|
+
* it.
|
|
185
|
+
*
|
|
186
|
+
* Live events keep landing while the listings are in flight, and Studio's own create / rename /
|
|
187
|
+
* delete re-load their parent, so a listing can come back OLDER than the cache it would replace:
|
|
188
|
+
* nothing orders an HTTP response against a message on the event socket. Each directory is
|
|
189
|
+
* therefore snapshotted before its read and the listing applied only if the cache still matches the
|
|
190
|
+
* snapshot. A directory that moved is read again — that read starts after the change, so it
|
|
191
|
+
* includes it — and if it moves again under the second read the cache keeps what the newer changes
|
|
192
|
+
* made of it. Identity alone could not tell: `applyFsEvents` edits a listing in place. For the same
|
|
193
|
+
* reason a directory that left the cache mid-read (an `unlinkDir`) stays gone: the event is newer
|
|
194
|
+
* than the read.
|
|
195
|
+
*/
|
|
196
|
+
export async function relistLoadedDirs(
|
|
197
|
+
dirs: Map<string, DirEntry[]>,
|
|
198
|
+
listDirectory: StudioPlatform["listDirectory"],
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const keys = [...dirs.keys()];
|
|
201
|
+
let next = 0;
|
|
202
|
+
const relist = async (dir: string) => {
|
|
203
|
+
for (let attempt = 0; attempt < RELIST_ATTEMPTS; attempt += 1) {
|
|
204
|
+
const before = dirs.get(dir);
|
|
205
|
+
if (!before) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const snapshot = [...before];
|
|
209
|
+
let entries: DirEntry[] | null;
|
|
210
|
+
try {
|
|
211
|
+
entries = await listDirectory(dir);
|
|
212
|
+
} catch {
|
|
213
|
+
entries = null;
|
|
214
|
+
}
|
|
215
|
+
if (!dirs.has(dir)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!sameEntries(dirs.get(dir), snapshot)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (entries) {
|
|
222
|
+
dirs.set(dir, entries);
|
|
223
|
+
} else {
|
|
224
|
+
dirs.delete(dir);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const worker = async () => {
|
|
230
|
+
while (next < keys.length) {
|
|
231
|
+
const dir = keys[next]!;
|
|
232
|
+
next += 1;
|
|
233
|
+
await relist(dir);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
await Promise.all(Array.from({ length: Math.min(RELIST_CONCURRENCY, keys.length) }, worker));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Modes only a tab whose `document` is a placeholder offers: the media viewer, a CSV grid, the
|
|
241
|
+
* Library. Spelled out as strings, not imported, because each constant's module drags its whole
|
|
242
|
+
* editor in behind it.
|
|
243
|
+
*/
|
|
244
|
+
const STUB_DOCUMENT_MODES = new Set(["media", "grid", "manage"]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether a tab's `document` is a placeholder rather than its parsed file. Such a tab reads its
|
|
248
|
+
* file through its own channel, if at all, so a re-read through `readFile` has nothing to refresh —
|
|
249
|
+
* and is worse than waste: the cloud session's `/file` route refuses a binary file (415), and the
|
|
250
|
+
* failed reload would raise "could not reload" for a file that never changed. A git comparison stub
|
|
251
|
+
* is recognised by offering `git-diff` and NOTHING else, since every document tab offers it too.
|
|
252
|
+
*/
|
|
253
|
+
function holdsPlaceholder(modes: string[]): boolean {
|
|
254
|
+
return (
|
|
255
|
+
modes.some((mode) => STUB_DOCUMENT_MODES.has(mode)) ||
|
|
256
|
+
(modes.length > 0 && modes.every((mode) => mode === "git-diff"))
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The open tabs a reconnect may refresh from disk: clean ones holding a parsed document, and none
|
|
262
|
+
* that are co-edited.
|
|
263
|
+
*
|
|
264
|
+
* A dirty tab holds the author's unsaved work, which a reload would discard. A co-edited one never
|
|
265
|
+
* reloads from disk at all — its shared document is ahead of whatever the provider wrote back (the
|
|
266
|
+
* same rule `reloadCleanTab` applies to a single event). A stub tab has no parsed document to
|
|
267
|
+
* refresh ({@link holdsPlaceholder}).
|
|
268
|
+
*/
|
|
269
|
+
export function cleanOpenTabPaths(): string[] {
|
|
270
|
+
const paths = new Set<string>();
|
|
271
|
+
for (const tab of workspace.tabs.values()) {
|
|
272
|
+
const path = tab.documentPath;
|
|
273
|
+
if (
|
|
274
|
+
path &&
|
|
275
|
+
!tab.doc.dirty &&
|
|
276
|
+
!isCollabPath(path) &&
|
|
277
|
+
!holdsPlaceholder(tab.capabilities.modes)
|
|
278
|
+
) {
|
|
279
|
+
paths.add(path);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return [...paths];
|
|
283
|
+
}
|
|
284
|
+
|
|
117
285
|
export interface FsSyncContext {
|
|
118
286
|
renderLeftPanel: () => void;
|
|
119
287
|
/** Optional hook for an external content change to an open file (e.g. reload a clean tab). */
|
|
@@ -127,6 +295,12 @@ export interface FsSyncContext {
|
|
|
127
295
|
* scope.
|
|
128
296
|
*/
|
|
129
297
|
invalidateDerivedCaches?: () => void;
|
|
298
|
+
/**
|
|
299
|
+
* After a reconnect, the clean, non-collab open tabs whose files may have changed while the
|
|
300
|
+
* stream was down. The handler re-reads them; it should leave a tab whose content did not change
|
|
301
|
+
* alone, since a reconnect usually changed nothing and a reload churns undo and repaints.
|
|
302
|
+
*/
|
|
303
|
+
onResyncContent?: (paths: string[]) => void;
|
|
130
304
|
}
|
|
131
305
|
|
|
132
306
|
/**
|
|
@@ -141,6 +315,8 @@ export function startFsSync(ctx: FsSyncContext): () => void {
|
|
|
141
315
|
}
|
|
142
316
|
let pending: FsEvent[] = [];
|
|
143
317
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
318
|
+
let resyncReason: FsResyncReason | null = null;
|
|
319
|
+
let resyncTimer: ReturnType<typeof setTimeout> | null = null;
|
|
144
320
|
|
|
145
321
|
const flush = () => {
|
|
146
322
|
timer = null;
|
|
@@ -163,30 +339,105 @@ export function startFsSync(ctx: FsSyncContext): () => void {
|
|
|
163
339
|
}
|
|
164
340
|
};
|
|
165
341
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
342
|
+
/*
|
|
343
|
+
* A reconnect lost an unknown set of events, so it re-derives everything the events would have
|
|
344
|
+
* kept current: the caches keyed on which files exist (dropped, exactly as one event drops them),
|
|
345
|
+
* the `.gitignore` rules, every cached listing, and the open documents. A commit lost nothing —
|
|
346
|
+
* the DO simply sends no per-file events for one — so it re-reads the listings alone. An open is
|
|
347
|
+
* the stream going live after the first listings were already made; it re-reads them too, since
|
|
348
|
+
* a change broadcast before the socket joined reached nobody. (Each gap's content versions were
|
|
349
|
+
* already forgotten when it was reported — see `requestResync`.)
|
|
350
|
+
*/
|
|
351
|
+
const resync = async () => {
|
|
352
|
+
resyncTimer = null;
|
|
353
|
+
const reason = resyncReason;
|
|
354
|
+
resyncReason = null;
|
|
355
|
+
const state = projectState;
|
|
356
|
+
if (!state || !reason) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
/* Noted, so a commit — which makes dirty files clean — hands their versions back, and a gap's
|
|
360
|
+
re-list vouches again for the tree's share of what it forgot. */
|
|
361
|
+
const relist = relistLoadedDirs(state.dirs, (dir) =>
|
|
362
|
+
notedListing(() => platform.listDirectory(dir)),
|
|
363
|
+
);
|
|
364
|
+
if (reason === "reconnect") {
|
|
365
|
+
invalidateUsages();
|
|
366
|
+
ctx.invalidateDerivedCaches?.();
|
|
367
|
+
await Promise.all([relist, reloadIgnoreCache()]);
|
|
368
|
+
} else {
|
|
369
|
+
await relist;
|
|
370
|
+
}
|
|
371
|
+
// The project changed under the listings: they belong to a tree nobody is drawing any more.
|
|
372
|
+
if (projectState !== state) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (reason === "reconnect") {
|
|
376
|
+
ctx.onResyncContent?.(cleanOpenTabPaths());
|
|
377
|
+
}
|
|
378
|
+
ctx.renderLeftPanel();
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const requestResync = (reason: FsResyncReason) => {
|
|
382
|
+
/* A gap in the stream — a reconnect, or the first open — forgets EVERY content version, at once
|
|
383
|
+
rather than after the debounce: the events that would have forgotten single paths are gone,
|
|
384
|
+
and many versions were learned outside the tree (the Library walk, the media picker, media
|
|
385
|
+
metadata), where no re-list of the loaded directories reaches. A version Studio can no longer
|
|
386
|
+
vouch for may be answered from an immutable cache with the old bytes, so it cannot wait. The
|
|
387
|
+
floor this raises also discards any listing still in flight across the gap. The re-list
|
|
388
|
+
below, and every later listing, vouch afresh. A commit loses no events, so it keeps them. */
|
|
389
|
+
if (reason !== "commit") {
|
|
390
|
+
forgetAllVersions();
|
|
391
|
+
}
|
|
392
|
+
// A wider pass includes a narrower one's, so the widest reason wins a coalesced burst.
|
|
393
|
+
if (!resyncReason || RESYNC_RANK[reason] > RESYNC_RANK[resyncReason]) {
|
|
394
|
+
resyncReason = reason;
|
|
395
|
+
}
|
|
396
|
+
if (resyncTimer) {
|
|
397
|
+
clearTimeout(resyncTimer);
|
|
398
|
+
}
|
|
399
|
+
resyncTimer = setTimeout(() => {
|
|
400
|
+
void resync();
|
|
401
|
+
}, RESYNC_DEBOUNCE_MS);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const unsubscribe = platform.subscribeFileEvents(
|
|
405
|
+
(events) => {
|
|
406
|
+
// Before the echo filter, deliberately. `isRecentLocal` drops the events Studio caused, which
|
|
407
|
+
// Is right for the tree (it already repainted) and wrong for a DERIVED cache — Studio's own
|
|
408
|
+
// Write changes what the project contains exactly as much as anyone else's does.
|
|
409
|
+
//
|
|
410
|
+
// Every cache keyed on "what files exist" is dropped here, in one place, because they answer
|
|
411
|
+
// One event. Each one had an invalidator and no caller, and each stale answer is visible: the
|
|
412
|
+
// Link-target picker offering a route whose page was deleted, the layout picker attributing a
|
|
413
|
+
// Removed layout's `$head` to the open page, a `$paths` enumeration listing entries that are
|
|
414
|
+
// Gone.
|
|
415
|
+
invalidateUsages();
|
|
416
|
+
ctx.invalidateDerivedCaches?.();
|
|
417
|
+
// Content versions are derived state of the same kind, and Studio's own writes change bytes:
|
|
418
|
+
// A version kept past its echo would let an immutable cache answer with the old file.
|
|
419
|
+
forgetEventVersions(events);
|
|
420
|
+
/* A `.gitignore` governs the whole tree beneath it, so it is handled HERE rather than in the
|
|
421
|
+
batch below: the echo filter drops the events Studio caused, and an author who just edited
|
|
422
|
+
their own `.gitignore` in Studio is precisely the one waiting to see the tree change. It also
|
|
423
|
+
never reaches `applyFsEvents` — a dotfile is in no listing, so no directory reads as changed
|
|
424
|
+
and no repaint would follow. Hence the explicit one, once the rules are back. */
|
|
425
|
+
if (touchesGitignore(events.map((event) => event.path))) {
|
|
426
|
+
void reloadIgnoreCache().then(() => ctx.renderLeftPanel());
|
|
427
|
+
}
|
|
428
|
+
pending.push(...events);
|
|
429
|
+
if (timer) {
|
|
430
|
+
clearTimeout(timer);
|
|
431
|
+
}
|
|
432
|
+
timer = setTimeout(flush, 50);
|
|
433
|
+
},
|
|
434
|
+
{ onResync: requestResync },
|
|
435
|
+
);
|
|
436
|
+
return () => {
|
|
437
|
+
if (resyncTimer) {
|
|
438
|
+
clearTimeout(resyncTimer);
|
|
439
|
+
resyncTimer = null;
|
|
440
|
+
}
|
|
441
|
+
unsubscribe();
|
|
442
|
+
};
|
|
192
443
|
}
|
package/src/files/media-meta.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import { getPlatform } from "../platform";
|
|
27
27
|
import { baseName, dirName, normalizeProjectPath } from "./media-paths";
|
|
28
|
+
import { notedListing } from "./asset-versions";
|
|
28
29
|
import { extensionOf, mediaKind } from "./media-upload";
|
|
29
30
|
import type { AssetKind } from "./media-upload";
|
|
30
31
|
import type { DirEntry } from "../types";
|
|
@@ -126,7 +127,7 @@ export async function loadMediaMeta(path: string): Promise<MediaMeta> {
|
|
|
126
127
|
const pending = (async (): Promise<MediaMeta> => {
|
|
127
128
|
let entry: DirEntry | undefined;
|
|
128
129
|
try {
|
|
129
|
-
const listing = await getPlatform().listDirectory(dirName(key));
|
|
130
|
+
const listing = await notedListing(() => getPlatform().listDirectory(dirName(key)));
|
|
130
131
|
entry = listing.find((item) => normalizeProjectPath(item.path) === key);
|
|
131
132
|
} catch {
|
|
132
133
|
// Unreadable or not yet created — the size stays unknown rather than becoming zero.
|
package/src/files/media-paths.ts
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
|
|
39
39
|
import { encodeProjectPath, normalizeProjectPath, PUBLIC_DIR } from "@jxsuite/schema/asset-paths";
|
|
40
40
|
import { contentMountFor, hostAssetDeclarations, projectLocales } from "../canvas/asset-refs";
|
|
41
|
+
import { withVersion } from "../canvas/asset-resolve";
|
|
42
|
+
import { versionOf } from "./asset-versions";
|
|
41
43
|
import { loopbackAssetSrc } from "../canvas/canvas-origin";
|
|
42
44
|
import { projectState } from "../store";
|
|
43
45
|
import type { AssetMount } from "@jxsuite/schema/asset-paths";
|
|
@@ -126,10 +128,11 @@ export function previewFileSrc(path: string): string {
|
|
|
126
128
|
return path;
|
|
127
129
|
}
|
|
128
130
|
/* In repo space nothing answers a site URL, so the site URL is the wrong question entirely: the
|
|
129
|
-
host serves the FILE, at its own path, under the declared base
|
|
131
|
+
host serves the FILE, at its own path, under the declared base — versioned when a listing
|
|
132
|
+
vouched for its bytes, so a host that recognizes the version can let the browser cache it. */
|
|
130
133
|
const { space, fileBaseUrl } = hostAssetDeclarations();
|
|
131
134
|
if (space === "repo") {
|
|
132
|
-
return `${fileBaseUrl}${encodeProjectPath(normalized)}
|
|
135
|
+
return withVersion(`${fileBaseUrl}${encodeProjectPath(normalized)}`, versionOf(normalized));
|
|
133
136
|
}
|
|
134
137
|
return loopbackAssetSrc(mediaSiteUrl(normalized));
|
|
135
138
|
}
|
|
@@ -20,6 +20,7 @@ import { errorMessage } from "@jxsuite/schema/parse";
|
|
|
20
20
|
import { getPlatform, hasPlatform } from "../platform";
|
|
21
21
|
import { notify } from "../services/notify";
|
|
22
22
|
import { activeTab } from "../workspace/workspace";
|
|
23
|
+
import { forgetVersions, notedListing } from "./asset-versions";
|
|
23
24
|
|
|
24
25
|
// ─── File classification ─────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -245,7 +246,8 @@ export async function uploadAssets(
|
|
|
245
246
|
|
|
246
247
|
let taken: Set<string>;
|
|
247
248
|
try {
|
|
248
|
-
|
|
249
|
+
// The upload directory is a media directory, so its listing vouches for versions as well.
|
|
250
|
+
const existing = await notedListing(() => platform.listDirectory(dir));
|
|
249
251
|
taken = new Set(existing.map((entry) => entry.name));
|
|
250
252
|
} catch {
|
|
251
253
|
taken = new Set(); // Directory doesn't exist yet — the backend mkdir -p's on write.
|
|
@@ -273,6 +275,8 @@ export async function uploadAssets(
|
|
|
273
275
|
hash, appends a collision suffix, or normalizes a name writes somewhere else, and the
|
|
274
276
|
reference that goes into the document has to name what is really there. */
|
|
275
277
|
const { path } = await platform.uploadFile(requested, file);
|
|
278
|
+
// New bytes at `path`: whatever version a listing held for it no longer names them.
|
|
279
|
+
forgetVersions([path]);
|
|
276
280
|
uploaded.push({ kind: mediaKind(file), name, path, ref: assetRef(path) });
|
|
277
281
|
} catch (error) {
|
|
278
282
|
notify.error(`Could not upload ${file.name}.`, {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* closer nothing in the app called was left over from drawing the box by hand.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { actIfRequired } from "../account/action-flow";
|
|
34
35
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
35
36
|
import {
|
|
36
37
|
getAccountStatus,
|
|
@@ -167,7 +168,11 @@ function settle(result: { root: string } | null): void {
|
|
|
167
168
|
handle?.close();
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Adopt the picked repository. A refusal that offers an action (desktop.md §10.4) is offered, and
|
|
173
|
+
* an action that is done runs the import again, once.
|
|
174
|
+
*/
|
|
175
|
+
async function chooseRepo(fullName: string, retried = false): Promise<void> {
|
|
171
176
|
if (_importing) {
|
|
172
177
|
return;
|
|
173
178
|
}
|
|
@@ -187,6 +192,12 @@ async function chooseRepo(fullName: string) {
|
|
|
187
192
|
_error = "This platform cannot import repositories.";
|
|
188
193
|
} catch (error) {
|
|
189
194
|
_error = errorMessage(error);
|
|
195
|
+
_importing = "";
|
|
196
|
+
redraw();
|
|
197
|
+
if (!retried && (await actIfRequired(error))) {
|
|
198
|
+
await chooseRepo(fullName, true);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
190
201
|
}
|
|
191
202
|
_importing = "";
|
|
192
203
|
redraw();
|
|
@@ -44,6 +44,7 @@ import { enumArg, enumProperty } from "../commands/command-args";
|
|
|
44
44
|
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
45
45
|
import { getPlatform } from "../platform";
|
|
46
46
|
import { installUrlOf } from "../platform-errors";
|
|
47
|
+
import { actIfRequired } from "../account/action-flow";
|
|
47
48
|
import { hasAiCredentials } from "../services/ai-models";
|
|
48
49
|
import { setPendingAgentPrompt } from "../services/agent-seed";
|
|
49
50
|
import { adoptCreatedProject } from "../services/project-adoption";
|
|
@@ -500,10 +501,17 @@ function goBack(): void {
|
|
|
500
501
|
redraw();
|
|
501
502
|
}
|
|
502
503
|
|
|
503
|
-
/**
|
|
504
|
+
/**
|
|
505
|
+
* Create the project from the chosen source, then run `after` with the result.
|
|
506
|
+
*
|
|
507
|
+
* A refusal that offers an action (desktop.md §10.4) is offered over the modal, and an action that
|
|
508
|
+
* is done runs the create again — once, so a platform that still refuses afterwards leaves its
|
|
509
|
+
* error standing rather than asking forever.
|
|
510
|
+
*/
|
|
504
511
|
async function create(
|
|
505
512
|
source: { starter: string } | { template: string },
|
|
506
513
|
after: (result: { root: string; config: ProjectConfig }) => void | Promise<void>,
|
|
514
|
+
retried = false,
|
|
507
515
|
): Promise<void> {
|
|
508
516
|
const destination = validateParams();
|
|
509
517
|
if (!destination) {
|
|
@@ -522,6 +530,9 @@ async function create(
|
|
|
522
530
|
_creating = false;
|
|
523
531
|
captureError(error);
|
|
524
532
|
redraw();
|
|
533
|
+
if (!retried && (await actIfRequired(error))) {
|
|
534
|
+
await create(source, after, true);
|
|
535
|
+
}
|
|
525
536
|
}
|
|
526
537
|
}
|
|
527
538
|
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -342,8 +342,9 @@ export async function signInToGithub(): Promise<void> {
|
|
|
342
342
|
/**
|
|
343
343
|
* @param {string} action
|
|
344
344
|
* @param {unknown} [body]
|
|
345
|
+
* @returns {Promise<boolean>} Whether the action succeeded — its failure is already on screen.
|
|
345
346
|
*/
|
|
346
|
-
async function gitAction(action: string, body?: unknown) {
|
|
347
|
+
async function gitAction(action: string, body?: unknown): Promise<boolean> {
|
|
347
348
|
const plat = getPlatform() as Record<string, (...args: unknown[]) => Promise<unknown>> &
|
|
348
349
|
StudioPlatform;
|
|
349
350
|
shell.git.loading = true;
|
|
@@ -351,9 +352,23 @@ async function gitAction(action: string, body?: unknown) {
|
|
|
351
352
|
try {
|
|
352
353
|
await plat[action]!(body);
|
|
353
354
|
await refreshGitStatus();
|
|
355
|
+
return true;
|
|
354
356
|
} catch (error) {
|
|
355
357
|
shell.git.error = errorMessage(error);
|
|
356
358
|
shell.git.loading = false;
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Put a message back in the field after the commit that took it failed — unless the user has
|
|
365
|
+
* already started typing another. The field is cleared before the call so a second click cannot
|
|
366
|
+
* commit the same text twice, which used to mean a refused commit (a conflict, a backend that asks
|
|
367
|
+
* for an action first) silently threw the message away with it.
|
|
368
|
+
*/
|
|
369
|
+
function restoreCommitMessage(message: string): void {
|
|
370
|
+
if (!shell.git.commitMessage) {
|
|
371
|
+
shell.git.commitMessage = message;
|
|
357
372
|
}
|
|
358
373
|
}
|
|
359
374
|
|
|
@@ -578,7 +593,9 @@ async function doCommit(): Promise<void> {
|
|
|
578
593
|
// Fold co-editing sessions into the backend's tree first so the commit never misses trailing
|
|
579
594
|
// Keystrokes (the mirror is debounced).
|
|
580
595
|
await flushAllCollab();
|
|
581
|
-
await gitAction("gitCommit", message)
|
|
596
|
+
if (!(await gitAction("gitCommit", message))) {
|
|
597
|
+
restoreCommitMessage(message);
|
|
598
|
+
}
|
|
582
599
|
}
|
|
583
600
|
|
|
584
601
|
/** Commit and push, as one operation with one loading state and one error. */
|
|
@@ -592,13 +609,19 @@ async function doCommitAndSync(): Promise<void> {
|
|
|
592
609
|
shell.git.error = null;
|
|
593
610
|
await flushAllCollab();
|
|
594
611
|
const plat = getPlatform();
|
|
612
|
+
let committed = false;
|
|
595
613
|
try {
|
|
596
614
|
await plat.gitCommit(message);
|
|
615
|
+
committed = true;
|
|
597
616
|
await plat.gitPush();
|
|
598
617
|
await refreshGitStatus();
|
|
599
618
|
} catch (error) {
|
|
600
619
|
shell.git.error = errorMessage(error);
|
|
601
620
|
shell.git.loading = false;
|
|
621
|
+
// A push that failed after the commit landed must not offer the same message again.
|
|
622
|
+
if (!committed) {
|
|
623
|
+
restoreCommitMessage(message);
|
|
624
|
+
}
|
|
602
625
|
}
|
|
603
626
|
}
|
|
604
627
|
|