stillmade-sdk 0.1.49 → 0.1.51
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/AGENTS.md +6 -2
- package/AGENT_CONTROL.md +15 -0
- package/FRAME_VIEWS.md +30 -0
- package/INTERFACE_DESIGN.md +84 -7
- package/MODULES.md +2 -2
- package/PASTE_TO_LLM.md +26 -19
- package/SDK.md +180 -45
- package/UI_AESTHETIC.md +15 -0
- package/blocks/generate/package.js +1 -1
- package/blocks/generate/versions/1.1.0/package.js +471 -0
- package/blocks/generate/versions/1.2.0/package.js +555 -0
- package/blocks/native/sm.board-editor/versions/3.0.0/package.js +50 -17
- package/blocks/native/sm.board-editor/versions/3.1.0/package.js +1037 -0
- package/blocks/native/sm.editor/versions/3.0.0/package.js +128 -83
- package/blocks/native/sm.editor/versions/3.1.0/package.js +1299 -0
- package/blocks/native/sm.panel-strip/versions/3.0.0/package.js +45 -12
- package/blocks/native/sm.panel-strip/versions/3.1.0/package.js +479 -0
- package/blocks/native/sm.voiceover/versions/3.0.0/package.js +9 -3
- package/docs/build/block-builder-contract.md +6 -2
- package/docs/build/frame-views.md +30 -0
- package/docs/build/interface-design.md +84 -7
- package/docs/build/modules.md +2 -2
- package/docs/reference/authoring-guide.md +4 -4
- package/docs/reference/errors.md +1 -1
- package/docs/reference/execution.md +18 -4
- package/docs/reference/first-party-blocks.md +4 -4
- package/docs/reference/parity.md +1 -1
- package/docs/reference/ports.md +1 -1
- package/docs/reference/view-bridge.md +3 -0
- package/docs/tools/llm.md +26 -19
- package/package.json +1 -1
- package/packages/block-cli/dev-host/assets/host.js +161 -33
- package/packages/block-cli/dev-host/assets/index.css +1 -1
- package/packages/block-platform/block-readiness.js +16 -3
- package/packages/block-platform/canvas.js +4 -1
- package/packages/block-platform/catalog.js +32 -5
- package/packages/block-platform/flow-inspection.js +30 -2
- package/packages/block-platform/listings.js +16 -2
- package/packages/block-platform/steps.js +4 -1
- package/packages/block-sdk/admission.js +2 -2
- package/packages/block-sdk/agent-inputs.js +4 -2
- package/packages/block-sdk/appearance.js +19 -4
- package/packages/block-sdk/builder-contract.js +7 -4
- package/packages/block-sdk/first-party-identities.js +9 -1
- package/packages/block-sdk/first-party-packages.js +11 -1
- package/packages/block-sdk/generation-catalog.js +55 -5
- package/packages/block-sdk/generation-models.js +60 -0
- package/packages/block-sdk/index.d.ts +18 -4
- package/packages/block-sdk/index.js +2 -1
- package/packages/block-sdk/input-source-policy.js +2 -1
- package/packages/block-sdk/module-hosted.js +13 -5
- package/packages/block-sdk/module-worker.js +1 -1
- package/packages/block-sdk/port-types.js +6 -2
- package/packages/block-sdk/semantic-contracts.d.ts +1 -1
- package/packages/block-sdk/semantic-relationships.js +3 -0
- package/packages/block-sdk/semantic-schema.js +4 -1
- package/packages/block-sdk/shared-controls.js +4 -1
- package/packages/block-sdk/staged-releases.js +2 -1
- package/packages/block-sdk/testing.js +3 -0
- package/packages/block-sdk/view-bridge.js +3 -0
- package/packages/block-sdk/view-design.js +56 -12
- package/packages/block-sdk/view-renderer.js +19 -6
- package/packages/block-sdk/view.js +3 -2
- package/packages/design-system/block-kit.css +12 -3
|
@@ -148,6 +148,8 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
|
|
|
148
148
|
const archivedByStage=new Map();
|
|
149
149
|
for(const entry of canvas.workflowArchive||[])if(entry.step?.id&&entry.node?.data?.blockLastRun){if(!archivedByStage.has(entry.step.id))archivedByStage.set(entry.step.id,[]);archivedByStage.get(entry.step.id).push(entry);}
|
|
150
150
|
const manifestFor=(stage,node)=>node?.data?.blockPackage?.manifest||(packageForStage(type,stage,[...BUILTIN_BLOCKS,...catalog])?.manifest);
|
|
151
|
+
// An Automation (sm.when) runs the project's Steps with nobody there, so nothing is left for the person to give then.
|
|
152
|
+
const unattended=type.stages.some(stage=>stage.blockId==='sm.when'&&stage.enabled!==false);
|
|
151
153
|
// The Step (job) a Block sits in, by the name the person sees (October 19, 2026: Chat read
|
|
152
154
|
// Block names only and told a tester her "Upload sermon" Step did not exist).
|
|
153
155
|
const stepOf=new Map((type.groups||[]).filter(group=>typeof group?.label==='string'&&group.label&&Array.isArray(group.stages)).flatMap(group=>group.stages.map(id=>[id,group.label])));
|
|
@@ -204,6 +206,20 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
|
|
|
204
206
|
const pinOptions=selectedEdge&&!item.selected?.adapter?pinOptionsFor(canvas,producer,sourceStage,source.port,item.requirement,selectedEdge,labels,mediaVersions,restrictedResults):{canPinVersion:false,pinSource:null,pinChoices:[]};
|
|
205
207
|
return {key:item.key,label:item.label,...(item.requirement.context?{context:item.requirement.context}:{}),required:item.requirement.required!==false,state,reason:sourceSkipped?'The selected Block skipped this conditional path, so it produced no value.':native?.reason||(item.requirement.context&&!materialized?contextInputNote(item.requirement.context):item.reason),source:sourceName(item.selected,labels),sourceKind:source?.kind||null,sourcePlacementId:source?.stageId||null,sourceOutputKey:source?.port||null,selectedCandidateId:item.selected?.id||null,explicit:item.explicit===true,selectionReason:source?selectionReason(item,source):null,valuePolicy:source?.kind==='block'?(item.explicit?'follow-selected-output':'automatic-resolution'):source?.kind==='default'?'declared-default':source?'current-source-value':null,adapter:item.selected?.adapter?{id:item.selected.adapter.id,version:item.selected.adapter.version}:null,adapterDetail:adapterDetail(item.selected?.adapter,materialized),materialized,...(native?.observation?{sourceObservation:{...native.observation,revision:retainedRevision([native.observation,native.value])}}:{}),preview:materialized&&!sensitive(item.selected?.record)&&!(source?.kind==='block'&&(!(native?.port||producer?.data?.blockPackage?.manifest?.outputs?.[source.port])||sensitive(native?.port||producer.data.blockPackage.manifest.outputs[source.port])))&&!restrictedResults.has(`${source?.stageId}:${source?.port}`)?inputPreview(value,item.requirement):null,...pinOptions,...candidatePageSummary,...overrideMeta};
|
|
206
208
|
});
|
|
209
|
+
// The Block's main input (its manifest marks it primary) is what the map always shows it takes, wherever its value
|
|
210
|
+
// comes from (November 9, 2026, owner on the Voiceover card that showed no input: "is it not fucking text").
|
|
211
|
+
for(const input of inputs)if(manifest?.inputs?.[input.key]?.primary===true)input.primary=true;
|
|
212
|
+
// An optional input a person gives when they run the Block (the dates and the song of a tribute video) is part of what
|
|
213
|
+
// it takes, so the map lists it (November 9, 2026, owner: "theres way more"): left empty, no default, the person may
|
|
214
|
+
// give it (not only the Block's own screen, `sources:["view"]`), not project context, and no earlier Step could supply it.
|
|
215
|
+
for(const input of inputs){
|
|
216
|
+
const port=manifest?.inputs?.[input.key],item=row.requirements.find(requirement=>requirement.key===input.key);
|
|
217
|
+
if(!port||input.required||input.state!=='not-used'||port.default!==undefined||port.context||item?.requirement?.context||input.sourcePlacementId)continue;
|
|
218
|
+
const sources=port.sources||item?.requirement?.sources;
|
|
219
|
+
if(sources&&!sources.includes('user'))continue;
|
|
220
|
+
if((item?.candidates||[]).some(candidate=>candidate.record?.source?.kind==='block'||String(candidate.kind||'').startsWith('conversion-block')))continue;
|
|
221
|
+
input.personGives=true;
|
|
222
|
+
}
|
|
207
223
|
const last=node?.data?.blockLastRun;
|
|
208
224
|
const usedInputs=inputEvidence(last?.capturedInputs,inputs,[manifest?.inputs]);
|
|
209
225
|
const retainedRaw=[...(node?.data?.blockUnusedResults||[]).filter(record=>!record.previouslyApplied&&record.packageDigest===node?.data?.blockDigest).map(record=>({record,kind:'unused'})),...(node?.data?.blockSegmentResults||[]).filter(record=>supersededRuns.has(record.runId)&&record.packageDigest===node?.data?.blockDigest&&!(node?.data?.blockSegmentSelection?.runIds||[]).includes(record.runId)).map(record=>({record,kind:'connected'})),...(node?.data?.blockBatchResults||[]).filter(record=>record.unusedAfterSourceChange===true&&record.digest===node?.data?.blockDigest&&record.key!==last?.batchKey).map(record=>({record,kind:'batch'})),...(loopRetainedByPlacement.get(row.id)||[])];
|
|
@@ -257,9 +273,21 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
|
|
|
257
273
|
const appliedActivity=last?.manualSelection?{applied:true,at:last.at||null,runId:null,...(selectedManually?{selection:selectedManually}:{})}:last?{applied:true,at:last.at||null,runId:last.executionRunId||last.semanticRunId||last.segmentRunId||last.runId||null,skipped:last.skipped===true&&!last.failureRecovery,execution:executionEvidence(last,{conditional:!!stage.condition,outputs:node?.data?.blockOutputs||{}})}:null;
|
|
258
274
|
const condition=stage.condition?{inputKey:stage.condition.input,inputLabel:manifest?.inputs?.[stage.condition.input]?.description||stage.condition.input,path:stage.condition.path.slice(0,8),operator:stage.condition.operator,outcome:last?(last.skipped===true&&!last.failureRecovery?'skipped':'ran'):'unknown'}:null;
|
|
259
275
|
const latestFailure=(node?.data?.blockFailedAttempts||[]).slice(-1)[0],failureIsLatest=typeof latestFailure?.runId==='string'&&Number.isFinite(Date.parse(latestFailure.at))&&(!last?.at||latestFailure.at>last.at)&&!failureRecovered(latestFailure),failedActivity=failureIsLatest?{runId:latestFailure.runId.slice(0,160),at:latestFailure.at,code:failureCode(latestFailure),durationMs:Number.isSafeInteger(latestFailure.diagnostic?.durationMs)?Math.max(0,Math.min(86400000,latestFailure.diagnostic.durationMs)):null,...Object.fromEntries(['attempt','batchIndex','batchCount'].filter(key=>failedCounter(latestFailure[key])).map(key=>[key,latestFailure[key]]))}:null;
|
|
276
|
+
// An empty input the person types or adds themself is asked where they run the Block, on its own screen or its Step's
|
|
277
|
+
// form, so it is how every Block starts, not something the project needs from them (November 8, 2026, owner on
|
|
278
|
+
// Generate's Prompt: "i dont like how this is in my face always, like no shit i have to give it a prompt", then "make
|
|
279
|
+
// sure it was platform fix not just this one block"). Only when nothing was chosen, a person may give it, no earlier
|
|
280
|
+
// Step could supply it (then which one is a real question), it is not project context, and no Automation runs the
|
|
281
|
+
// project. A chosen source that stopped working still needs a fix. The Step is then like any Step that has not run
|
|
282
|
+
// (waiting; Chat reads ready as "worked in, never run"), and its card says it is ready to fill in and run.
|
|
283
|
+
const askedWhenRun=unattended?new Set():new Set(inputs.filter(input=>{
|
|
284
|
+
const item=row.requirements.find(requirement=>requirement.key===input.key);
|
|
285
|
+
return input.state==='needs-choice'&&item?.state==='needs-user-input'&&!input.explicit&&!input.sourcePlacementId&&!item.requirement?.context&&(!item.requirement?.sources||item.requirement.sources.includes('user'))&&!(item.candidates||[]).some(candidate=>candidate.record?.source?.kind==='block'||String(candidate.kind||'').startsWith('conversion-block'));
|
|
286
|
+
}).map(input=>input.key));
|
|
287
|
+
const askedOnly=askedWhenRun.size>0&&row.state!=='broken'&&inputs.every(input=>input.state!=='needs-choice'||askedWhenRun.has(input.key));
|
|
260
288
|
// A host workspace (the Editor) is worked in, never run, so with nothing to choose it is ready, not waiting (November 1,
|
|
261
289
|
// 2026, live: Chat read "waiting" as the Editor having no clip and would not export).
|
|
262
|
-
return {id:row.id,nodeId:stageNodeId(stage),label:row.label,...(stepOf.has(row.id)?{step:stepOf.get(row.id)}:{}),blockId:stage.blockId,version:stage.version,...(manifest?.permissions?.capabilities?.includes('connection.execute')?{connector:true}:{}),transformation:String(manifest?.description||'').slice(0,500),enabled:stage.enabled!==false,state:node?.data?.blockPrivacy?'restricted':active?'working':row.paused?'paused':node?.data?.blockStale||last&&last.digest!==node?.data?.blockDigest?'needs-updating':failureIsLatest?(failureCode(latestFailure)==='EXECUTION_FAILED'?'failed':'needs-recovery'):last?.skipped===true&&!last?.failureRecovery&&stage.condition?'skipped':last?'finished':row.state==='broken'||row.state==='needs-user-input'?'needs-choice':manifest?.kind==='workspace'&&manifest?.runtime==='host'?'ready':'waiting',...(condition?{condition}:{}),inputs:inputs.map(input=>({...input,valueType:manifest?.inputs?.[input.key]?.type||row.requirements.find(requirement=>requirement.key===input.key)?.requirement?.type||null})),outputs,unusedResults,unusedResultsRevision,unusedResultsCount,unusedResultsOffset,unusedResultsLimit,unusedResultsNextOffset,...(retainedPage?.candidateId?{inspectedRetainedResult}:{}),usedInputs,activity:active?{...(appliedActivity||{}),active,activeRuns,activeRunsOmitted:Math.max(0,(runsByPlacement.get(row.id)||[]).length-activeRuns.length)}:failedActivity?{...(appliedActivity||{}),failedAttempt:failedActivity}:appliedActivity,history:recentHistory,historyComplete,retainedLoopRuns:retainedWorkflowLoopSummaries(canvas,row.id)};
|
|
290
|
+
return {id:row.id,nodeId:stageNodeId(stage),label:row.label,...(stepOf.has(row.id)?{step:stepOf.get(row.id)}:{}),blockId:stage.blockId,version:stage.version,...(manifest?.permissions?.capabilities?.includes('connection.execute')?{connector:true}:{}),transformation:String(manifest?.description||'').slice(0,500),enabled:stage.enabled!==false,state:node?.data?.blockPrivacy?'restricted':active?'working':row.paused?'paused':node?.data?.blockStale||last&&last.digest!==node?.data?.blockDigest?'needs-updating':failureIsLatest?(failureCode(latestFailure)==='EXECUTION_FAILED'?'failed':'needs-recovery'):last?.skipped===true&&!last?.failureRecovery&&stage.condition?'skipped':last?'finished':row.state==='broken'||row.state==='needs-user-input'&&!askedOnly?'needs-choice':manifest?.kind==='workspace'&&manifest?.runtime==='host'?'ready':'waiting',...(condition?{condition}:{}),inputs:inputs.map(input=>({...input,...(askedWhenRun.has(input.key)?{askedWhenRun:true,source:'you, when you run it',reason:'The person gives this where they run the Block, on its screen or its Step’s form.'}:{}),valueType:manifest?.inputs?.[input.key]?.type||row.requirements.find(requirement=>requirement.key===input.key)?.requirement?.type||null})),outputs,unusedResults,unusedResultsRevision,unusedResultsCount,unusedResultsOffset,unusedResultsLimit,unusedResultsNextOffset,...(retainedPage?.candidateId?{inspectedRetainedResult}:{}),usedInputs,activity:active?{...(appliedActivity||{}),active,activeRuns,activeRunsOmitted:Math.max(0,(runsByPlacement.get(row.id)||[]).length-activeRuns.length)}:failedActivity?{...(appliedActivity||{}),failedAttempt:failedActivity}:appliedActivity,history:recentHistory,historyComplete,retainedLoopRuns:retainedWorkflowLoopSummaries(canvas,row.id)};
|
|
263
291
|
});
|
|
264
292
|
if(retainedResultPage){const placement=placements.find(item=>item.id===retainedResultPage.placementId);if(!placement||placement.unusedResultsRevision!==retainedResultPage.revision||retainedResultPage.operation==='list'&&retainedResultPage.offset>placement.unusedResultsCount||retainedResultPage.candidateId&&!placement.inspectedRetainedResult)throw new Error('This retained result list or exact candidate changed. Inspect Result again.');}
|
|
265
293
|
if(receiptInputPage){const entry=placements.find(item=>item.id===receiptInputPage.placementId)?.history?.find(item=>projectFlowRunReceiptIdentity(item)===receiptInputPage.receiptId);if(!entry||entry.inputsRevision!==receiptInputPage.revision||!entry.inputs||receiptInputPage.offset>entry.inputsCount)throw new Error('This retained receipt or its captured inputs changed. Inspect the run again.');}
|
|
@@ -286,7 +314,7 @@ export function inspectProjectFlow(type,canvas={nodes:[],edges:[]},{projectId=nu
|
|
|
286
314
|
if(placement.condition)conditionalLabels.push(`Conditional Block · last recorded ${placement.condition.outcome==='unknown'?'not run':placement.condition.outcome}`);
|
|
287
315
|
return {from:input.sourcePlacementId,outputKey:input.sourceOutputKey,to:placement.id,inputKey:input.key,state:input.state,materialized:input.materialized===true,sourceRunId:input.pinRunId||sourceOutput?.runId||null,adapter:input.adapter,...(conditionalLabels.length?{conditionalLabel:conditionalLabels.join(' · ')}:{})};
|
|
288
316
|
}));
|
|
289
|
-
return projectFlowSnapshot({contractVersion:PROJECT_FLOW_CONTRACT_VERSION,projectId,definition:{id:type.id,version:type.version},...(type.groups?.length?{jobs:type.groups.map(group=>({id:group.id,label:group.label,parts:[...group.stages]}))}:{}),acceptanceCriteria:(type.acceptanceCriteria||[]).map(item=>({...item,verified:false})),goalSourceMaterials:validateGoalSourceMaterials(type),goalSourceInputs:currentGoalSourceInputs(type,[...BUILTIN_BLOCKS,...catalog]),placements,edges,issues:audit.issues.filter(issue=>!(issue.state==='needs-user-input'&&placements.find(placement=>placement.id===issue.stageId)?.inputs?.some(input=>input.key===issue.key&&input.state==='ready'))).slice(0,50).map(issue=>({placementId:issue.stageId||null,inputKey:issue.key||null,...(issue.state?{state:issue.state}:{}),message:issue.message||issue.reason||'Connection needs attention'})),partial:audit.requirementsInspected===false});
|
|
317
|
+
return projectFlowSnapshot({contractVersion:PROJECT_FLOW_CONTRACT_VERSION,projectId,definition:{id:type.id,version:type.version},...(type.groups?.length?{jobs:type.groups.map(group=>({id:group.id,label:group.label,parts:[...group.stages]}))}:{}),acceptanceCriteria:(type.acceptanceCriteria||[]).map(item=>({...item,verified:false})),goalSourceMaterials:validateGoalSourceMaterials(type),goalSourceInputs:currentGoalSourceInputs(type,[...BUILTIN_BLOCKS,...catalog]),placements,edges,issues:audit.issues.filter(issue=>!(issue.state==='needs-user-input'&&placements.find(placement=>placement.id===issue.stageId)?.inputs?.some(input=>input.key===issue.key&&(input.state==='ready'||input.askedWhenRun===true)))).slice(0,50).map(issue=>({placementId:issue.stageId||null,inputKey:issue.key||null,...(issue.state?{state:issue.state}:{}),message:issue.message||issue.reason||'Connection needs attention'})),partial:audit.requirementsInspected===false});
|
|
290
318
|
}
|
|
291
319
|
|
|
292
320
|
// A read-only explanation of the current handoff. Each hop is an inspected
|
|
@@ -10,8 +10,22 @@ export function isCanvasListing(item) {
|
|
|
10
10
|
return (item.kind==='block' && item.id==='sm.pipeline') || (item.kind==='type' && item.id==='sm.blank');
|
|
11
11
|
}
|
|
12
12
|
export function listingPreviewPath(item){return `/api/public-listings/${item.kind}/${encodeURIComponent(item.id)}/${encodeURIComponent(item.version)}/preview.png?cover=working-3`;}
|
|
13
|
-
|
|
14
|
-
export function
|
|
13
|
+
// `image` picks a listing image other than the cover (index 1); the cover keeps its old URL.
|
|
14
|
+
export function listingThumbnailPath(item,width=640,image=0){return `/api/public-listings/${item.kind}/${encodeURIComponent(item.id)}/${encodeURIComponent(item.version)}/preview.webp?cover=working-3&width=${width}${image>0?`&image=${image}`:''}`;}
|
|
15
|
+
export function listingThumbnailSrcSet(item,image=0){return [320,640,960].map(width=>`${listingThumbnailPath(item,width,image)} ${width}w`).join(', ');}
|
|
16
|
+
// A listing's media as counts for its card ({listingMedia:{images,videos}}, or nothing): from the saved
|
|
17
|
+
// counts of a creator listing or the saved key lists of a built-in one. Storage keys never leave the server.
|
|
18
|
+
export function listingMediaCounts(media){
|
|
19
|
+
const count=(value,max)=>Math.min(max,Array.isArray(value)?value.filter(entry=>typeof entry==='string').length:Number.isInteger(value)&&value>0?value:0),images=count(media?.images,2),videos=count(media?.videos,4);
|
|
20
|
+
return images||videos?{listingMedia:{images,videos}}:{};
|
|
21
|
+
}
|
|
22
|
+
// What a Marketplace card slides through: the listing's own videos first (what it makes, moving), then its
|
|
23
|
+
// images as thumbnails. `listingMedia` holds counts only ({images, videos}); the paths are the public media routes.
|
|
24
|
+
export function listingCardSlides(item){
|
|
25
|
+
const media=item?.listingMedia||{},count=(value,max)=>Math.max(0,Math.min(max,Number.isInteger(value)?value:0)),base=`/api/public-listings/${item.kind}/${encodeURIComponent(item.id)}/${encodeURIComponent(item.version)}`;
|
|
26
|
+
return [...Array.from({length:count(media.videos,4)},(_,index)=>({kind:'video',key:`video-${index}`,src:`${base}/video-${index}`})),
|
|
27
|
+
...Array.from({length:count(media.images,2)},(_,index)=>({kind:'image',key:`image-${index}`,src:listingThumbnailPath(item,640,index),srcSet:listingThumbnailSrcSet(item,index)}))];
|
|
28
|
+
}
|
|
15
29
|
|
|
16
30
|
export function hostSourcePath({id,version}) {return `/block-sdk/host/${encodeURIComponent(id)}${version==='1.0.0' ? '' : '/'+encodeURIComponent(version)}.zip`;}
|
|
17
31
|
|
|
@@ -201,5 +201,8 @@ export function replaceStep(type,id,pkg,catalog=[]) {
|
|
|
201
201
|
const packageIndex=packages.findIndex(item=>stableStringify(item)===stableStringify(pkg));
|
|
202
202
|
if(packageIndex>=0)replacement.packageIndex=packageIndex;
|
|
203
203
|
if(!Object.keys(config).length)delete replacement.config;
|
|
204
|
-
|
|
204
|
+
// A wire to or from a port the new Block does not have cannot follow it (November 7, 2026: swapping Voiceover 3.0.0, whose
|
|
205
|
+
// voice and timings the Type wires on, failed with "Connection references missing stage or port").
|
|
206
|
+
const follows=edge=>edge.to?.stage===id?Object.hasOwn(manifest.inputs||{},edge.to.port):edge.from?.stage===id?Object.hasOwn(manifest.outputs||{},edge.from.port):true;
|
|
207
|
+
return changeSteps({...type,packages,connections:(type.connections||[]).filter(follows)},type.stages.map(step=>step.id===id?replacement:step),catalog);
|
|
205
208
|
}
|
|
@@ -99,10 +99,10 @@ export function scanPackageSource(pkg) {
|
|
|
99
99
|
else results.push({name:'Static code review',passed:true,message:pkg.manifest.runtime === 'comfyui' ? 'Only reviewed core image nodes and bounded typed bindings; no custom Python, dependencies, credentials, or paths' : pkg.manifest.runtime === 'capability' ? `Bounded declarative ${pkg.capability.operation} request only; provider, model, payment and approval belong to the host. No live execution has been verified.` : 'Only allowlisted recipe operations and bounded JSON are accepted'});
|
|
100
100
|
if(pkg.view){
|
|
101
101
|
results.push({name:'Sandbox interface review',passed:true,message:pkg.view.runtime==='frame'?'Frame view: any interface code, compiled with task guards and run in an opaque-origin frame with no network, generated code, navigation, popups or WebAssembly':'Bounded HTML controls and reviewed standalone UI JavaScript; opaque-origin frame, no navigation or remote resources'});
|
|
102
|
-
const design=reviewViewDesign(pkg.view,{mode:interfaceMode(pkg.manifest)}),notes=[...design.issues,...design.warnings];
|
|
102
|
+
const design=reviewViewDesign(pkg.view,{mode:interfaceMode(pkg.manifest),manifest:pkg.manifest}),notes=[...design.issues,...design.warnings];
|
|
103
103
|
results.push({name:'Task interface design',passed:design.passed,message:notes.length?notes.join(' '):'The task object leads, choices are bounded, and secondary detail stays progressively disclosed.'});
|
|
104
104
|
}
|
|
105
|
-
try {validateViewAppearance(pkg.view);results.push({name:'Appearance compatibility',passed:true,message:pkg.view?.appearance==='original'?'Interface preserves its declared original colors; StillMade theming is optional. Sandbox and shared-state checks still apply.':'Interface follows Light, Dark and White; original preview colors are preserved'});} catch(error) {results.push(pkg.view?.runtime==='frame'?{name:'Appearance compatibility',passed:true,advisory:true,message:`Advice: ${error.message} Frame views receive the theme variables; using them keeps the interface right in Light, Dark and White.`}:{name:'Appearance compatibility',passed:false,message:error.message});}
|
|
105
|
+
try {validateViewAppearance(pkg.view);results.push({name:'Appearance compatibility',passed:true,message:pkg.view?.appearance==='none'?'Interface keeps its own style: StillMade adds none of its theme, colors or Block kit. Sandbox, shared-state, input and output checks still apply.':pkg.view?.appearance==='original'?'Interface preserves its declared original colors; StillMade theming is optional. Sandbox and shared-state checks still apply.':'Interface follows Light, Dark and White; original preview colors are preserved'});} catch(error) {results.push(pkg.view?.runtime==='frame'?{name:'Appearance compatibility',passed:true,advisory:true,message:`Advice: ${error.message} Frame views receive the theme variables; using them keeps the interface right in Light, Dark and White.`}:{name:'Appearance compatibility',passed:false,message:error.message});}
|
|
106
106
|
return {...summary(results),...(['capability','module'].includes(pkg.manifest.runtime)?{execution:'not-run',liveVerified:false,reviewRequired:true}:{})};
|
|
107
107
|
}
|
|
108
108
|
// Execution safety is also used for immutable installed releases. New admission
|
|
@@ -3,10 +3,12 @@ import {plain,safeDocument,valueMatches} from './contracts.js';
|
|
|
3
3
|
const structured=['json','object','document','table','url','date','color','script','transcript','brief','bible','shot-plan','panel-document','board-document','scene-document','timeline','timeline-edit','workspace-edit','timeline-range','research','character','location','style','pipeline','scene','shot','mask','depth','pose','metadata','project-context'];
|
|
4
4
|
const editable=new Set(['text','number','integer','boolean','text[]','number[]','integer[]','boolean[]',...structured,...structured.filter(type=>!['json','object'].includes(type)).map(type=>`${type}[]`)]);
|
|
5
5
|
// Stable input inspection order survives JSONB persistence and source reload.
|
|
6
|
+
// An input only the Block's own screen fills (sources ['view']) is never Chat's to set.
|
|
7
|
+
const viewOnly=port=>Array.isArray(port.sources)&&!port.sources.some(source=>source!=='view');
|
|
6
8
|
export const agentInputEntries=manifest=>Object.entries(manifest.inputs||{}).sort(([a],[b])=>a<b?-1:a>b?1:0);
|
|
7
9
|
export function agentInputChoices(manifest,locked=[],{offset=0}={}){
|
|
8
10
|
let schemaBudget=12000,optionBudget=12000;
|
|
9
|
-
return agentInputEntries(manifest).slice(Math.max(0,Math.trunc(offset)||0),Math.max(0,Math.trunc(offset)||0)+50).filter(([id,port])=>editable.has(port.type)&&!port.context&&!locked.includes(id)).slice(0,50).map(([id,port])=>{
|
|
11
|
+
return agentInputEntries(manifest).slice(Math.max(0,Math.trunc(offset)||0),Math.max(0,Math.trunc(offset)||0)+50).filter(([id,port])=>editable.has(port.type)&&!port.context&&!viewOnly(port)&&!locked.includes(id)).slice(0,50).map(([id,port])=>{
|
|
10
12
|
const control=manifest.ui?.find(row=>row.port===id);
|
|
11
13
|
const offered=control?.options?.slice(0,50).map(option=>plain(option)?option.value:option),optionsText=offered?JSON.stringify(offered):'',options=optionsText&&optionsText.length<=Math.min(4000,optionBudget)?offered:undefined;
|
|
12
14
|
if(options)optionBudget-=optionsText.length;
|
|
@@ -19,7 +21,7 @@ export function agentInputChoices(manifest,locked=[],{offset=0}={}){
|
|
|
19
21
|
export function parseAgentInput(manifest,args,{locked=[],busy=false,disabled=false}={}){
|
|
20
22
|
if(!plain(args)||Object.keys(args).length!==2||!Object.hasOwn(args,'field')||!Object.hasOwn(args,'value')||typeof args.field!=='string')throw new Error('Choose an input and provide its value.');
|
|
21
23
|
const port=manifest.inputs?.[args.field];
|
|
22
|
-
if(busy||disabled||!port||port.context||locked.includes(args.field))throw new Error('This input is locked or the Block is busy.');
|
|
24
|
+
if(busy||disabled||!port||port.context||viewOnly(port)||locked.includes(args.field))throw new Error('This input is locked or the Block is busy.');
|
|
23
25
|
if(!editable.has(port.type))throw new Error('Use the matching media or project-item control for this input.');
|
|
24
26
|
// A script or brief takes plain text too, stored the way its text control stores it.
|
|
25
27
|
if(['script','brief'].includes(port.type)&&typeof args.value==='string')args={...args,value:{schemaVersion:1,text:args.value}};
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import {assert} from './contracts.js';
|
|
2
|
+
// host: StillMade's theme (the default); original: the view's own colors over StillMade's page and Block kit; none: an
|
|
3
|
+
// app's own style with nothing of StillMade's (owner, November 9, 2026: software mode "can also choose not to use our
|
|
4
|
+
// style"). Only an app (manifest.kind "workspace") may set none; validatePackage checks it against the manifest.
|
|
5
|
+
export const VIEW_APPEARANCES=Object.freeze(['host','original','none']);
|
|
2
6
|
// Words that may not appear anywhere in view JavaScript: interface state changes use CSS classes.
|
|
3
7
|
export const VIEW_STYLE_WORDS=Object.freeze(['style','cssText','setProperty','removeProperty','insertRule','deleteRule','styleSheets','adoptedStyleSheets','animate']);
|
|
4
8
|
|
|
@@ -33,16 +37,27 @@ function checkCss(css,{original=false}={}) {
|
|
|
33
37
|
assert(words.every(word=>allowed.has(word)) && !/#|\b(?:rgb|hsl|hwb|lab|lch|oklab|oklch|color|var)\s*\(/i.test(reduced),'APPEARANCE',`Use semantic theme tokens for ${property}; keep original colors in view.css and provide view.themedCss.`);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
40
|
+
// Theme colors live in reviewed stylesheets, not mutable UI code. Canvas pixel
|
|
41
|
+
// drawing remains available: media content does not inherit interface colors.
|
|
42
|
+
const assertNoCodeStyles=view=>assert(!new RegExp(`\\b(?:${VIEW_STYLE_WORDS.join('|')})\\b`).test(view.javascript || ''),'APPEARANCE','Use CSS classes for interface changes; put themed colors in the stylesheet instead of setting styles in JavaScript.');
|
|
36
43
|
export function validateViewAppearance(view,{appearance=view?.appearance ?? 'host'}={}) {
|
|
37
44
|
if(!view)return true; // Generated SDK controls inherit StillMade automatically.
|
|
38
|
-
assert(
|
|
45
|
+
assert(VIEW_APPEARANCES.includes(appearance),'APPEARANCE','View appearance must be host, original or none.');
|
|
46
|
+
// StillMade adds none of its theme to an app with its own style, so the theme rules below are not its: its own colors,
|
|
47
|
+
// tokens, filters, blending and animations are fine. A standard view still keeps what guards validateView's
|
|
48
|
+
// no-resources check: no CSS escapes (one could spell a url()) and no styles set from code. A frame view runs any code.
|
|
49
|
+
if(appearance==='none'){
|
|
50
|
+
if(view.runtime==='frame')return true;
|
|
51
|
+
const inline=[...view.html.matchAll(/\sstyle\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)].map(match=>match[1]??match[2]??match[3]);
|
|
52
|
+
for(const css of [view.css,view.themedCss,...inline])assert(!String(css??'').replace(/\/\*[\s\S]*?\*\//g,'').includes('\\'),'APPEARANCE','A standard view\'s CSS cannot use escapes; write the characters themselves.');
|
|
53
|
+
assertNoCodeStyles(view);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
39
56
|
const original=appearance==='original';
|
|
40
57
|
assert(!/\s(?:bgcolor|bordercolor|color|text|link|vlink|alink)\s*=/i.test(view.html),'APPEARANCE','Move presentational HTML colors into the original and themed stylesheets.');
|
|
41
58
|
checkCss(original ? view.css ?? '' : view.themedCss ?? view.css ?? '',{original});
|
|
42
59
|
if(original && view.themedCss!==undefined)checkCss(view.themedCss);
|
|
43
60
|
for(const attribute of view.html.matchAll(/\sstyle\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi))checkCss(attribute[1]??attribute[2]??attribute[3],{original});
|
|
44
|
-
|
|
45
|
-
// drawing remains available: media content does not inherit interface colors.
|
|
46
|
-
assert(!new RegExp(`\\b(?:${VIEW_STYLE_WORDS.join('|')})\\b`).test(view.javascript || ''),'APPEARANCE','Use CSS classes for interface changes; put themed colors in the stylesheet instead of setting styles in JavaScript.');
|
|
61
|
+
assertNoCodeStyles(view);
|
|
47
62
|
return true;
|
|
48
63
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import {HOSTED_OPERATION_NAMES} from './contracts.js';
|
|
10
10
|
|
|
11
11
|
export const BUILDER_CONTRACT_ID='stillmade.block-builder';
|
|
12
|
-
export const BUILDER_CONTRACT_VERSION='1.
|
|
12
|
+
export const BUILDER_CONTRACT_VERSION='1.21.0';
|
|
13
13
|
export const BUILDER_CONTRACT_STAMP=`${BUILDER_CONTRACT_ID}@${BUILDER_CONTRACT_VERSION}`;
|
|
14
14
|
export const BUILDER_CONTRACT_DOCS_PATH='/docs/build/block-builder-contract';
|
|
15
15
|
// Block names appear in step bars, cards and pickers; new releases keep them short.
|
|
@@ -86,14 +86,17 @@ const SECTIONS=Object.freeze([
|
|
|
86
86
|
// pictures its full-screen button; a video without controls stays the Block's own. Also that day (owner: "there is no
|
|
87
87
|
// separation and I cannot tell what is going on"): a pick is the kit's ink fill, and media that cannot show is a quiet tile.
|
|
88
88
|
rule('ui','block',
|
|
89
|
-
'Prefer `manifest.ui`; custom views use the Block kit (`sm-*`) unless a look is named. Pictures over words: preview each option before choosing; few words, fold the rest; buttons carry `data-stillmade-action`; the start button names the action (never "Run" or "Make it") and shows no price (StillMade adds it). A job fits one screen unscrolled and a 320px phone; an app (editor, configurator, CAD tool) sets `manifest.kind: "workspace"` and works like that app: full canvas, panels, tools, undo, never a form; checks then use the app rules. An app puts `data-stillmade-layout="own"` on its root: StillMade never centers or zooms it. `<video controls>` gets StillMade\'s player; leave out `controls` when the Block drives playback. A pick keeps the kit\'s ink fill; media that cannot show is a wordless `sm-media-off` tile. Every part shows at once: the result\'s place is drawn before the first run in its own shape and size, so nothing pops in; on wide screens inputs sit left and the result right; on phones they stack, the result after the inputs. Actions and status sit on the stage (`sm-actions`, `sm-status` in `sm-stage`); the run button stays rightmost, so making again never moves.',
|
|
90
|
-
'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, drawn before the first run in the shape and size of what is coming (`data-ratio="9:16"` for a vertical clip, lines for text, a player bar for sound) so every part of the screen is in place at once and the result fills a place the person already sees instead of popping in somewhere unexpected; StillMade draws it the same way on the Steps it draws; on a wide screen what goes in sits on the left and the result on the right, side by side, and they stack only on narrow screens and phones, the result\'s place after the inputs with the job\'s action row on it (`sm-actions` and `sm-status` inside `sm-stage`: the kit puts the status top-left and the actions top-right in the player\'s cream blocks, over a band above the media; the run button is always the rightmost, so a button that spends credits never moves and making again is pressed where it made the first time, with Download and other result actions to its left), picks as pictures in `sm-choices`, the picked one in the kit\'s ink fill (never restyled to an outline, a tint or the page\'s background) so it reads at a glance, one solid `sm-primary` naming the action in the job\'s words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade
|
|
89
|
+
'Prefer `manifest.ui`; custom views use the Block kit (`sm-*`) unless a look is named. Pictures over words: preview each option before choosing; few words, fold the rest; buttons carry `data-stillmade-action`; the start button names the action (never "Run" or "Make it") and shows no price (StillMade adds it) once the screen says what its next run makes (`StillMade.nextRun`). A job fits one screen unscrolled and a 320px phone; an app (editor, configurator, CAD tool) sets `manifest.kind: "workspace"` and works like that app: full canvas, panels, tools, undo, never a form; checks then use the app rules. An app puts `data-stillmade-layout="own"` on its root: StillMade never centers or zooms it, and may set view `appearance: "none"` to drop StillMade\'s style (theme, colors, Block kit); a job keeps it. `<video controls>` gets StillMade\'s player; leave out `controls` when the Block drives playback. A pick keeps the kit\'s ink fill; media that cannot show is a wordless `sm-media-off` tile. Every part shows at once: the result\'s place is drawn before the first run in its own shape and size, so nothing pops in; on wide screens inputs sit left and the result right; on phones they stack, the result after the inputs. Actions and status sit on the stage (`sm-actions`, `sm-status` in `sm-stage`); the run button stays rightmost, so making again never moves; a `data-stillmade-action="download"` button beside it saves what the stage shows.',
|
|
90
|
+
'If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction needs one. Build custom views from the Block kit the host injects (`block-kit.css`): the result in `sm-stage`, drawn before the first run in the shape and size of what is coming (`data-ratio="9:16"` for a vertical clip, lines for text, a player bar for sound) so every part of the screen is in place at once and the result fills a place the person already sees instead of popping in somewhere unexpected; StillMade draws it the same way on the Steps it draws; on a wide screen what goes in sits on the left and the result on the right, side by side, and they stack only on narrow screens and phones, the result\'s place after the inputs with the job\'s action row on it (`sm-actions` and `sm-status` inside `sm-stage`: the kit puts the status top-left and the actions top-right in the player\'s cream blocks, over a band above the media; the run button is always the rightmost, so a button that spends credits never moves and making again is pressed where it made the first time, with Download (`data-stillmade-action="download"`, which needs no code: StillMade saves the stage\'s picture or clip under the export policy) and other result actions to its left), picks as pictures in `sm-choices`, the picked one in the kit\'s ink fill (never restyled to an outline, a tint or the page\'s background) so it reads at a glance, one solid `sm-primary` naming the action in the job\'s words ("Plan the tour", "Generate voiceover"; never "Run" or "Make it") and no price (StillMade adds it) once the view says what its next run makes (`StillMade.nextRun`: its `media.generate` items or hosted operations), extras in a closed `sm-more`; a look the person names wins. Fit one laptop screen without scrolling: label beside control, status in a few words, files as tiles, rare tools as icons, (i) on unclear controls. These compact rules are for a job or an ingredient. Size the view to the Block\'s tier (block-tier.js): an app, a workspace a person works in for a while (video editor, 3D configurator, CAD or drawing tool, photo editor), declares `manifest.kind: "workspace"` and looks and works like that kind of app instead of a compact page, and StillMade\'s checks (admission, readiness, its own builder) then apply the app rules instead of the compact ones: no limit on controls in view, no Block kit page, its own layout, the craft\'s own words, and only its run button (`data-stillmade-action="run"`) held to naming what it makes: its canvas or stage fills the view, with the panels, toolbars, inspector or timeline such an app has, direct manipulation (select, drag, zoom), keyboard shortcuts and undo; it keeps the host appearance and Block kit controls unless it sets view `appearance: "none"` (`src/view.config.json`), which only an app may: StillMade then adds none of its theme, colors, fonts, page or Block kit, the app\'s own CSS styles everything from the browser\'s defaults and the style checks are its own call, while shared state, inputs and outputs, hosted operations, the run button\'s price and StillMade\'s picker work as before; its panels may scroll, and on a phone its tools fold into sheets while the canvas stays usable. Never shrink an app into a form of fields and one button. StillMade centers a standard view narrower than its Step and zooms a view to fill its space; a stage shows what it holds in its middle as one group (a caption stays with its picture) and several results fill its width as one row, on a Block\'s screen and on the pages StillMade draws; an app that lays itself out (full-size canvas or WebGL, its own scroll, pan or zoom, its own Ctrl/Cmd +/- keys) puts `data-stillmade-layout="own"` on its outermost element, and StillMade then never centers, zooms or takes those keys. StillMade draws one player for every `<video controls>` (play, timeline, time, sound and full screen, shown for 3 s the first time it comes into view, then while the pointer is on the video, with keys and touch) and a full-screen button on result pictures in `sm-stage` or `sm-gallery` and on any element marked `data-stillmade-expand` (the same value makes one set of pictures, `off` removes it); `data-stillmade-player="native"` keeps the browser\'s controls, and a `<video>` without `controls` is the Block\'s own and is never touched. A picture or clip that cannot show here (kept on another device, or it did not open) keeps its place as a quiet `sm-media-off` tile in its own shape with the `image-off` glyph and the reason in its label, never words in its place. Drop words nobody needs; put helpful ones in a closed `sm-hint`. Mark each button `data-stillmade-action` (`run` on the one that runs the Block, which the device checks press once; a page that checks first keeps `run` on that button for both presses; what it does on the others, `"play sample"`) and label each field: Chat presses and fills them. Expose the capability, not the implementation: sensible defaults, previews, progress, clear errors, visible outputs. Show no raw JSON, internal IDs, filesystem details, provider terms or developer settings unless the Block is a developer tool. When a person chooses among options they need to hear or see (voices, music, fonts, looks, templates, images), let them preview each option before choosing: a sample or thumbnail from the Block\'s own files or the host\'s free samples, never a paid generation. Skip previews only when there is nothing to preview. Choices StillMade owns (voices, models in the run review) already show samples. Follow the host appearance (Light, Dark and White) and one elastic layout: stacked on a 320px phone with no sideways scroll (the inputs, the button in normal flow, never pinned, then the result\'s place under it), and from 860px the controls left (≤380px), the result filling the right. Short settings sit side by side, each label beside its control (`sm-field`, `sm-settings`; three or four short options as one `data-segmented` row); many items are one line each (`sm-list`) that open in place; input files are small tiles (`sm-gallery data-size="small"`); on a phone chip rows scroll sideways and long lists show three. Compacting moves controls, never deletes them.'),
|
|
91
91
|
rule('multiplayer','block',
|
|
92
92
|
'StillMade shares every screen control (`data-stillmade-local` keeps one local); a view sharing its own state (`updateShared`) adds `onShared`, `canEdit`, no re-run on remote edits.',
|
|
93
93
|
'Projects are multiplayer, and StillMade builds it: every ordinary control on a Block\'s screen (text, number, range, checkbox, color, select, textarea and a kit choice group) is shared by itself, under its `data-stillmade-share`, else its `name`, else its `id`, including controls the screen adds later; the Block writes no sharing code. `StillMade.bindShared(field, elementId)` shares a control under another field name. Mark a truly device-only control (playback, hover, device selection), or an element around several, with `data-stillmade-local`. A view that publishes its own structured state (`StillMade.updateShared(values, before, options)` or `StillMade.connectDocument`) shares its controls itself: StillMade then steps aside, and it subscribes through `StillMade.onShared(callback)`; private variables and canvas drawings are not shared unless the view publishes them. Observe shared outputs as well as state, honor `canEdit` in every mutating callback (read-only participants cannot write), never run, click or call a paid integration in response to a remote update, and preserve conflicting drafts. Shared background work uses `sharedWork` with the `work.shared` permission.'),
|
|
94
94
|
rule('external-services','block',
|
|
95
95
|
'External services only via hosted operations or `connection.execute`; the host owns providers, keys, BYOK, credits and consent.',
|
|
96
96
|
'Reach external services only through StillMade\'s hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs), `stillmade.net.connect` for a live WebSocket to one of them, and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person\'s own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as outcomes the Block reports, not crashes: a call the result needs throws and fails the run (see Useful failures); an optional part\'s failure is caught, given its fallback and named in an optional `notes` text output. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user\'s StillMade credits at StillMade\'s prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly (threaded: `"threads": true` in module.json) or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade\'s theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.'),
|
|
97
|
+
rule('models-from-platform','block',
|
|
98
|
+
'Never name or list models: ask for shape, size and length; the person picks the model, and a picker draws from `StillMade.models()`.',
|
|
99
|
+
'A Block never names a model, in its capability, its module code or its screen, and never keeps a list of models: the person picks the model on the Step from the models StillMade runs that day, so models StillMade adds or retires later reach the Block with no new version (owner, November 8, 2026). Ask for what the result needs instead, in `settings` (`aspectRatio`, `resolution`, `quality`, `duration`, `generateAudio`): with no model picked StillMade runs its default model, or the first one that makes what was asked, and a value the chosen model does not make falls back to its own. A screen that offers its own model picker draws it from `StillMade.models(operation)` when it opens (each model\'s settings arrive as data, so a setting StillMade adds later draws too) and passes the picked `id` as `settings.model`. A model StillMade retires runs on the one that replaced it.'),
|
|
97
100
|
rule('dependencies','block',
|
|
98
101
|
'No npm or system dependencies; inline small licensed code and record its license and notice in the manifest; the person is the copyright holder.',
|
|
99
102
|
'A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. The copyright holder in the Block\'s own `provenance.notice` is the person it is made for: replace the placeholder `create` writes with their name, never StillMade, the SDK or yourself. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user\'s machine has it.'),
|
|
@@ -160,7 +163,7 @@ export const BUILDER_CONTRACT=Object.freeze({
|
|
|
160
163
|
gapClasses:GAP_CLASSES,
|
|
161
164
|
});
|
|
162
165
|
|
|
163
|
-
const SECTION_TITLES=Object.freeze({'runtime-choice':'Choose the runtime','grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','input-words':'Name inputs in the job\'s words','job-words':'Speak the job','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
166
|
+
const SECTION_TITLES=Object.freeze({'runtime-choice':'Choose the runtime','grounded-text':'Only supplied facts','source-of-truth':'Source of truth','reuse-first':'Reuse first','block-tier':'Three sizes of Block','block-scope':'One job per Step','capability-contract':'Define the capability first','feature-map':'Feature map','compose':'Blocks compose','platform-primitives':'Use platform primitives','no-workarounds':'No hidden workarounds','naming':'Name it briefly','input-words':'Name inputs in the job\'s words','job-words':'Speak the job','ui':'Interface','multiplayer':'Shared editing','external-services':'External services','models-from-platform':'Models come from StillMade','dependencies':'Dependencies','security':'Security','lifecycle':'Execution lifecycle','errors':'Useful failures','adapt-to-platform':'Adapt to the platform','verification':'What StillMade verifies','project-types':'Project Types','readiness-report':'Readiness report'});
|
|
164
167
|
const inScope=(scope,wanted)=>wanted==='all'||scope==='both'||scope===wanted;
|
|
165
168
|
|
|
166
169
|
/** Render the contract for a model prompt ('compact'), a guide ('full') or an SDK folder ('agents-md'). */
|
|
@@ -5,6 +5,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
5
5
|
"sm.ai-decide@1.0.0",
|
|
6
6
|
"sm.ai-write@1.0.0",
|
|
7
7
|
"sm.board-editor.hosted@3.0.0",
|
|
8
|
+
"sm.board-editor.hosted@3.1.0",
|
|
8
9
|
"sm.board-editor@2.0.0",
|
|
9
10
|
"sm.board-editor@2.1.0",
|
|
10
11
|
"sm.board-editor@2.2.0",
|
|
@@ -20,6 +21,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
20
21
|
"sm.board-editor@2.2.8",
|
|
21
22
|
"sm.board-editor@2.2.9",
|
|
22
23
|
"sm.board-editor@3.0.0",
|
|
24
|
+
"sm.board-editor@3.1.0",
|
|
23
25
|
"sm.captions@2.0.0",
|
|
24
26
|
"sm.character@2.0.0",
|
|
25
27
|
"sm.character@2.0.1",
|
|
@@ -32,6 +34,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
32
34
|
"sm.edit-assistant@2.0.1",
|
|
33
35
|
"sm.edit-assistant@2.0.2",
|
|
34
36
|
"sm.editor.hosted@3.0.0",
|
|
37
|
+
"sm.editor.hosted@3.1.0",
|
|
35
38
|
"sm.editor@2.0.0",
|
|
36
39
|
"sm.editor@2.1.0",
|
|
37
40
|
"sm.editor@2.2.0",
|
|
@@ -48,6 +51,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
48
51
|
"sm.editor@2.2.8",
|
|
49
52
|
"sm.editor@2.2.9",
|
|
50
53
|
"sm.editor@3.0.0",
|
|
54
|
+
"sm.editor@3.1.0",
|
|
51
55
|
"sm.export-audio@2.0.0",
|
|
52
56
|
"sm.export-timeline@2.0.0",
|
|
53
57
|
"sm.export-video@2.0.0",
|
|
@@ -70,6 +74,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
70
74
|
"sm.meta-creative@1.0.0",
|
|
71
75
|
"sm.motion-graphics@2.0.0",
|
|
72
76
|
"sm.panel-strip.hosted@3.0.0",
|
|
77
|
+
"sm.panel-strip.hosted@3.1.0",
|
|
73
78
|
"sm.panel-strip@2.0.0",
|
|
74
79
|
"sm.panel-strip@2.1.0",
|
|
75
80
|
"sm.panel-strip@2.2.0",
|
|
@@ -85,6 +90,7 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
85
90
|
"sm.panel-strip@2.4.1",
|
|
86
91
|
"sm.panel-strip@2.4.2",
|
|
87
92
|
"sm.panel-strip@3.0.0",
|
|
93
|
+
"sm.panel-strip@3.1.0",
|
|
88
94
|
"sm.photo-editor@1.0.0",
|
|
89
95
|
"sm.photo-notes@1.0.0",
|
|
90
96
|
"sm.photo-notes@1.1.0",
|
|
@@ -125,6 +131,8 @@ export const FIRST_PARTY_SDK_IDENTITIES=Object.freeze(new Set([
|
|
|
125
131
|
"sm.when@1.0.0",
|
|
126
132
|
"sm.when@1.1.0",
|
|
127
133
|
"sm.write-summary@1.0.0",
|
|
128
|
-
"stillmade.generate@1.0.0"
|
|
134
|
+
"stillmade.generate@1.0.0",
|
|
135
|
+
"stillmade.generate@1.1.0",
|
|
136
|
+
"stillmade.generate@1.2.0"
|
|
129
137
|
]));
|
|
130
138
|
export const isFirstPartySdkIdentity=manifest=>FIRST_PARTY_SDK_IDENTITIES.has(`${manifest?.id}@${manifest?.version}`);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import generate from '../../blocks/generate/package.js';
|
|
2
|
+
// 1.1.0 (November 8, 2026) draws its models from StillMade.models(); 1.0.0 baked a copy of the catalog and stays for exact pins.
|
|
3
|
+
// 1.2.0 (the same day) is the generator studio; 1.1.0 stays for exact pins.
|
|
4
|
+
import generateV100 from '../../blocks/generate/versions/1.0.0/package.js';
|
|
5
|
+
import generateV110 from '../../blocks/generate/versions/1.1.0/package.js';
|
|
2
6
|
import importMedia from '../../blocks/native/sm.import-media/versions/2.0.0/package.js';
|
|
3
7
|
import importMediaV201 from '../../blocks/native/sm.import-media/versions/2.0.1/package.js';
|
|
4
8
|
import importMediaV202 from '../../blocks/native/sm.import-media/versions/2.0.2/package.js';
|
|
@@ -6,6 +10,8 @@ import editorV229 from '../../blocks/native/sm.editor/versions/2.2.9/package.js'
|
|
|
6
10
|
import editorV2210 from '../../blocks/native/sm.editor/versions/2.2.10/package.js';
|
|
7
11
|
import editorV2211 from '../../blocks/native/sm.editor/versions/2.2.11/package.js';
|
|
8
12
|
import editorV300 from '../../blocks/native/sm.editor/versions/3.0.0/package.js';
|
|
13
|
+
// 3.1.0 (November 8, 2026): the Sound tab is back and its Audio Score runs as hosted calls; 3.0.0 stays for exact pins.
|
|
14
|
+
import editorV310 from '../../blocks/native/sm.editor/versions/3.1.0/package.js';
|
|
9
15
|
import editorV2212 from '../../blocks/native/sm.editor/versions/2.2.12/package.js';
|
|
10
16
|
import captions from '../../blocks/native/sm.captions/versions/2.0.0/package.js';
|
|
11
17
|
import exportTimeline from '../../blocks/native/sm.export-timeline/versions/2.0.0/package.js';
|
|
@@ -105,6 +111,8 @@ import panelStripV241 from '../../blocks/native/sm.panel-strip/versions/2.4.1/pa
|
|
|
105
111
|
import panelStripV242 from '../../blocks/native/sm.panel-strip/versions/2.4.2/package.js';
|
|
106
112
|
// 3.0.0 (November 6, 2026): the original Image Panels screen as an SDK module Block with a frame view.
|
|
107
113
|
import panelStripV300 from '../../blocks/native/sm.panel-strip/versions/3.0.0/package.js';
|
|
114
|
+
// 3.1.0 (November 8, 2026): its panel, style and clip model pickers list the models StillMade runs now (StillMade.models).
|
|
115
|
+
import panelStripV310 from '../../blocks/native/sm.panel-strip/versions/3.1.0/package.js';
|
|
108
116
|
import boardEditorV210 from '../../blocks/native/sm.board-editor/versions/2.1.0/package.js';
|
|
109
117
|
import boardEditorV220 from '../../blocks/native/sm.board-editor/versions/2.2.0/package.js';
|
|
110
118
|
import boardEditorV221 from '../../blocks/native/sm.board-editor/versions/2.2.1/package.js';
|
|
@@ -119,6 +127,8 @@ import boardEditorV229 from '../../blocks/native/sm.board-editor/versions/2.2.9/
|
|
|
119
127
|
import boardEditorV2210 from '../../blocks/native/sm.board-editor/versions/2.2.10/package.js';
|
|
120
128
|
import boardEditorV2211 from '../../blocks/native/sm.board-editor/versions/2.2.11/package.js';
|
|
121
129
|
import boardEditorV300 from '../../blocks/native/sm.board-editor/versions/3.0.0/package.js';
|
|
130
|
+
// 3.1.0 (November 8, 2026): its "+ Character Pose" model picker lists the models StillMade runs now (StillMade.models).
|
|
131
|
+
import boardEditorV310 from '../../blocks/native/sm.board-editor/versions/3.1.0/package.js';
|
|
122
132
|
|
|
123
133
|
import photoNotes from '../../blocks/native/sm.photo-notes/versions/1.0.0/package.js';
|
|
124
134
|
import photoNotesV110 from '../../blocks/native/sm.photo-notes/versions/1.1.0/package.js';
|
|
@@ -128,5 +138,5 @@ import photoEditor from '../../blocks/native/sm.photo-editor/versions/1.0.0/pack
|
|
|
128
138
|
// Exact, independently executable first-party releases. Host workspaces do not
|
|
129
139
|
// belong here. Changing implementation requires a new immutable version.
|
|
130
140
|
const freeze=value=>{if(value&&typeof value==='object'&&!Object.isFrozen(value)){Object.values(value).forEach(freeze);Object.freeze(value);}return value;};
|
|
131
|
-
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(generate),freeze(photoEditor),freeze(photoNotesV110),freeze(photoNotes),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV300),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV300),freeze(panelStripV242),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV300),freeze(boardEditorV2211),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(pipelineV226),freeze(pipelineV225),freeze(pipelineV224),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(voiceoverV300),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(writeSummary),freeze(automationV110),freeze(automationWhen),freeze(automationAiDecide),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(generateVideoV201),freeze(generateVideo),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222) ]);
|
|
141
|
+
export const FIRST_PARTY_SDK_PACKAGES = Object.freeze([freeze(generate),freeze(generateV110),freeze(generateV100),freeze(photoEditor),freeze(photoNotesV110),freeze(photoNotes),freeze(exportVideoV210),freeze(exportVideo),freeze(exportAudio),freeze(motionGraphics),freeze(audioScore),freeze(importMediaV202),freeze(importMediaV201),freeze(editorV310),freeze(editorV300),freeze(editorV2212),freeze(editorV2211),freeze(editorV2210),freeze(editorV229),freeze(editorV228),freeze(panelStripV310),freeze(panelStripV300),freeze(panelStripV242),freeze(panelStripV241),freeze(panelStripV240),freeze(panelStripV237),freeze(panelStripV236),freeze(boardEditorV310),freeze(boardEditorV300),freeze(boardEditorV2211),freeze(boardEditorV2210),freeze(boardEditorV229),freeze(pipelineV226),freeze(pipelineV225),freeze(pipelineV224),freeze(videoAssetsV214),freeze(videoAssetsV213),freeze(voiceoverV300),freeze(voiceoverV216),freeze(voiceoverV215),freeze(voiceoverV214),freeze(voiceoverV213),freeze(roughCutV202),freeze(roughCutV201),freeze(finalEditV202),freeze(finalEditV201),freeze(editAssistantV202),freeze(editAssistantV201),freeze(gradeV202),freeze(gradeV201),freeze(transitionV202),freeze(transitionV201),freeze(compareV202),freeze(compareV201),freeze(compare),freeze(grade),freeze(transition),freeze(exportTimeline),freeze(captions),freeze(importMedia),freeze(generateImageV210),freeze(generateImage),freeze(writeSummary),freeze(automationV110),freeze(automationWhen),freeze(automationAiDecide),freeze(connectApp),freeze(automationContactFields),freeze(automationAiWrite),freeze(automationMetaCreative),freeze(voiceover),freeze(voiceoverV210),freeze(voiceoverV211),freeze(voiceoverV212),freeze(generateVideoV201),freeze(generateVideo),freeze(characterV201),freeze(character),freeze(locationV201),freeze(location),freeze(styleV201),freeze(style),freeze(videoAssets),freeze(videoAssetsV210),freeze(videoAssetsV211),freeze(videoAssetsV212),freeze(roughCut),freeze(finalEdit),freeze(editAssistant),freeze(boardEditor),freeze(boardEditorV210),freeze(boardEditorV220),freeze(boardEditorV221),freeze(boardEditorV222),freeze(boardEditorV223),freeze(boardEditorV224),freeze(boardEditorV225),freeze(boardEditorV226),freeze(boardEditorV227),freeze(boardEditorV228),freeze(panelStripV200),freeze(panelStrip),freeze(panelStripV220),freeze(panelStripV230),freeze(panelStripV231),freeze(panelStripV232),freeze(panelStripV233),freeze(panelStripV234),freeze(panelStripV235),freeze(editor),freeze(editorV210),freeze(editorV220),freeze(editorV221),freeze(editorV222),freeze(editorV223),freeze(editorV224),freeze(editorV225),freeze(editorV226),freeze(editorV227),freeze(pipelineV200),freeze(pipeline),freeze(pipelineV220),freeze(pipelineV221),freeze(pipelineV223),freeze(pipelineV222) ]);
|
|
132
142
|
export const firstPartySdkPackage = manifest => FIRST_PARTY_SDK_PACKAGES.find(pkg => pkg.manifest.id === manifest?.id && pkg.manifest.version === manifest?.version);
|
|
@@ -40,6 +40,19 @@ export const VIDEO_GENERATION_MODELS=Object.freeze({
|
|
|
40
40
|
'wan-2.7':video('Wan 2.7',{resolutions:['720p','1080p'],durations:[2,3,4,5,6,7,8,9,10,11,12,13,14,15],lastFrame:true,requiresFirstFrame:true,seed:true}),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
// Model ids StillMade no longer offers, each pointing at the model that replaced it (owner, November 8, 2026: adding or
|
|
44
|
+
// retiring a model never makes anyone change a Block). A Block or a saved Step that names one runs on its replacement,
|
|
45
|
+
// with the replacement's nearest settings, and its run review shows the model that will run. Never take a model out of
|
|
46
|
+
// the lists above without adding it here: scripts/generation-model-history.test.js holds every id StillMade has offered.
|
|
47
|
+
// 'seedream-4.5' was never offered, but the Editor's own list used it for Seedream 4.5.
|
|
48
|
+
export const RETIRED_IMAGE_MODELS=Object.freeze({'seedream-4.5':'seedream-v4.5'});
|
|
49
|
+
export const RETIRED_VIDEO_MODELS=Object.freeze({});
|
|
50
|
+
const replacement=(models,retired,id)=>{for(let hops=0;typeof id==='string'&&!Object.hasOwn(models,id)&&Object.hasOwn(retired,id)&&hops<8;hops++)id=retired[id];return id;};
|
|
51
|
+
/** The image model that runs for this id: itself, or the model that replaced it. */
|
|
52
|
+
export const currentImageModel=id=>replacement(IMAGE_GENERATION_MODELS,RETIRED_IMAGE_MODELS,id);
|
|
53
|
+
/** The video model that runs for this id: itself, or the model that replaced it. */
|
|
54
|
+
export const currentVideoModel=id=>replacement(VIDEO_GENERATION_MODELS,RETIRED_VIDEO_MODELS,id);
|
|
55
|
+
|
|
43
56
|
// Axes a model has no knob for are fixed to 'default' so a settings object
|
|
44
57
|
// always has the same keys and its price stays deterministic.
|
|
45
58
|
export const DEFAULT_SETTING='default';
|
|
@@ -58,14 +71,35 @@ function settingsObject(value,keys,label){
|
|
|
58
71
|
return value;
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
// The settings a Block asked for, by the list each axis is checked against.
|
|
75
|
+
const IMAGE_AXES=Object.freeze({aspectRatio:'aspectRatios',resolution:'resolutions',quality:'qualities'});
|
|
76
|
+
const VIDEO_AXES=Object.freeze({aspectRatio:'aspectRatios',resolution:'resolutions',duration:'durations'});
|
|
77
|
+
const asked=value=>value!==undefined&&value!==DEFAULT_SETTING;
|
|
78
|
+
/** Whether a catalog model takes every setting asked for (an axis left out or 'default' always fits). */
|
|
79
|
+
export function modelTakes(kind,model,settings={}){
|
|
80
|
+
const entry=(kind==='video'?VIDEO_GENERATION_MODELS:IMAGE_GENERATION_MODELS)[model];
|
|
81
|
+
if(!entry||!plain(settings))return false;
|
|
82
|
+
return Object.entries(kind==='video'?VIDEO_AXES:IMAGE_AXES).every(([key,field])=>!asked(settings[key])||entry[field].includes(settings[key]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// November 8, 2026 (owner: "never name a model"): with no model named, a setting the default cannot make picks the first
|
|
86
|
+
// model here that makes it, as video does below. Nano Banana 2 makes every shape and size, so this is quality today.
|
|
87
|
+
export const IMAGE_SHAPE_MODELS=Object.freeze(['gpt-image-2.5-sunburst','gpt-image-2.5-flare','gpt-image-2.6-flare','gpt-image-2.6-sunburst','gpt-image-2','seedream-v4.5','kling-image-v3','nano-banana-pro','gpt-image-1.5']);
|
|
88
|
+
function defaultImageModel(raw){
|
|
89
|
+
if(modelTakes('image',DEFAULT_IMAGE_MODEL,raw))return DEFAULT_IMAGE_MODEL;
|
|
90
|
+
return IMAGE_SHAPE_MODELS.find(model=>IMAGE_GENERATION_MODELS[model]&&modelTakes('image',model,raw))??DEFAULT_IMAGE_MODEL;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Fill and check image settings. `strict` rejects a value the model does not accept instead of substituting its default.
|
|
94
|
+
* A retired model runs on its replacement with the nearest settings it takes; with no model named, the model is the default,
|
|
95
|
+
* or the first one that takes the settings asked for (IMAGE_SHAPE_MODELS). */
|
|
62
96
|
export function imageSettings(value,{strict=false}={}){
|
|
63
97
|
const raw=settingsObject(value,IMAGE_SETTING_KEYS,'Image');
|
|
64
|
-
const model=raw.model
|
|
98
|
+
const model=raw.model===undefined?defaultImageModel(raw):currentImageModel(raw.model),replaced=raw.model!==undefined&&model!==raw.model,entry=IMAGE_GENERATION_MODELS[model];
|
|
65
99
|
assert(entry,'CAPABILITY_SETTINGS',`Choose an image model from: ${Object.keys(IMAGE_GENERATION_MODELS).join(', ')}`);
|
|
66
100
|
const axis=(name,allowed,fallback)=>{
|
|
67
101
|
const chosen=pick(allowed,raw[name],fallback);
|
|
68
|
-
if(strict&&own(raw,name))assert(raw[name]===chosen,'CAPABILITY_SETTINGS',`${entry.label} accepts ${name} ${allowed.length?allowed.join(', '):DEFAULT_SETTING}`);
|
|
102
|
+
if(strict&&!replaced&&own(raw,name))assert(raw[name]===chosen,'CAPABILITY_SETTINGS',`${entry.label} accepts ${name} ${allowed.length?allowed.join(', '):DEFAULT_SETTING}`);
|
|
69
103
|
return chosen;
|
|
70
104
|
};
|
|
71
105
|
return {model,aspectRatio:axis('aspectRatio',entry.aspectRatios,entry.aspectRatios.includes('1:1')?'1:1':undefined),resolution:axis('resolution',entry.resolutions),quality:axis('quality',entry.qualities,entry.qualities.includes('medium')?'medium':undefined)};
|
|
@@ -86,11 +120,11 @@ function defaultVideoModel(raw,{firstFrame,lastFrame}){
|
|
|
86
120
|
* the one that makes the asked aspect ratio (VIDEO_SHAPE_MODELS). */
|
|
87
121
|
export function videoSettings(value,{strict=false,firstFrame=false,lastFrame=false}={}){
|
|
88
122
|
const raw=settingsObject(value,VIDEO_SETTING_KEYS,'Video');
|
|
89
|
-
const model=raw.model
|
|
123
|
+
const model=raw.model===undefined?defaultVideoModel(raw,{firstFrame,lastFrame}):currentVideoModel(raw.model),replaced=raw.model!==undefined&&model!==raw.model,entry=VIDEO_GENERATION_MODELS[model];
|
|
90
124
|
assert(entry,'CAPABILITY_SETTINGS',`Choose a video model from: ${Object.keys(VIDEO_GENERATION_MODELS).join(', ')}`);
|
|
91
125
|
const axis=(name,allowed,fallback)=>{
|
|
92
126
|
const chosen=pick(allowed,raw[name],fallback);
|
|
93
|
-
if(strict&&own(raw,name))assert(raw[name]===chosen,'CAPABILITY_SETTINGS',`${entry.label} accepts ${name} ${allowed.length?allowed.join(', '):DEFAULT_SETTING}`);
|
|
127
|
+
if(strict&&!replaced&&own(raw,name))assert(raw[name]===chosen,'CAPABILITY_SETTINGS',`${entry.label} accepts ${name} ${allowed.length?allowed.join(', '):DEFAULT_SETTING}`);
|
|
94
128
|
return chosen;
|
|
95
129
|
};
|
|
96
130
|
const resolution=axis('resolution',entry.resolutions,entry.resolutions.includes('720p')?'720p':undefined);
|
|
@@ -101,6 +135,22 @@ export function videoSettings(value,{strict=false,firstFrame=false,lastFrame=fal
|
|
|
101
135
|
return {model,duration,resolution,aspectRatio:axis('aspectRatio',entry.aspectRatios,entry.aspectRatios.includes('16:9')?'16:9':undefined),generateAudio:raw.generateAudio??true};
|
|
102
136
|
}
|
|
103
137
|
|
|
138
|
+
/**
|
|
139
|
+
* A Block's settings as StillMade runs them (owner, November 8, 2026: a setting never breaks a run): only the keys the
|
|
140
|
+
* kind has, a retired model mapped forward, a model never offered left out, and a value the model that runs does not
|
|
141
|
+
* make left out so that model's own applies. Running it again changes nothing. Undefined when nothing is left.
|
|
142
|
+
*/
|
|
143
|
+
export function runnableSettings(kind,value,{firstFrame=false,lastFrame=false}={}){
|
|
144
|
+
if(!plain(value))return undefined;
|
|
145
|
+
const video=kind==='video',keys=video?VIDEO_SETTING_KEYS:IMAGE_SETTING_KEYS,models=video?VIDEO_GENERATION_MODELS:IMAGE_GENERATION_MODELS;
|
|
146
|
+
const kept=Object.fromEntries(Object.entries(value).filter(([key,item])=>keys.includes(key)&&item!==undefined&&item!==null&&item!==''));
|
|
147
|
+
if(kept.model!==undefined){const model=typeof kept.model==='string'?(video?currentVideoModel(kept.model):currentImageModel(kept.model)):undefined;if(models[model])kept.model=model;else delete kept.model;}
|
|
148
|
+
if(kept.generateAudio!==undefined&&typeof kept.generateAudio!=='boolean')delete kept.generateAudio;
|
|
149
|
+
const runs=video?videoSettings(kept,{firstFrame,lastFrame}):imageSettings(kept);
|
|
150
|
+
for(const key of Object.keys(kept))if(key!=='model'&&runs[key]!==kept[key])delete kept[key];
|
|
151
|
+
return Object.keys(kept).length?kept:undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
104
154
|
// Voices, music and sound effects on the app's own generators.
|
|
105
155
|
export const AUDIO_GENERATION=Object.freeze({
|
|
106
156
|
speech:Object.freeze({text:3800,defaultVoice:'asteria',speed:Object.freeze({min:.25,max:4,default:1})}),
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The models StillMade runs right now, for a Block's own screen: `StillMade.models(operation)` (owner, November 8, 2026:
|
|
2
|
+
// "as I add more models, it all gets updated without updating the Block"). A screen that offers a model picker asks for
|
|
3
|
+
// this list when it opens and draws it, instead of keeping a copy in its code that freezes the day the Block is built.
|
|
4
|
+
// StillMade answers from the same list a Step's model picker shows (the host's /options), so a model added or retired on
|
|
5
|
+
// StillMade appears or disappears on every Block at once. Each model lists its settings as data, so a screen draws a
|
|
6
|
+
// setting StillMade adds later without a new version.
|
|
7
|
+
import {CAPABILITY_RUN_OPERATIONS} from './contracts.js';
|
|
8
|
+
import {IMAGE_GENERATION_MODELS,VIDEO_GENERATION_MODELS,DEFAULT_SETTING,DEFAULT_IMAGE_MODEL,DEFAULT_VIDEO_MODEL,imageSettings,videoSettings} from './generation-catalog.js';
|
|
9
|
+
|
|
10
|
+
/** What a media.generate item's operation asks of the list. */
|
|
11
|
+
export const MEDIA_GENERATE_MODEL_OPERATIONS=Object.freeze({image:'image.generate',video:'video.generate',speech:'audio.speech',music:'audio.music',sfx:'audio.sfx'});
|
|
12
|
+
const SETTING_LABELS=Object.freeze({aspectRatio:'Shape',resolution:'Size',quality:'Quality',duration:'Length (seconds)',generateAudio:'Sound'});
|
|
13
|
+
|
|
14
|
+
/** The operations a Block may ask about: the hosted operations it declares, and what its media actions make. */
|
|
15
|
+
export function modelOperationsFor(manifest){
|
|
16
|
+
const permissions=manifest?.permissions||{},declared=new Set((permissions.capabilities||[]).filter(operation=>CAPABILITY_RUN_OPERATIONS.includes(operation)));
|
|
17
|
+
const actions=permissions.actions||[];
|
|
18
|
+
if(actions.includes('media.generate'))for(const operation of Object.values(MEDIA_GENERATE_MODEL_OPERATIONS))declared.add(operation);
|
|
19
|
+
if(actions.includes('image.generate')||actions.includes('image.generate.batch'))declared.add('image.generate');
|
|
20
|
+
return [...declared];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// One catalog model's settings, as data: each setting it takes, its values and the value a run uses when none is chosen.
|
|
24
|
+
function catalogEntry(kind,id){
|
|
25
|
+
const image=kind==='image',entry=(image?IMAGE_GENERATION_MODELS:VIDEO_GENERATION_MODELS)[id];if(!entry)return null;
|
|
26
|
+
const defaults=image?imageSettings({model:id}):videoSettings({model:id});
|
|
27
|
+
const axes=image?[['aspectRatio',entry.aspectRatios],['resolution',entry.resolutions],['quality',entry.qualities]]:[['duration',entry.durations],['resolution',entry.resolutions],['aspectRatio',entry.aspectRatios]];
|
|
28
|
+
const settings=axes.filter(([,values])=>values.length).map(([key,values])=>({key,label:SETTING_LABELS[key],values:[...values],default:defaults[key]===DEFAULT_SETTING?values[0]:defaults[key]}));
|
|
29
|
+
if(!image&&entry.nativeAudio)settings.push({key:'generateAudio',label:SETTING_LABELS.generateAudio,values:[true,false],default:true});
|
|
30
|
+
return image?{settings,references:entry.references}:{settings,firstFrame:entry.firstFrame,lastFrame:entry.lastFrame,needsFirstFrame:entry.requiresFirstFrame,sound:entry.nativeAudio};
|
|
31
|
+
}
|
|
32
|
+
const voiceList=voices=>Array.isArray(voices)?voices.filter(item=>typeof item?.id==='string'&&/^[a-z][a-z0-9_-]{0,63}$/.test(item.id)&&typeof item.label==='string').map(item=>({id:item.id,label:item.label.slice(0,120)})):[];
|
|
33
|
+
const credits=value=>Number.isSafeInteger(value)&&value>=0?value:null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The list a screen receives, from the rows the host offers a Step for this operation (/options, after the Step's own
|
|
37
|
+
* filtering): `{operation, default, models:[{id, label, credits, available, settings, ...}]}`. `default` is the model a
|
|
38
|
+
* run uses when the Block names none. Image and video models carry their settings and what they take (reference photos,
|
|
39
|
+
* frames, sound); speech carries its voices.
|
|
40
|
+
*/
|
|
41
|
+
export function modelList(operation,rows=[]){
|
|
42
|
+
const image=operation==='image.generate',video=operation==='video.generate',models=[];
|
|
43
|
+
for(const row of Array.isArray(rows)?rows:[]){
|
|
44
|
+
const id=row?.selection?.model;if(typeof id!=='string'||!id||models.some(model=>model.id===id))continue;
|
|
45
|
+
// Image and video come from StillMade's own catalog; a direct provider route (a fixed-size OpenAI picture) is not one.
|
|
46
|
+
const catalog=image||video?row.selection.provider==='stillmade'&&catalogEntry(image?'image':'video',id):null;if((image||video)&&!catalog)continue;
|
|
47
|
+
const fromCredits=credits(row.fromCredits),voices=operation==='audio.speech'?voiceList(row.voices):[];
|
|
48
|
+
models.push({id,label:typeof row.label==='string'&&row.label?row.label.slice(0,120):id,credits:credits(row.creditsPerCall),...(fromCredits!==null?{fromCredits}:{}),available:row.available!==false,...(row.available===false&&typeof row.reason==='string'?{reason:row.reason.slice(0,240)}:{}),...(catalog||{settings:[]}),...(voices.length?{voices}:{})});
|
|
49
|
+
}
|
|
50
|
+
return {operation,default:models.find(model=>model.available)?.id??null,models};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The same list from the SDK's own catalog, with no prices: the SDK preview and tests, where StillMade is not reached. */
|
|
54
|
+
export function catalogModelList(operation){
|
|
55
|
+
const image=operation==='image.generate',video=operation==='video.generate';
|
|
56
|
+
if(!image&&!video)return {operation,default:null,models:[]};
|
|
57
|
+
const models=image?IMAGE_GENERATION_MODELS:VIDEO_GENERATION_MODELS,first=image?DEFAULT_IMAGE_MODEL:DEFAULT_VIDEO_MODEL;
|
|
58
|
+
const ids=[first,...Object.keys(models).filter(id=>id!==first)];
|
|
59
|
+
return modelList(operation,ids.map(id=>({selection:{provider:'stillmade',model:id},label:models[id].label})));
|
|
60
|
+
}
|