thinkpool-pair 0.7.228 → 0.7.230
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/bridge.mjs +37 -24
- package/design-edit.mjs +73 -16
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +1 -1
package/bridge.mjs
CHANGED
|
@@ -66,7 +66,7 @@ import { writeLaneArtifact, digestSlice, appendDigest, resumeLane } from './flow
|
|
|
66
66
|
import { createFlowWorktree, worktreeSpec } from './flow-worktree.mjs'
|
|
67
67
|
import { startPreview, stopAllPreviews, previews } from './flow-preview.mjs'
|
|
68
68
|
import { ViewportManager, createViewportTools, sharedViewportBrowser } from './viewport.mjs'
|
|
69
|
-
import { designPrompt, materializeDesignAsset, resolveDesignSource, resolveManifestDesignSource, restoreDesignSources, syncRealtimeAuth,
|
|
69
|
+
import { deleteDesignAssetDraft, designPrompt, designTranscript, materializeDesignAsset, resolveDesignSource, resolveManifestDesignSource, restoreDesignSources, syncRealtimeAuth, validateDesignBatchRequest } from './design-edit.mjs'
|
|
70
70
|
// FL-M9 — per-lane preview servers leak (one per done lane, never stopped until shutdown).
|
|
71
71
|
// Lane previews are keyed `lane:<flowId>:<laneId>`; stop a whole flow's set when it assembles
|
|
72
72
|
// (the assembled preview supersedes them) or when a lane is reverted.
|
|
@@ -1283,12 +1283,14 @@ function pumpDesign(term) {
|
|
|
1283
1283
|
const active = { ...next, resultOk: false, proof: null, timer: null }
|
|
1284
1284
|
designActive.set(term, active)
|
|
1285
1285
|
designStatus({ previewId: next.record.previewId, requestId: next.request.cid, state: 'applying' })
|
|
1286
|
-
//
|
|
1287
|
-
const
|
|
1288
|
-
:
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1286
|
+
// Complete room-visible instructions, without host paths or locator packets.
|
|
1287
|
+
const visible = {
|
|
1288
|
+
kind: 'you',
|
|
1289
|
+
text: designTranscript(next.request, { restore: !!next.restoreRecord }),
|
|
1290
|
+
cid: next.request.cid,
|
|
1291
|
+
by: next.by,
|
|
1292
|
+
design: true,
|
|
1293
|
+
}
|
|
1292
1294
|
stampEvent(visible); pushLog(lane, visible); bcast('code-event', { term, evt: visible })
|
|
1293
1295
|
try { lane.session.sendTurn(designPrompt({ record: next.record, request: next.request, by: next.by, restore: !!next.restoreRecord, priorRecord: next.restoreRecord })) }
|
|
1294
1296
|
catch { finishDesign(term, 'failed', { message: 'The producing lane could not start the edit.' }) }
|
|
@@ -3991,39 +3993,49 @@ designChannel
|
|
|
3991
3993
|
designChannel.send({ type: 'broadcast', event: 'design-capability-res', payload: { previewId: String(payload?.previewId || ''), revision: String(payload?.revision || ''), ok } })
|
|
3992
3994
|
})
|
|
3993
3995
|
.on('broadcast', { event: 'design-apply' }, async ({ payload }) => {
|
|
3994
|
-
const verdict =
|
|
3996
|
+
const verdict = validateDesignBatchRequest(payload, designArtifacts, room)
|
|
3995
3997
|
const previewId = String(payload?.previewId || '')
|
|
3996
3998
|
const requestId = String(payload?.cid || '').slice(0, 80)
|
|
3997
3999
|
if (!verdict.ok) {
|
|
3998
|
-
|
|
4000
|
+
const prefix = Number.isSafeInteger(verdict.editIndex) ? `Edit ${verdict.editIndex + 1}: ` : ''
|
|
4001
|
+
designStatus({ previewId, requestId, state: verdict.error === 'stale-preview' ? 'stale' : 'failed', message: `${prefix}${verdict.error}` })
|
|
3999
4002
|
return
|
|
4000
4003
|
}
|
|
4001
4004
|
if (!requestId || seenDesignRequests.has(requestId)) return
|
|
4002
4005
|
seenDesignRequests.add(requestId)
|
|
4003
4006
|
if (seenDesignRequests.size > 500) seenDesignRequests.delete(seenDesignRequests.values().next().value)
|
|
4004
|
-
|
|
4007
|
+
const term = verdict.record.term
|
|
4008
|
+
const queue = designQueues.get(term) || []
|
|
4009
|
+
designQueues.set(term, queue)
|
|
4010
|
+
if (queue.length >= 8) {
|
|
4011
|
+
designStatus({ previewId, requestId, state: 'failed', message: 'Too many Design batches are queued for this lane.' })
|
|
4012
|
+
return
|
|
4013
|
+
}
|
|
4014
|
+
const edits = verdict.request.mode === 'batch' ? verdict.request.edits : [verdict.request]
|
|
4015
|
+
const materializedDrafts = []
|
|
4016
|
+
for (let index = 0; index < edits.length; index++) {
|
|
4017
|
+
const edit = edits[index]
|
|
4018
|
+
if (edit.mode !== 'image') continue
|
|
4005
4019
|
const localPath = await materializeDesignAsset({
|
|
4006
4020
|
webBase: WEB_BASE,
|
|
4007
4021
|
token: codeAuthToken,
|
|
4008
4022
|
room,
|
|
4009
|
-
asset:
|
|
4010
|
-
requestId
|
|
4023
|
+
asset: edit.asset,
|
|
4024
|
+
requestId: `${requestId}-${index + 1}`,
|
|
4025
|
+
cleanup: false,
|
|
4011
4026
|
})
|
|
4012
4027
|
if (!localPath) {
|
|
4013
|
-
designStatus({ previewId, requestId, state: 'failed', message:
|
|
4028
|
+
designStatus({ previewId, requestId, state: 'failed', message: `Edit ${index + 1}: The private image draft could not be opened.` })
|
|
4014
4029
|
return
|
|
4015
4030
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
const term = verdict.record.term
|
|
4019
|
-
const queue = designQueues.get(term) || []
|
|
4020
|
-
designQueues.set(term, queue)
|
|
4021
|
-
if (queue.length >= 8) {
|
|
4022
|
-
designStatus({ previewId, requestId, state: 'failed', message: 'Too many Design edits are queued for this lane.' })
|
|
4023
|
-
return
|
|
4031
|
+
edit.asset.localPath = localPath
|
|
4032
|
+
materializedDrafts.push(edit.asset.path)
|
|
4024
4033
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4034
|
+
await Promise.all(materializedDrafts.map((assetPath) => deleteDesignAssetDraft({ webBase: WEB_BASE, token: codeAuthToken, room, path: assetPath })))
|
|
4035
|
+
const by = String(payload?.by || 'A room member').replace(/\0/g, '').trim().slice(0, 120) || 'A room member'
|
|
4036
|
+
queue.push({ ...verdict, by })
|
|
4037
|
+
const batchCount = verdict.request.mode === 'batch' ? verdict.request.edits.length : 1
|
|
4038
|
+
designStatus({ previewId, requestId, state: 'queued', message: `${batchCount} edit${batchCount === 1 ? '' : 's'} pooled to the producing lane.` })
|
|
4027
4039
|
pumpDesign(term)
|
|
4028
4040
|
})
|
|
4029
4041
|
.on('broadcast', { event: 'design-restore' }, ({ payload }) => {
|
|
@@ -4044,7 +4056,8 @@ designChannel
|
|
|
4044
4056
|
designStatus({ previewId, requestId, state: 'failed', message: 'Too many Design edits are queued for this lane.' })
|
|
4045
4057
|
return
|
|
4046
4058
|
}
|
|
4047
|
-
|
|
4059
|
+
const by = String(payload?.by || 'A room member').replace(/\0/g, '').trim().slice(0, 120) || 'A room member'
|
|
4060
|
+
queue.push({ record, restoreRecord: restore.priorRecord, request: { previewId, revision: record.revision, mode: 'restore', intent: 'restore previous verified revision', target: restore.request.target || { selector: 'html', tag: 'artifact', name: `${restore.request.edits?.length || 1} pooled edits` }, cid: requestId }, by })
|
|
4048
4061
|
designStatus({ previewId, requestId, state: 'queued', message: 'Restore queued in the producing lane.' })
|
|
4049
4062
|
pumpDesign(record.term)
|
|
4050
4063
|
})
|
package/design-edit.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
const MAX_SOURCE_BYTES = 2 * 1024 * 1024
|
|
7
7
|
const MAX_INTENT = 2_000
|
|
8
8
|
const MAX_TEXT = 4_000
|
|
9
|
+
const MAX_BATCH_EDITS = 8
|
|
9
10
|
|
|
10
11
|
const clean = (value, max) => String(value ?? '').replace(/\0/g, '').trim().slice(0, max)
|
|
11
12
|
|
|
@@ -160,7 +161,38 @@ export function validateDesignRequest(payload, registry, room = '') {
|
|
|
160
161
|
return { ok: true, record, request: { previewId, revision: record.revision, mode, intent, target, movement, asset, cid: clean(payload.cid, 80) } }
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
export
|
|
164
|
+
export function validateDesignBatchRequest(payload, registry, room = '') {
|
|
165
|
+
if (!payload || typeof payload !== 'object' || !Array.isArray(payload.edits)) return validateDesignRequest(payload, registry, room)
|
|
166
|
+
if (payload.edits.length < 1 || payload.edits.length > MAX_BATCH_EDITS) return { ok: false, error: 'invalid-batch' }
|
|
167
|
+
const previewId = clean(payload.previewId, 64)
|
|
168
|
+
const revision = clean(payload.revision, 64)
|
|
169
|
+
const cid = clean(payload.cid, 80)
|
|
170
|
+
const edits = []
|
|
171
|
+
const targets = new Set()
|
|
172
|
+
let record = null
|
|
173
|
+
for (let index = 0; index < payload.edits.length; index++) {
|
|
174
|
+
const verdict = validateDesignRequest({ ...payload.edits[index], previewId, revision, cid }, registry, room)
|
|
175
|
+
if (!verdict.ok) return { ...verdict, editIndex: index }
|
|
176
|
+
const targetKey = verdict.request.target.nodeKey || verdict.request.target.selector || verdict.request.target.xpath
|
|
177
|
+
if (targets.has(targetKey)) return { ok: false, error: 'duplicate-target', editIndex: index }
|
|
178
|
+
targets.add(targetKey)
|
|
179
|
+
record = verdict.record
|
|
180
|
+
edits.push(verdict.request)
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, record, request: { previewId, revision: record.revision, mode: 'batch', edits, cid } }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function deleteDesignAssetDraft({ webBase, token, room, path: assetPath, fetcher = fetch }) {
|
|
186
|
+
if (!webBase || !token || !room || !assetPath) return false
|
|
187
|
+
const response = await fetcher(`${String(webBase).replace(/\/$/, '')}/api/code-design-draft`, {
|
|
188
|
+
method: 'DELETE',
|
|
189
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
190
|
+
body: JSON.stringify({ code: room, path: assetPath }),
|
|
191
|
+
}).catch(() => null)
|
|
192
|
+
return !!response?.ok
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function materializeDesignAsset({ webBase, token, room, asset, requestId, cleanup = true, fetcher = fetch }) {
|
|
164
196
|
if (!webBase || !token || !room || !asset?.path || !requestId) return null
|
|
165
197
|
const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }
|
|
166
198
|
const signed = await fetcher(`${String(webBase).replace(/\/$/, '')}/api/code-attachment-url`, {
|
|
@@ -180,24 +212,49 @@ export async function materializeDesignAsset({ webBase, token, room, asset, requ
|
|
|
180
212
|
const file = path.join(dir, `${clean(requestId, 80)}${ext}`)
|
|
181
213
|
fs.mkdirSync(dir, { recursive: true })
|
|
182
214
|
fs.writeFileSync(file, data)
|
|
183
|
-
await
|
|
184
|
-
method: 'DELETE',
|
|
185
|
-
headers,
|
|
186
|
-
body: JSON.stringify({ code: room, path: asset.path }),
|
|
187
|
-
}).catch(() => {})
|
|
215
|
+
if (cleanup) await deleteDesignAssetDraft({ webBase, token, room, path: asset.path, fetcher })
|
|
188
216
|
return file
|
|
189
217
|
}
|
|
190
218
|
|
|
219
|
+
function designAction(request) {
|
|
220
|
+
if (request.mode === 'text') return `Replace only this element's visible text with:\n${request.intent}`
|
|
221
|
+
if (request.mode === 'move') return `Move only this selected element within its current direct parent, from sibling position ${request.movement.fromIndex + 1} to ${request.movement.toIndex + 1}. Preserve the parent, responsive behavior, semantics, and every other sibling.`
|
|
222
|
+
if (request.mode === 'image') return `Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${request.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`
|
|
223
|
+
return `Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${request.intent}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const designModeLabel = (mode) => ({ text: 'Text', prompt: 'Prompt', move: 'Move', image: 'Image' })[mode] || 'Edit'
|
|
227
|
+
|
|
228
|
+
function visibleDesignAction(request) {
|
|
229
|
+
if (request.mode === 'text') return `Replacement text:\n${clean(request.intent, MAX_TEXT)}`
|
|
230
|
+
if (request.mode === 'prompt') return `Instruction:\n${clean(request.intent, MAX_INTENT)}`
|
|
231
|
+
if (request.mode === 'move') return `Move from position ${request.movement.fromIndex + 1} to position ${request.movement.toIndex + 1}.`
|
|
232
|
+
if (request.mode === 'image') return `Replacement image: ${clean(request.asset?.name || 'uploaded image', 240)}`
|
|
233
|
+
return ''
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Complete, room-safe transcript text for the visible Design dispatch. Keep the
|
|
237
|
+
// user instructions and order; omit host-only paths and locator packets.
|
|
238
|
+
export function designTranscript(request, { restore = false } = {}) {
|
|
239
|
+
if (restore) return 'Thinkpool Design · restore the previous verified revision'
|
|
240
|
+
const edits = request?.mode === 'batch' ? request.edits : [request]
|
|
241
|
+
const valid = Array.isArray(edits) ? edits.filter(Boolean).slice(0, MAX_BATCH_EDITS) : []
|
|
242
|
+
const heading = request?.mode === 'batch'
|
|
243
|
+
? `Thinkpool Design · ${valid.length} pooled edit${valid.length === 1 ? '' : 's'}`
|
|
244
|
+
: 'Thinkpool Design · 1 edit'
|
|
245
|
+
const details = valid.map((edit, index) => {
|
|
246
|
+
const target = clean(edit.target?.name || edit.target?.tag || 'element', 240)
|
|
247
|
+
return `${index + 1}. ${designModeLabel(edit.mode)} · ${target}\n${visibleDesignAction(edit)}`
|
|
248
|
+
})
|
|
249
|
+
return [heading, ...details].join('\n\n')
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
export function designPrompt({ record, request, by, restore = false, priorRecord = null }) {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
253
|
+
const batch = request.mode === 'batch' ? request.edits : null
|
|
254
|
+
const work = restore
|
|
194
255
|
? `Restore only the immediately previous Design Mode change by comparing the current artifact with this bridge-created verified backup: ${priorRecord?.backupPath || '(backup unavailable)'}. Do not overwrite unrelated changes.`
|
|
195
|
-
:
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
: request.mode === 'image'
|
|
200
|
-
? `Replace only this selected image's content with the uploaded image at this bridge-local temporary path:\n${request.asset.localPath}\nCopy it into an appropriate durable project asset location. Preserve the existing image frame, dimensions, border radius, object-fit behavior, responsive rules, and surrounding layout.`
|
|
201
|
-
: `Apply this visual change to only the selected element unless the instruction explicitly requires its immediate context:\n${request.intent}`
|
|
202
|
-
return `A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${record.sourcePath}\nWorkspace: ${record.workspaceRoot}\nExpected source revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${restore ? 'restore the immediately previous verified Design Mode revision' : request.mode}\n\nSelected element:\n${target}\n\n${action}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches the selected element before editing.\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${request.cid} and TP_DESIGN_PARENT_REVISION=${record.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If the target is stale or ambiguous, stop without editing and explain that the person must reselect it.`
|
|
256
|
+
: batch
|
|
257
|
+
? batch.map((edit, index) => `Edit ${index + 1} of ${batch.length} · ${edit.mode}\nSelected element:\n${JSON.stringify(edit.target, null, 2)}\n\n${designAction(edit)}`).join('\n\n---\n\n')
|
|
258
|
+
: `Selected element:\n${JSON.stringify(request.target, null, 2)}\n\n${designAction(request)}`
|
|
259
|
+
return `A room member used ThinkPool Design Mode on the artifact below. This is an authorized source edit in your current workspace.\n\nArtifact source: ${record.sourcePath}\nWorkspace: ${record.workspaceRoot}\nExpected source revision: ${record.revision}\nRequested by: ${clean(by || 'A room member', 120)}\nOperation: ${restore ? 'restore the immediately previous verified Design Mode revision' : batch ? `${batch.length} queued edits in one batch` : request.mode}\n\n${work}\n\nRules:\n- Re-read the exact artifact source and confirm its revision/content still matches every selected element before editing.\n${batch ? '- Apply the queued edits in order in one source pass, then render once after the full batch.' : '- Apply only the requested change, then render the result once.'}\n- Edit the canonical source above; do not search for or edit a different plausible file.\n- Preserve the represented product page faithfully: keep its real copy, structure, fonts, assets, spacing, and responsive behavior except for the selected edits. Never replace it with generic mockup content.\n- Preserve unrelated content and styling.\n- Use the existing mockup render workflow to produce both desktop and mobile captures after the edit. Run it with TP_DESIGN_REQUEST_ID=${request.cid} and TP_DESIGN_PARENT_REVISION=${record.revision} in the command environment so the proof correlates to this request.\n- Do not claim this is live until both captures succeed and the room receives the manifest.\n- If any target is stale or ambiguous, stop without editing and explain that the person must reselect it.`
|
|
203
260
|
}
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the first human turn in each fresh native thread. Keep the runtime-specific
|
|
4
4
|
// orchestration rules in their own drivers — these are the cross-runtime truths
|
|
5
5
|
// about where the people are and how finished work must reach them.
|
|
6
|
-
export const THINKPOOL_DESIGN_DELIVERY_RULE = 'THINKPOOL DESIGN (mandatory for HTML): save the editable HTML source in the workspace, then use the $TP_MOCKUP_OUTBOX render helper so the room receives a Thinkpool Design card with both desktop (1440×900) and mobile (390×844) previews. Never paste raw HTML into chat, send an .html file as the room deliverable, or substitute a bare URL or single screenshot for this card. A shareable browser URL may accompany the card, but never replaces it.'
|
|
6
|
+
export const THINKPOOL_DESIGN_DELIVERY_RULE = 'THINKPOOL DESIGN (mandatory for HTML): save the editable HTML source in the workspace, then use the $TP_MOCKUP_OUTBOX render helper so the room receives a Thinkpool Design card with both desktop (1440×900) and mobile (390×844) previews. When the requested Design surface is an existing product page or route, first build and capture the actual route at both viewports, then read its current source, styles, copy, fonts, and assets. Derive the editable Design artifact from that evidence and compare both artifact captures against the real route before delivery. Preserve the real page faithfully except for explicitly proposed edits—never hand-recreate it from memory, simplify it, replace it with generic mockup content, or label an approximation as the product. If a faithful editable artifact cannot be produced, surface the actual route captures and say that Design editing is unavailable for that surface. Never paste raw HTML into chat, send an .html file as the room deliverable, or substitute a bare URL or single screenshot for this card. A shareable browser URL may accompany the card, but never replaces it.'
|
|
7
7
|
|
|
8
8
|
export const THINKPOOL_REMOTE_DELIVERY_RULES = Object.freeze([
|
|
9
9
|
'REMOTE USER (authoritative default for ThinkPool Code rooms): unless a person explicitly says they are at the host machine, assume everyone driving this room is remote — possibly on a phone — with no terminal and no access to the host filesystem. Never ask them to run a local command, open a local file, or "go check" something on the machine. Anything host-side, you run yourself and show the result in the room. Only suggest actions they can actually do from the room UI or a browser.',
|