@mindstudio-ai/remy 0.1.302 → 0.1.304
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/headless.d.ts +41 -7
- package/dist/headless.js +146 -62
- package/dist/index.js +146 -62
- package/dist/prompt/compiled/interfaces.md +23 -2
- package/dist/prompt/skills/publishing.md +1 -0
- package/dist/subagents/codeSanityCheck/prompt.md +1 -1
- package/package.json +1 -1
package/dist/headless.d.ts
CHANGED
|
@@ -63,6 +63,20 @@ declare class HeadlessSession {
|
|
|
63
63
|
* right state (the triggering turn's state, not a stale one).
|
|
64
64
|
*/
|
|
65
65
|
private currentOnboardingState;
|
|
66
|
+
/**
|
|
67
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
68
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
69
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
70
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
71
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
72
|
+
*
|
|
73
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
74
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
75
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
76
|
+
*/
|
|
77
|
+
private currentChainStep;
|
|
78
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
79
|
+
private chainRequestSeq;
|
|
66
80
|
/**
|
|
67
81
|
* Unified message queue. Holds pending work to deliver after the current
|
|
68
82
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -134,6 +148,14 @@ declare class HeadlessSession {
|
|
|
134
148
|
* filename de-dup set is per-call, so parallel calls race on names.
|
|
135
149
|
*/
|
|
136
150
|
private persistEntryAttachments;
|
|
151
|
+
/**
|
|
152
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
153
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
154
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
155
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
156
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
157
|
+
*/
|
|
158
|
+
private nextChainRequestId;
|
|
137
159
|
/**
|
|
138
160
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
139
161
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -213,9 +235,9 @@ declare class HeadlessSession {
|
|
|
213
235
|
/**
|
|
214
236
|
* Stop everything the user can see running: the turn, an in-flight
|
|
215
237
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
216
|
-
* follow-ups that belonged to the turn
|
|
217
|
-
* `source: 'user'` items
|
|
218
|
-
*
|
|
238
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
239
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
240
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
219
241
|
*
|
|
220
242
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
221
243
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -224,6 +246,13 @@ declare class HeadlessSession {
|
|
|
224
246
|
* and every additional press hit a turn that had just started. They now wait
|
|
225
247
|
* in the queue card until the user sends again or promotes one.
|
|
226
248
|
*
|
|
249
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
250
|
+
* away the rest of the build — including the step that finalizes it and
|
|
251
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
252
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
253
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
254
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
255
|
+
*
|
|
227
256
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
228
257
|
* message and outlives the turn that started it, so leaving it running means
|
|
229
258
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -236,10 +265,15 @@ declare class HeadlessSession {
|
|
|
236
265
|
*/
|
|
237
266
|
private handleCancel;
|
|
238
267
|
/**
|
|
239
|
-
* Remove pending queued messages
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
268
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
269
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
270
|
+
*
|
|
271
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
272
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
273
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
274
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
275
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
276
|
+
* before running it.)
|
|
243
277
|
*/
|
|
244
278
|
private handleCancelQueued;
|
|
245
279
|
private handleStdinLine;
|
package/dist/headless.js
CHANGED
|
@@ -7712,16 +7712,7 @@ function clearSession(state) {
|
|
|
7712
7712
|
log11.warn("Session archive on clear failed", { error: err.message });
|
|
7713
7713
|
}
|
|
7714
7714
|
state.messages = [];
|
|
7715
|
-
state
|
|
7716
|
-
try {
|
|
7717
|
-
if (fs21.existsSync(SESSION_FILE)) {
|
|
7718
|
-
fs21.unlinkSync(SESSION_FILE);
|
|
7719
|
-
}
|
|
7720
|
-
} catch (err) {
|
|
7721
|
-
log11.warn("Session clear: could not remove live file", {
|
|
7722
|
-
error: err.message
|
|
7723
|
-
});
|
|
7724
|
-
}
|
|
7715
|
+
saveSession(state);
|
|
7725
7716
|
}
|
|
7726
7717
|
|
|
7727
7718
|
// src/compaction/trigger.ts
|
|
@@ -9358,6 +9349,15 @@ var MessageQueue = class {
|
|
|
9358
9349
|
this.items.push(item);
|
|
9359
9350
|
this.onChange?.();
|
|
9360
9351
|
}
|
|
9352
|
+
/**
|
|
9353
|
+
* Add an item at the head. Used to put an interrupted chain step back in
|
|
9354
|
+
* front of the rest of its pipeline on cancel; `promoteToFront` can't serve
|
|
9355
|
+
* that case, since it addresses an item already in the queue by requestId.
|
|
9356
|
+
*/
|
|
9357
|
+
unshift(item) {
|
|
9358
|
+
this.items.unshift(item);
|
|
9359
|
+
this.onChange?.();
|
|
9360
|
+
}
|
|
9361
9361
|
/**
|
|
9362
9362
|
* Index of the first deliverable item, or -1 when there is none.
|
|
9363
9363
|
*
|
|
@@ -9426,25 +9426,6 @@ var MessageQueue = class {
|
|
|
9426
9426
|
}
|
|
9427
9427
|
return held;
|
|
9428
9428
|
}
|
|
9429
|
-
/**
|
|
9430
|
-
* Release held items so the normal drain picks them up again — all of them,
|
|
9431
|
-
* or one by command requestId. Fires onChange only if something changed.
|
|
9432
|
-
* Returns the released items.
|
|
9433
|
-
*/
|
|
9434
|
-
releaseHeld(id) {
|
|
9435
|
-
const released = [];
|
|
9436
|
-
for (const item of this.items) {
|
|
9437
|
-
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
9438
|
-
continue;
|
|
9439
|
-
}
|
|
9440
|
-
delete item.held;
|
|
9441
|
-
released.push(item);
|
|
9442
|
-
}
|
|
9443
|
-
if (released.length > 0) {
|
|
9444
|
-
this.onChange?.();
|
|
9445
|
-
}
|
|
9446
|
-
return released;
|
|
9447
|
-
}
|
|
9448
9429
|
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
9449
9430
|
hasDeliverable() {
|
|
9450
9431
|
return this.items.some((item) => !item.held);
|
|
@@ -9489,6 +9470,38 @@ var MessageQueue = class {
|
|
|
9489
9470
|
this.onChange?.();
|
|
9490
9471
|
return item;
|
|
9491
9472
|
}
|
|
9473
|
+
/**
|
|
9474
|
+
* Fold-in release: un-hold everything, then move items matching `defer` to
|
|
9475
|
+
* the tail, relative order preserved. One onChange for the whole
|
|
9476
|
+
* rearrangement, so no observer ever sees it half-applied.
|
|
9477
|
+
*
|
|
9478
|
+
* Callers push the new user message FIRST and call this second — "behind" is
|
|
9479
|
+
* defined by the array at the moment this runs. Held user messages stay put
|
|
9480
|
+
* so they still merge with that message into one mailbox batch; a paused
|
|
9481
|
+
* pipeline goes to the back, so the person's words run before the build
|
|
9482
|
+
* continues. `defer` is applied to every item, not just the newly-released
|
|
9483
|
+
* ones, so a chain step that was already deliverable defers too.
|
|
9484
|
+
*/
|
|
9485
|
+
releaseHeldDeferring(defer) {
|
|
9486
|
+
const released = [];
|
|
9487
|
+
for (const item of this.items) {
|
|
9488
|
+
if (!item.held) {
|
|
9489
|
+
continue;
|
|
9490
|
+
}
|
|
9491
|
+
delete item.held;
|
|
9492
|
+
released.push(item);
|
|
9493
|
+
}
|
|
9494
|
+
const back = this.items.filter(defer);
|
|
9495
|
+
const front = this.items.filter((item) => !defer(item));
|
|
9496
|
+
const reordered = back.length > 0 && front.length > 0;
|
|
9497
|
+
if (reordered) {
|
|
9498
|
+
this.items = [...front, ...back];
|
|
9499
|
+
}
|
|
9500
|
+
if (released.length > 0 || reordered) {
|
|
9501
|
+
this.onChange?.();
|
|
9502
|
+
}
|
|
9503
|
+
return released;
|
|
9504
|
+
}
|
|
9492
9505
|
/** Copy of current queue contents (for surfacing on events). */
|
|
9493
9506
|
snapshot() {
|
|
9494
9507
|
return [...this.items];
|
|
@@ -9538,6 +9551,20 @@ var HeadlessSession = class {
|
|
|
9538
9551
|
* right state (the triggering turn's state, not a stale one).
|
|
9539
9552
|
*/
|
|
9540
9553
|
currentOnboardingState;
|
|
9554
|
+
/**
|
|
9555
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
9556
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
9557
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
9558
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
9559
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
9560
|
+
*
|
|
9561
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
9562
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
9563
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
9564
|
+
*/
|
|
9565
|
+
currentChainStep = null;
|
|
9566
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
9567
|
+
chainRequestSeq = 0;
|
|
9541
9568
|
/**
|
|
9542
9569
|
* Unified message queue. Holds pending work to deliver after the current
|
|
9543
9570
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -10103,6 +10130,16 @@ var HeadlessSession = class {
|
|
|
10103
10130
|
return void 0;
|
|
10104
10131
|
}
|
|
10105
10132
|
}
|
|
10133
|
+
/**
|
|
10134
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
10135
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
10136
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
10137
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
10138
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
10139
|
+
*/
|
|
10140
|
+
nextChainRequestId() {
|
|
10141
|
+
return `chain-${Date.now()}-${++this.chainRequestSeq}`;
|
|
10142
|
+
}
|
|
10106
10143
|
/**
|
|
10107
10144
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
10108
10145
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -10140,36 +10177,42 @@ var HeadlessSession = class {
|
|
|
10140
10177
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
10141
10178
|
this.currentOnboardingState = onboardingState;
|
|
10142
10179
|
const system = buildSystemPrompt(onboardingState);
|
|
10180
|
+
this.currentChainStep = resolved && (resolved.next != null || fromChain) ? { text: rawText, onboardingState } : null;
|
|
10143
10181
|
if (resolved?.next && !fromChain) {
|
|
10144
10182
|
for (const step of getActionChain(resolved.next)) {
|
|
10145
10183
|
this.queue.push({
|
|
10146
10184
|
command: {
|
|
10147
10185
|
action: "message",
|
|
10148
10186
|
text: sentinel(step),
|
|
10149
|
-
onboardingState
|
|
10187
|
+
onboardingState,
|
|
10188
|
+
requestId: this.nextChainRequestId()
|
|
10150
10189
|
},
|
|
10151
10190
|
source: "chain",
|
|
10152
10191
|
enqueuedAt: Date.now()
|
|
10153
10192
|
});
|
|
10154
10193
|
}
|
|
10155
10194
|
}
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10195
|
+
try {
|
|
10196
|
+
await this.executeTurn({
|
|
10197
|
+
entries: [
|
|
10198
|
+
{
|
|
10199
|
+
text: userMessage,
|
|
10200
|
+
attachments,
|
|
10201
|
+
attachmentHeader,
|
|
10202
|
+
hidden: isHidden || void 0,
|
|
10203
|
+
requestId,
|
|
10204
|
+
queued: queued || void 0
|
|
10205
|
+
}
|
|
10206
|
+
],
|
|
10207
|
+
requestId,
|
|
10208
|
+
absorbedRids: [],
|
|
10209
|
+
onboardingState,
|
|
10210
|
+
system,
|
|
10211
|
+
buildModel
|
|
10212
|
+
});
|
|
10213
|
+
} finally {
|
|
10214
|
+
this.currentChainStep = null;
|
|
10215
|
+
}
|
|
10173
10216
|
}
|
|
10174
10217
|
/**
|
|
10175
10218
|
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
@@ -10182,6 +10225,7 @@ var HeadlessSession = class {
|
|
|
10182
10225
|
* with no side effects).
|
|
10183
10226
|
*/
|
|
10184
10227
|
async runMergedTurn(batch) {
|
|
10228
|
+
this.currentChainStep = null;
|
|
10185
10229
|
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
10186
10230
|
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
10187
10231
|
const entryList = [];
|
|
@@ -10336,9 +10380,6 @@ var HeadlessSession = class {
|
|
|
10336
10380
|
}
|
|
10337
10381
|
async handleMessage(parsed, requestId) {
|
|
10338
10382
|
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
10339
|
-
if (foldIn) {
|
|
10340
|
-
this.queue.releaseHeld();
|
|
10341
|
-
}
|
|
10342
10383
|
if (this.running || getInflightCompaction() || foldIn) {
|
|
10343
10384
|
const command = { ...parsed };
|
|
10344
10385
|
if (requestId && command.requestId === void 0) {
|
|
@@ -10349,6 +10390,9 @@ var HeadlessSession = class {
|
|
|
10349
10390
|
source: "user",
|
|
10350
10391
|
enqueuedAt: Date.now()
|
|
10351
10392
|
});
|
|
10393
|
+
if (foldIn) {
|
|
10394
|
+
this.queue.releaseHeldDeferring((item) => item.source === "chain");
|
|
10395
|
+
}
|
|
10352
10396
|
if (!this.running && !getInflightCompaction()) {
|
|
10353
10397
|
this.kickDrain();
|
|
10354
10398
|
}
|
|
@@ -10480,6 +10524,7 @@ var HeadlessSession = class {
|
|
|
10480
10524
|
handleClear() {
|
|
10481
10525
|
clearSession(this.state);
|
|
10482
10526
|
return {
|
|
10527
|
+
...this.state.models && { models: this.state.models },
|
|
10483
10528
|
modelSurfaces: getEffectiveModelSurfaces(),
|
|
10484
10529
|
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
10485
10530
|
};
|
|
@@ -10500,9 +10545,9 @@ var HeadlessSession = class {
|
|
|
10500
10545
|
/**
|
|
10501
10546
|
* Stop everything the user can see running: the turn, an in-flight
|
|
10502
10547
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
10503
|
-
* follow-ups that belonged to the turn
|
|
10504
|
-
* `source: 'user'` items
|
|
10505
|
-
*
|
|
10548
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
10549
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
10550
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
10506
10551
|
*
|
|
10507
10552
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
10508
10553
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -10511,6 +10556,13 @@ var HeadlessSession = class {
|
|
|
10511
10556
|
* and every additional press hit a turn that had just started. They now wait
|
|
10512
10557
|
* in the queue card until the user sends again or promotes one.
|
|
10513
10558
|
*
|
|
10559
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
10560
|
+
* away the rest of the build — including the step that finalizes it and
|
|
10561
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
10562
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
10563
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
10564
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
10565
|
+
*
|
|
10514
10566
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
10515
10567
|
* message and outlives the turn that started it, so leaving it running means
|
|
10516
10568
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -10531,19 +10583,50 @@ var HeadlessSession = class {
|
|
|
10531
10583
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
10532
10584
|
this.pendingTools.delete(id);
|
|
10533
10585
|
}
|
|
10534
|
-
const flushed = this.queue.removeWhere(
|
|
10535
|
-
|
|
10536
|
-
|
|
10586
|
+
const flushed = this.queue.removeWhere(
|
|
10587
|
+
(item) => item.source === "background"
|
|
10588
|
+
);
|
|
10589
|
+
const step = this.currentChainStep;
|
|
10590
|
+
if (step) {
|
|
10591
|
+
this.queue.unshift({
|
|
10592
|
+
command: {
|
|
10593
|
+
action: "message",
|
|
10594
|
+
text: step.text,
|
|
10595
|
+
onboardingState: step.onboardingState,
|
|
10596
|
+
// Fresh id: the original command's terminal has already gone out as
|
|
10597
|
+
// cancelled, and one command gets exactly one `completed`.
|
|
10598
|
+
requestId: this.nextChainRequestId()
|
|
10599
|
+
},
|
|
10600
|
+
source: "chain",
|
|
10601
|
+
enqueuedAt: Date.now(),
|
|
10602
|
+
held: true
|
|
10603
|
+
});
|
|
10604
|
+
this.currentChainStep = null;
|
|
10605
|
+
}
|
|
10606
|
+
const held = this.queue.holdWhere(
|
|
10607
|
+
(item) => item.source === "user" || item.source === "chain"
|
|
10608
|
+
);
|
|
10609
|
+
return {
|
|
10610
|
+
flushed,
|
|
10611
|
+
held,
|
|
10612
|
+
pausedPipeline: held.some((item) => item.source === "chain"),
|
|
10613
|
+
cancelledCompaction
|
|
10614
|
+
};
|
|
10537
10615
|
}
|
|
10538
10616
|
/**
|
|
10539
|
-
* Remove pending queued messages
|
|
10540
|
-
*
|
|
10541
|
-
*
|
|
10542
|
-
*
|
|
10617
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
10618
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
10619
|
+
*
|
|
10620
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
10621
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
10622
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
10623
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
10624
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
10625
|
+
* before running it.)
|
|
10543
10626
|
*/
|
|
10544
10627
|
handleCancelQueued(id) {
|
|
10545
10628
|
return this.queue.removeWhere(
|
|
10546
|
-
(item) => item.source === "user" && (
|
|
10629
|
+
(item) => id === void 0 ? item.source === "user" : item.command.requestId === id && (item.source === "user" || item.source === "chain" && !!item.held)
|
|
10547
10630
|
);
|
|
10548
10631
|
}
|
|
10549
10632
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -10642,13 +10725,14 @@ var HeadlessSession = class {
|
|
|
10642
10725
|
return;
|
|
10643
10726
|
}
|
|
10644
10727
|
if (action === "cancel") {
|
|
10645
|
-
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
10728
|
+
const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
|
|
10646
10729
|
this.emit(
|
|
10647
10730
|
"completed",
|
|
10648
10731
|
{
|
|
10649
10732
|
success: true,
|
|
10650
10733
|
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
10651
10734
|
...held.length > 0 && { heldMessages: held },
|
|
10735
|
+
...pausedPipeline && { pausedPipeline: true },
|
|
10652
10736
|
...cancelledCompaction && { cancelledCompaction: true }
|
|
10653
10737
|
},
|
|
10654
10738
|
requestId
|
package/dist/index.js
CHANGED
|
@@ -2843,16 +2843,7 @@ function clearSession(state) {
|
|
|
2843
2843
|
log3.warn("Session archive on clear failed", { error: err.message });
|
|
2844
2844
|
}
|
|
2845
2845
|
state.messages = [];
|
|
2846
|
-
state
|
|
2847
|
-
try {
|
|
2848
|
-
if (fs10.existsSync(SESSION_FILE)) {
|
|
2849
|
-
fs10.unlinkSync(SESSION_FILE);
|
|
2850
|
-
}
|
|
2851
|
-
} catch (err) {
|
|
2852
|
-
log3.warn("Session clear: could not remove live file", {
|
|
2853
|
-
error: err.message
|
|
2854
|
-
});
|
|
2855
|
-
}
|
|
2846
|
+
saveSession(state);
|
|
2856
2847
|
}
|
|
2857
2848
|
var log3, SESSION_FILE, ARCHIVE_DIR, ARCHIVE_NAME_RE, archiveSortKey, ARCHIVE_COUNT_RE, archiveCountCache, archiveMsgCache, ARCHIVE_MSG_CACHE_MAX;
|
|
2858
2849
|
var init_session = __esm({
|
|
@@ -10320,6 +10311,15 @@ var init_messageQueue = __esm({
|
|
|
10320
10311
|
this.items.push(item);
|
|
10321
10312
|
this.onChange?.();
|
|
10322
10313
|
}
|
|
10314
|
+
/**
|
|
10315
|
+
* Add an item at the head. Used to put an interrupted chain step back in
|
|
10316
|
+
* front of the rest of its pipeline on cancel; `promoteToFront` can't serve
|
|
10317
|
+
* that case, since it addresses an item already in the queue by requestId.
|
|
10318
|
+
*/
|
|
10319
|
+
unshift(item) {
|
|
10320
|
+
this.items.unshift(item);
|
|
10321
|
+
this.onChange?.();
|
|
10322
|
+
}
|
|
10323
10323
|
/**
|
|
10324
10324
|
* Index of the first deliverable item, or -1 when there is none.
|
|
10325
10325
|
*
|
|
@@ -10388,25 +10388,6 @@ var init_messageQueue = __esm({
|
|
|
10388
10388
|
}
|
|
10389
10389
|
return held;
|
|
10390
10390
|
}
|
|
10391
|
-
/**
|
|
10392
|
-
* Release held items so the normal drain picks them up again — all of them,
|
|
10393
|
-
* or one by command requestId. Fires onChange only if something changed.
|
|
10394
|
-
* Returns the released items.
|
|
10395
|
-
*/
|
|
10396
|
-
releaseHeld(id) {
|
|
10397
|
-
const released = [];
|
|
10398
|
-
for (const item of this.items) {
|
|
10399
|
-
if (!item.held || id !== void 0 && item.command.requestId !== id) {
|
|
10400
|
-
continue;
|
|
10401
|
-
}
|
|
10402
|
-
delete item.held;
|
|
10403
|
-
released.push(item);
|
|
10404
|
-
}
|
|
10405
|
-
if (released.length > 0) {
|
|
10406
|
-
this.onChange?.();
|
|
10407
|
-
}
|
|
10408
|
-
return released;
|
|
10409
|
-
}
|
|
10410
10391
|
/** Whether anything in the queue will drain on its own (i.e. isn't held). */
|
|
10411
10392
|
hasDeliverable() {
|
|
10412
10393
|
return this.items.some((item) => !item.held);
|
|
@@ -10451,6 +10432,38 @@ var init_messageQueue = __esm({
|
|
|
10451
10432
|
this.onChange?.();
|
|
10452
10433
|
return item;
|
|
10453
10434
|
}
|
|
10435
|
+
/**
|
|
10436
|
+
* Fold-in release: un-hold everything, then move items matching `defer` to
|
|
10437
|
+
* the tail, relative order preserved. One onChange for the whole
|
|
10438
|
+
* rearrangement, so no observer ever sees it half-applied.
|
|
10439
|
+
*
|
|
10440
|
+
* Callers push the new user message FIRST and call this second — "behind" is
|
|
10441
|
+
* defined by the array at the moment this runs. Held user messages stay put
|
|
10442
|
+
* so they still merge with that message into one mailbox batch; a paused
|
|
10443
|
+
* pipeline goes to the back, so the person's words run before the build
|
|
10444
|
+
* continues. `defer` is applied to every item, not just the newly-released
|
|
10445
|
+
* ones, so a chain step that was already deliverable defers too.
|
|
10446
|
+
*/
|
|
10447
|
+
releaseHeldDeferring(defer) {
|
|
10448
|
+
const released = [];
|
|
10449
|
+
for (const item of this.items) {
|
|
10450
|
+
if (!item.held) {
|
|
10451
|
+
continue;
|
|
10452
|
+
}
|
|
10453
|
+
delete item.held;
|
|
10454
|
+
released.push(item);
|
|
10455
|
+
}
|
|
10456
|
+
const back = this.items.filter(defer);
|
|
10457
|
+
const front = this.items.filter((item) => !defer(item));
|
|
10458
|
+
const reordered = back.length > 0 && front.length > 0;
|
|
10459
|
+
if (reordered) {
|
|
10460
|
+
this.items = [...front, ...back];
|
|
10461
|
+
}
|
|
10462
|
+
if (released.length > 0 || reordered) {
|
|
10463
|
+
this.onChange?.();
|
|
10464
|
+
}
|
|
10465
|
+
return released;
|
|
10466
|
+
}
|
|
10454
10467
|
/** Copy of current queue contents (for surfacing on events). */
|
|
10455
10468
|
snapshot() {
|
|
10456
10469
|
return [...this.items];
|
|
@@ -10528,6 +10541,20 @@ var init_headless = __esm({
|
|
|
10528
10541
|
* right state (the triggering turn's state, not a stale one).
|
|
10529
10542
|
*/
|
|
10530
10543
|
currentOnboardingState;
|
|
10544
|
+
/**
|
|
10545
|
+
* The in-flight turn's command, when that turn is part of a build pipeline
|
|
10546
|
+
* (its action declares a `next`, or it is itself a chain step). Captured so a
|
|
10547
|
+
* cancel can put the interrupted step back on the queue held, instead of
|
|
10548
|
+
* leaving the pipeline to resume at the step *after* the one that was
|
|
10549
|
+
* stopped — which would run finalize/polish over half-built code.
|
|
10550
|
+
*
|
|
10551
|
+
* Null for every other turn shape, which is what scopes the whole
|
|
10552
|
+
* paused-pipeline affordance to chains: stopping a one-off automated action
|
|
10553
|
+
* (approvePlan, publish, sync) leaves nothing behind.
|
|
10554
|
+
*/
|
|
10555
|
+
currentChainStep = null;
|
|
10556
|
+
/** Monotonic suffix for synthesized chain requestIds — see nextChainRequestId. */
|
|
10557
|
+
chainRequestSeq = 0;
|
|
10531
10558
|
/**
|
|
10532
10559
|
* Unified message queue. Holds pending work to deliver after the current
|
|
10533
10560
|
* turn completes: chained automated actions, background sub-agent results,
|
|
@@ -11093,6 +11120,16 @@ var init_headless = __esm({
|
|
|
11093
11120
|
return void 0;
|
|
11094
11121
|
}
|
|
11095
11122
|
}
|
|
11123
|
+
/**
|
|
11124
|
+
* Stable id for a queued chain step, so the frontend can address one (to
|
|
11125
|
+
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
11126
|
+
* synthesize an id at dequeue. Deliberately not an `ac-` id — the frontend
|
|
11127
|
+
* keys "system turn" off that prefix — and deliberately not a bare
|
|
11128
|
+
* Date.now(), which collides for steps enqueued in the same millisecond.
|
|
11129
|
+
*/
|
|
11130
|
+
nextChainRequestId() {
|
|
11131
|
+
return `chain-${Date.now()}-${++this.chainRequestSeq}`;
|
|
11132
|
+
}
|
|
11096
11133
|
/**
|
|
11097
11134
|
* Run one turn for a single command (without acquiring the `running` lock).
|
|
11098
11135
|
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
@@ -11130,36 +11167,42 @@ var init_headless = __esm({
|
|
|
11130
11167
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
11131
11168
|
this.currentOnboardingState = onboardingState;
|
|
11132
11169
|
const system = buildSystemPrompt(onboardingState);
|
|
11170
|
+
this.currentChainStep = resolved && (resolved.next != null || fromChain) ? { text: rawText, onboardingState } : null;
|
|
11133
11171
|
if (resolved?.next && !fromChain) {
|
|
11134
11172
|
for (const step of getActionChain(resolved.next)) {
|
|
11135
11173
|
this.queue.push({
|
|
11136
11174
|
command: {
|
|
11137
11175
|
action: "message",
|
|
11138
11176
|
text: sentinel(step),
|
|
11139
|
-
onboardingState
|
|
11177
|
+
onboardingState,
|
|
11178
|
+
requestId: this.nextChainRequestId()
|
|
11140
11179
|
},
|
|
11141
11180
|
source: "chain",
|
|
11142
11181
|
enqueuedAt: Date.now()
|
|
11143
11182
|
});
|
|
11144
11183
|
}
|
|
11145
11184
|
}
|
|
11146
|
-
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11185
|
+
try {
|
|
11186
|
+
await this.executeTurn({
|
|
11187
|
+
entries: [
|
|
11188
|
+
{
|
|
11189
|
+
text: userMessage,
|
|
11190
|
+
attachments,
|
|
11191
|
+
attachmentHeader,
|
|
11192
|
+
hidden: isHidden || void 0,
|
|
11193
|
+
requestId,
|
|
11194
|
+
queued: queued || void 0
|
|
11195
|
+
}
|
|
11196
|
+
],
|
|
11197
|
+
requestId,
|
|
11198
|
+
absorbedRids: [],
|
|
11199
|
+
onboardingState,
|
|
11200
|
+
system,
|
|
11201
|
+
buildModel
|
|
11202
|
+
});
|
|
11203
|
+
} finally {
|
|
11204
|
+
this.currentChainStep = null;
|
|
11205
|
+
}
|
|
11163
11206
|
}
|
|
11164
11207
|
/**
|
|
11165
11208
|
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
@@ -11172,6 +11215,7 @@ var init_headless = __esm({
|
|
|
11172
11215
|
* with no side effects).
|
|
11173
11216
|
*/
|
|
11174
11217
|
async runMergedTurn(batch) {
|
|
11218
|
+
this.currentChainStep = null;
|
|
11175
11219
|
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
11176
11220
|
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
11177
11221
|
const entryList = [];
|
|
@@ -11326,9 +11370,6 @@ var init_headless = __esm({
|
|
|
11326
11370
|
}
|
|
11327
11371
|
async handleMessage(parsed, requestId) {
|
|
11328
11372
|
const foldIn = !this.running && !getInflightCompaction() && this.queue.length > 0 && !isAutomatedMessage(parsed.text ?? "");
|
|
11329
|
-
if (foldIn) {
|
|
11330
|
-
this.queue.releaseHeld();
|
|
11331
|
-
}
|
|
11332
11373
|
if (this.running || getInflightCompaction() || foldIn) {
|
|
11333
11374
|
const command = { ...parsed };
|
|
11334
11375
|
if (requestId && command.requestId === void 0) {
|
|
@@ -11339,6 +11380,9 @@ var init_headless = __esm({
|
|
|
11339
11380
|
source: "user",
|
|
11340
11381
|
enqueuedAt: Date.now()
|
|
11341
11382
|
});
|
|
11383
|
+
if (foldIn) {
|
|
11384
|
+
this.queue.releaseHeldDeferring((item) => item.source === "chain");
|
|
11385
|
+
}
|
|
11342
11386
|
if (!this.running && !getInflightCompaction()) {
|
|
11343
11387
|
this.kickDrain();
|
|
11344
11388
|
}
|
|
@@ -11470,6 +11514,7 @@ var init_headless = __esm({
|
|
|
11470
11514
|
handleClear() {
|
|
11471
11515
|
clearSession(this.state);
|
|
11472
11516
|
return {
|
|
11517
|
+
...this.state.models && { models: this.state.models },
|
|
11473
11518
|
modelSurfaces: getEffectiveModelSurfaces(),
|
|
11474
11519
|
allowedModelsByType: ALLOWED_MODELS_BY_TYPE
|
|
11475
11520
|
};
|
|
@@ -11490,9 +11535,9 @@ var init_headless = __esm({
|
|
|
11490
11535
|
/**
|
|
11491
11536
|
* Stop everything the user can see running: the turn, an in-flight
|
|
11492
11537
|
* compaction, and any external tool waiting on a result. Flushes the
|
|
11493
|
-
* follow-ups that belonged to the turn
|
|
11494
|
-
* `source: 'user'` items
|
|
11495
|
-
*
|
|
11538
|
+
* `background` follow-ups that belonged to the turn, and HOLDS everything
|
|
11539
|
+
* still queued — the `source: 'user'` items, which are independent user
|
|
11540
|
+
* intent, plus the `source: 'chain'` steps of a build pipeline.
|
|
11496
11541
|
*
|
|
11497
11542
|
* Holding is the difference between Stop working and Stop looking broken.
|
|
11498
11543
|
* These items used to drain immediately: `executeTurn` swallows the abort,
|
|
@@ -11501,6 +11546,13 @@ var init_headless = __esm({
|
|
|
11501
11546
|
* and every additional press hit a turn that had just started. They now wait
|
|
11502
11547
|
* in the queue card until the user sends again or promotes one.
|
|
11503
11548
|
*
|
|
11549
|
+
* The chain steps used to be flushed with the background items, which threw
|
|
11550
|
+
* away the rest of the build — including the step that finalizes it and
|
|
11551
|
+
* unlocks the editor. They are now paused instead, and the interrupted step
|
|
11552
|
+
* goes back at the head, so a Stop is a pause and the next send resumes the
|
|
11553
|
+
* pipeline where it stopped. Holding only the remainder would resume one step
|
|
11554
|
+
* PAST the interruption, polishing and finalizing half-built code.
|
|
11555
|
+
*
|
|
11504
11556
|
* A compaction is cancelled here too, unconditionally. It gates every queued
|
|
11505
11557
|
* message and outlives the turn that started it, so leaving it running means
|
|
11506
11558
|
* Stop can't reach idle. The cost is the summary work in flight; the forced
|
|
@@ -11521,19 +11573,50 @@ var init_headless = __esm({
|
|
|
11521
11573
|
pending2.resolve(USER_CANCELLED_RESULT);
|
|
11522
11574
|
this.pendingTools.delete(id);
|
|
11523
11575
|
}
|
|
11524
|
-
const flushed = this.queue.removeWhere(
|
|
11525
|
-
|
|
11526
|
-
|
|
11576
|
+
const flushed = this.queue.removeWhere(
|
|
11577
|
+
(item) => item.source === "background"
|
|
11578
|
+
);
|
|
11579
|
+
const step = this.currentChainStep;
|
|
11580
|
+
if (step) {
|
|
11581
|
+
this.queue.unshift({
|
|
11582
|
+
command: {
|
|
11583
|
+
action: "message",
|
|
11584
|
+
text: step.text,
|
|
11585
|
+
onboardingState: step.onboardingState,
|
|
11586
|
+
// Fresh id: the original command's terminal has already gone out as
|
|
11587
|
+
// cancelled, and one command gets exactly one `completed`.
|
|
11588
|
+
requestId: this.nextChainRequestId()
|
|
11589
|
+
},
|
|
11590
|
+
source: "chain",
|
|
11591
|
+
enqueuedAt: Date.now(),
|
|
11592
|
+
held: true
|
|
11593
|
+
});
|
|
11594
|
+
this.currentChainStep = null;
|
|
11595
|
+
}
|
|
11596
|
+
const held = this.queue.holdWhere(
|
|
11597
|
+
(item) => item.source === "user" || item.source === "chain"
|
|
11598
|
+
);
|
|
11599
|
+
return {
|
|
11600
|
+
flushed,
|
|
11601
|
+
held,
|
|
11602
|
+
pausedPipeline: held.some((item) => item.source === "chain"),
|
|
11603
|
+
cancelledCompaction
|
|
11604
|
+
};
|
|
11527
11605
|
}
|
|
11528
11606
|
/**
|
|
11529
|
-
* Remove pending queued messages
|
|
11530
|
-
*
|
|
11531
|
-
*
|
|
11532
|
-
*
|
|
11607
|
+
* Remove pending queued messages: all user messages (no id), or a single item
|
|
11608
|
+
* by id. Does not affect the in-flight turn (use `cancel` for that).
|
|
11609
|
+
*
|
|
11610
|
+
* Held chain items — a paused pipeline — are removable only by explicit id.
|
|
11611
|
+
* The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
|
|
11612
|
+
* neither of which should get to decide the pipeline's fate. A DELIVERABLE
|
|
11613
|
+
* chain item is never removable: that's live pipeline work. (The step
|
|
11614
|
+
* actually running isn't in the queue at all — drainQueueLoop takes it out
|
|
11615
|
+
* before running it.)
|
|
11533
11616
|
*/
|
|
11534
11617
|
handleCancelQueued(id) {
|
|
11535
11618
|
return this.queue.removeWhere(
|
|
11536
|
-
(item) => item.source === "user" && (
|
|
11619
|
+
(item) => id === void 0 ? item.source === "user" : item.command.requestId === id && (item.source === "user" || item.source === "chain" && !!item.held)
|
|
11537
11620
|
);
|
|
11538
11621
|
}
|
|
11539
11622
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -11632,13 +11715,14 @@ var init_headless = __esm({
|
|
|
11632
11715
|
return;
|
|
11633
11716
|
}
|
|
11634
11717
|
if (action === "cancel") {
|
|
11635
|
-
const { flushed, held, cancelledCompaction } = this.handleCancel();
|
|
11718
|
+
const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
|
|
11636
11719
|
this.emit(
|
|
11637
11720
|
"completed",
|
|
11638
11721
|
{
|
|
11639
11722
|
success: true,
|
|
11640
11723
|
...flushed.length > 0 && { cancelledMessages: flushed },
|
|
11641
11724
|
...held.length > 0 && { heldMessages: held },
|
|
11725
|
+
...pausedPipeline && { pausedPipeline: true },
|
|
11642
11726
|
...cancelledCompaction && { cancelledCompaction: true }
|
|
11643
11727
|
},
|
|
11644
11728
|
requestId
|
|
@@ -45,6 +45,8 @@ All fields are nested under the `"web"` key.
|
|
|
45
45
|
| `defaultPreviewMode` | `"desktop"` \| `"mobile"` | `"desktop"` | Default preview viewport in the editor. Set to `"mobile"` for mobile-first apps. |
|
|
46
46
|
| `prerender` | `object` | — | Opt into prerendering the listed routes/patterns for crawlers/unfurlers. See "Prerendering" below. |
|
|
47
47
|
| `mounts` | `array` | — | Serve other same-workspace apps under path prefixes of this app's hosts. See "Mounting other apps" below. |
|
|
48
|
+
| `redirects` | `array` | — | Path-level redirects. See "Redirects" below. |
|
|
49
|
+
| `trailingSlash` | `"strip"` \| `"append"` | — | Enforce a canonical trailing-slash form with a 308. Off by default. |
|
|
48
50
|
|
|
49
51
|
### Frontend SDK
|
|
50
52
|
|
|
@@ -136,7 +138,7 @@ Opt in per route in `web.json`:
|
|
|
136
138
|
{ "web": { "prerender": { "paths": ["/u/*", "/blog/*"] } } }
|
|
137
139
|
```
|
|
138
140
|
|
|
139
|
-
`prerender` is an object with a single field, `paths`: an array of route globs (`*` = one segment, `**` = any). Only listed routes prerender.
|
|
141
|
+
`prerender` is an object with a single field, `paths`: an array of route globs (`*` = one segment, `**` = any). Only listed routes prerender, and only routes — a path with a file extension (`/robots.txt`, `/sitemap.xml`) never prerenders, even under `/**`.
|
|
140
142
|
|
|
141
143
|
The opt-in alone is not enough. For every route in `prerender.paths`, the SPA must set `document.documentElement.setAttribute('data-prerender-ready', 'true')` once the head is written and the route has resolved — this is required, not optional. The renderer waits for this marker, so a prerendered route that never sets it times out. Set it even on routes that render synchronously, as soon as they're ready.
|
|
142
144
|
|
|
@@ -149,9 +151,28 @@ await prerender.invalidate(['/u/abc']); // omit arg to purge all
|
|
|
149
151
|
|
|
150
152
|
`remy-admin prerender` can help you verify/manage snapshots during development.
|
|
151
153
|
|
|
154
|
+
### Redirects
|
|
155
|
+
|
|
156
|
+
Declare moved URLs in `web.json` — never ship a component that redirects client-side, since that resolves after JS loads and crawlers won't follow it:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "web": { "trailingSlash": "strip", "redirects": [
|
|
160
|
+
{ "source": "/old-blog/*slug", "destination": "/blog/*slug", "permanent": true },
|
|
161
|
+
{ "source": "/launch", "destination": "https://example.com/launch", "statusCode": 302 }
|
|
162
|
+
] } }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`source` is a `/`-prefixed pattern; `destination` is a path or absolute http(s) URL. `permanent` (true → 308, false → 307) is required unless you set an explicit `statusCode` of 301/302/307/308; you can't set both. First match wins, and the request's query carries over unless the destination has its own.
|
|
166
|
+
|
|
167
|
+
**Pattern syntax is path-to-regexp v8, NOT the Next.js dialect** — the one thing to get right here. One segment is `:name`; a multi-segment wildcard is `*name` (Next's `:name*`); optional segments use braces (`/users{/:id}`); inline regex (`:id(\\d+)`) is unsupported. The v6/Next forms fail the build with the correct syntax in the error.
|
|
168
|
+
|
|
169
|
+
`trailingSlash` picks the canonical form (`"strip"`: `/about/` → `/about`; `"append"`: the reverse, skipping the root and paths with file extensions). Off by default. Write `source` patterns without trailing slashes either way — normalization and matching resolve in one hop. Sources under `/_/` are rejected, as are self-referential and two-rule loops. A redirecting path is never prerendered, so don't list one in `prerender.paths`.
|
|
170
|
+
|
|
152
171
|
### Mounting other apps
|
|
153
172
|
|
|
154
|
-
Rare: `mounts` serves another same-workspace app under a path prefix of this app's hosts — `{ "web": { "mounts": [{ "path": "/docs", "app": "docs-site" }] } }` (`app` = the target's `custom_subdomain` or appId). The child is served first-class (its own bundle, session, backend) and needs no mount-specific config; the two serving conventions above are what make an app mountable.
|
|
173
|
+
Rare: `mounts` serves another same-workspace app under a path prefix of this app's hosts — `{ "web": { "mounts": [{ "path": "/docs", "app": "docs-site" }] } }` (`app` = the target's `custom_subdomain` or appId). The child is served first-class (its own bundle, session, backend, prerendering) and needs no mount-specific config; the two serving conventions above are what make an app mountable.
|
|
174
|
+
|
|
175
|
+
Under a mount the child's own `web.json` governs its SEO and routing: its `prerender.paths` gates its mounted pages, its `prerender.invalidate` purges them, its `redirects`/`trailingSlash` apply within the prefix (written against its own paths, with the prefix added back to relative destinations), and its `sitemap.xml` is served with URLs rewritten to the mount and advertised in the parent's `robots.txt`. The child's `robots.txt` *rules* don't carry over (the build log lists them prefixed for the parent to adopt), and canonicals must be written from the page's own location — a hardcoded absolute canonical points crawlers back at the child's host and undoes the mount.
|
|
155
176
|
|
|
156
177
|
## API Interface
|
|
157
178
|
|
|
@@ -18,6 +18,7 @@ If dismissed, acknowledge and do nothing — no commit, no push.
|
|
|
18
18
|
|
|
19
19
|
## 2. Ship (on approval)
|
|
20
20
|
|
|
21
|
+
- On a meaningful release, glance at dependencies before committing — `npm outdated` in the methods package and in each interface's web directory. The first-party packages (`@mindstudio-ai/agent`, `@mindstudio-ai/interface`, `@madewithremy/admin`) are ours and versioned additively: a bump brings new capabilities and bug fixes, not a migration. Bring those current without asking, typecheck, and mention it in plain language when you report the deploy; if a bump does need a small code change, just make it. Third-party packages are the user's time to spend, so flag anything meaningfully behind and let them decide. Skip all of this on a hotfix — a quick fix going out doesn't need a dependency pass.
|
|
21
22
|
- Stage and commit any uncommitted changes with a clean, descriptive commit message. If the committed work resolves any open issues (`remy-admin issues`), reference them in the commit message with a closing keyword — `fixes #42`, `closes #7` — so the deploy closes them automatically once it goes live.
|
|
22
23
|
- Push to main.
|
|
23
24
|
- Use `remy-admin releases wait` to poll the build until it completes. Let the user know it's deploying, then report back when it's live.
|
|
@@ -39,7 +39,7 @@ These are things we already know about and have decided to accept:
|
|
|
39
39
|
- use [wouter](https://github.com/molefrog/wouter) for React routing instead of reaching for react-router
|
|
40
40
|
- uploading user files should always happen via `platform.uploadFile()` from `@mindstudio-ai/interface` — not custom S3 code, not FormData to a method endpoint
|
|
41
41
|
- for build-time prerendering of purely static sites (marketing pages with no dynamic content — distinct from the platform's crawler prerendering, below), roll your own with a post-build `renderToString` script — do not use `vite-prerender-plugin` (it bundles the prerender script as a client chunk, adding ~800KB to the user-facing bundle with no way to prevent it)
|
|
42
|
-
- **Prerendering for crawlers/unfurlers is a platform feature — don't design around it.** For SEO / link-unfurl / AI-crawler visibility on a non-static SPA, the platform already handles it: routes opt in via `web.json` (`{ "web": { "prerender": { "paths": ["/blog/*"] } } }`), the SPA signals readiness by setting `data-prerender-ready` on the html element, deploys invalidate the snapshot cache automatically, and content that changes outside deploys is invalidated at runtime with `await prerender.invalidate([...])` from `@mindstudio-ai/agent` (called from the mutating method). It serves cached headless snapshots of the live SPA to bots — it is NOT build-time rendering. Do not recommend post-build render scripts, rebuild-on-content-change, or third-party prerender services for this. The developer's main context carries the full interfaces reference with exact semantics — tell them to consult it rather than improvising.
|
|
42
|
+
- **Prerendering for crawlers/unfurlers is a platform feature — don't design around it.** For SEO / link-unfurl / AI-crawler visibility on a non-static SPA, the platform already handles it: routes opt in via `web.json` (`{ "web": { "prerender": { "paths": ["/blog/*"] } } }`), the SPA signals readiness by setting `data-prerender-ready` on the html element, deploys invalidate the snapshot cache automatically, and content that changes outside deploys is invalidated at runtime with `await prerender.invalidate([...])` from `@mindstudio-ai/agent` (called from the mutating method). It serves cached headless snapshots of the live SPA to bots — it is NOT build-time rendering. Do not recommend post-build render scripts, rebuild-on-content-change, or third-party prerender services for this. An app mounted under another app's path prefix keeps its own `prerender.paths` and its own `prerender.invalidate` — don't recommend moving prerender config to the parent, and don't hand-roll sitemap/robots rewriting for a mount (the platform rewrites the mounted sitemap and advertises it in the parent's robots.txt). The developer's main context carries the full interfaces reference with exact semantics — tell them to consult it rather than improvising.
|
|
43
43
|
|
|
44
44
|
### Common pitfalls (always flag these)
|
|
45
45
|
|