@dfosco/storyboard 0.5.0-beta.32 → 0.5.0-beta.34
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/package.json
CHANGED
|
@@ -167,12 +167,25 @@ export class HotPool {
|
|
|
167
167
|
this.#log('■ STOPPED — all sessions killed')
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Non-mutating probe: would the next acquire() return a session, and would it
|
|
172
|
+
* be webgl-ready? Used by callers that want to render in webgl-ready mode
|
|
173
|
+
* without claiming a slot (the actual claim happens later when the WS
|
|
174
|
+
* connects). Returns { ready, webglReady }.
|
|
175
|
+
*/
|
|
176
|
+
peek() {
|
|
177
|
+
if (!this.#enabled || this.#queue.length === 0) return { ready: false, webglReady: false }
|
|
178
|
+
const idx = this.#queue.findIndex(s => s.state === 'ready')
|
|
179
|
+
if (idx === -1) return { ready: false, webglReady: false }
|
|
180
|
+
const webglReady = this.#webglReadySlots > 0 && idx < this.#webglReadySlots
|
|
181
|
+
return { ready: true, webglReady }
|
|
182
|
+
}
|
|
183
|
+
|
|
170
184
|
acquire() {
|
|
171
185
|
if (!this.#enabled || this.#queue.length === 0) {
|
|
172
186
|
this.#log(`→ ACQUIRE — pool ${!this.#enabled ? 'disabled' : 'empty'}, returning null`)
|
|
173
187
|
return null
|
|
174
188
|
}
|
|
175
|
-
|
|
176
189
|
const idx = this.#queue.findIndex(s => s.state === 'ready')
|
|
177
190
|
if (idx === -1) {
|
|
178
191
|
this.#log(`→ ACQUIRE — ${this.#queue.length} in queue but none ready, returning null`)
|
|
@@ -731,6 +744,13 @@ export class HotPoolManager {
|
|
|
731
744
|
return pool.acquire()
|
|
732
745
|
}
|
|
733
746
|
|
|
747
|
+
/** Non-mutating probe: { ready, webglReady } for the specified pool. */
|
|
748
|
+
peek(poolId) {
|
|
749
|
+
const pool = this.#pools.get(poolId)
|
|
750
|
+
if (!pool) return { ready: false, webglReady: false }
|
|
751
|
+
return pool.peek()
|
|
752
|
+
}
|
|
753
|
+
|
|
734
754
|
/** Consume a session (transfer ownership out of pool permanently). */
|
|
735
755
|
consume(poolId, sessionId) {
|
|
736
756
|
this.#pools.get(poolId)?.consume(sessionId)
|
|
@@ -143,6 +143,7 @@ function findCanvasFiles(root) {
|
|
|
143
143
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
144
144
|
for (const entry of entries) {
|
|
145
145
|
if (ignore.has(entry.name)) continue
|
|
146
|
+
if (entry.name.startsWith('~')) continue
|
|
146
147
|
const fullPath = path.join(dir, entry.name)
|
|
147
148
|
const relPath = rel ? `${rel}/${entry.name}` : entry.name
|
|
148
149
|
if (entry.isDirectory()) {
|
|
@@ -775,18 +776,19 @@ export function createCanvasHandler(ctx) {
|
|
|
775
776
|
}
|
|
776
777
|
|
|
777
778
|
/**
|
|
778
|
-
*
|
|
779
|
+
* Non-mutating probe of a hot pool — returns webgl-readiness without
|
|
780
|
+
* claiming a slot. Use this when the canvas API just needs to tell the
|
|
781
|
+
* client whether to render in webgl-ready mode; the actual session claim
|
|
782
|
+
* happens later in terminal-server when the WebSocket connects.
|
|
779
783
|
* @param {Object|null} hotPool — HotPoolManager instance
|
|
780
|
-
* @param {string} poolId — pool to
|
|
784
|
+
* @param {string} poolId — pool to peek at
|
|
781
785
|
* @param {string} [mode] — 'auto' (default), 'hot', or 'cold'
|
|
782
|
-
* @returns {
|
|
786
|
+
* @returns {{ ready: boolean, webglReady: boolean }}
|
|
783
787
|
*/
|
|
784
|
-
function
|
|
785
|
-
if (!hotPool || mode === 'cold') return
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (!hotPool.has(poolId)) return null
|
|
789
|
-
return hotPool.acquire(poolId) || null
|
|
788
|
+
function peekPool(hotPool, poolId, mode) {
|
|
789
|
+
if (!hotPool || mode === 'cold') return { ready: false, webglReady: false }
|
|
790
|
+
if (!hotPool.has(poolId)) return { ready: false, webglReady: false }
|
|
791
|
+
return hotPool.peek(poolId)
|
|
790
792
|
}
|
|
791
793
|
|
|
792
794
|
/**
|
|
@@ -1117,12 +1119,17 @@ export function createCanvasHandler(ctx) {
|
|
|
1117
1119
|
|
|
1118
1120
|
await prepareTerminalWidget({ type, props, widgetId, canvasName: name, req })
|
|
1119
1121
|
|
|
1120
|
-
// Hot pool
|
|
1121
|
-
|
|
1122
|
+
// Hot pool readiness probe for terminal/agent widgets — non-mutating.
|
|
1123
|
+
// The actual session claim happens later in terminal-server when the
|
|
1124
|
+
// WS connects. Probing here only tells the client whether to render
|
|
1125
|
+
// in webgl-ready mode immediately. (Previously this called
|
|
1126
|
+
// acquireFromPool, which leaked a #acquired slot per widget creation
|
|
1127
|
+
// because no consume/release ever fired against this acquisition.)
|
|
1128
|
+
let hotProbe = { ready: false, webglReady: false }
|
|
1122
1129
|
if ((type === 'terminal' || type === 'agent') && pool !== 'cold') {
|
|
1123
1130
|
const poolId = resolvePoolId(type, props)
|
|
1124
|
-
|
|
1125
|
-
if (!
|
|
1131
|
+
hotProbe = peekPool(hotPool, poolId, pool)
|
|
1132
|
+
if (!hotProbe.ready && pool === 'hot') {
|
|
1126
1133
|
sendJson(res, 409, { error: `No warm sessions available in pool "${poolId}"` })
|
|
1127
1134
|
return
|
|
1128
1135
|
}
|
|
@@ -1137,7 +1144,7 @@ export function createCanvasHandler(ctx) {
|
|
|
1137
1144
|
})
|
|
1138
1145
|
|
|
1139
1146
|
const response = { success: true, widget }
|
|
1140
|
-
if (
|
|
1147
|
+
if (hotProbe.ready) response.hotSession = { id: null, tmuxName: null, webglReady: hotProbe.webglReady }
|
|
1141
1148
|
sendJson(res, 201, response)
|
|
1142
1149
|
pushCanvasUpdate(name, filePath, __viteWs)
|
|
1143
1150
|
} catch (err) {
|
|
@@ -1869,11 +1876,11 @@ export function createCanvasHandler(ctx) {
|
|
|
1869
1876
|
const widgetId = generateWidgetId(type)
|
|
1870
1877
|
await prepareTerminalWidget({ type, props, widgetId, canvasName: name, req })
|
|
1871
1878
|
|
|
1872
|
-
let
|
|
1879
|
+
let hotProbe = { ready: false, webglReady: false }
|
|
1873
1880
|
if ((type === 'terminal' || type === 'agent') && pool !== 'cold') {
|
|
1874
1881
|
const poolId = resolvePoolId(type, props)
|
|
1875
|
-
|
|
1876
|
-
if (!
|
|
1882
|
+
hotProbe = peekPool(hotPool, poolId, pool)
|
|
1883
|
+
if (!hotProbe.ready && pool === 'hot') throw new Error(`No warm sessions available in pool "${poolId}"`)
|
|
1877
1884
|
}
|
|
1878
1885
|
|
|
1879
1886
|
const widget = stampBounds({ id: widgetId, type, position, props })
|
|
@@ -1886,7 +1893,7 @@ export function createCanvasHandler(ctx) {
|
|
|
1886
1893
|
if (ref) refs[ref] = widgetId
|
|
1887
1894
|
|
|
1888
1895
|
const result = { index: i, op: 'create-widget', ref: ref || undefined, widgetId, widget }
|
|
1889
|
-
if (
|
|
1896
|
+
if (hotProbe.ready) result.hotSession = { id: null, tmuxName: null, webglReady: hotProbe.webglReady }
|
|
1890
1897
|
results.push(result)
|
|
1891
1898
|
break
|
|
1892
1899
|
}
|
|
@@ -2567,10 +2567,25 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
2567
2567
|
const y = widget.position?.y ?? 0
|
|
2568
2568
|
const w = widget.props?.width ?? fallback.width
|
|
2569
2569
|
const h = widget.props?.height ?? fallback.height
|
|
2570
|
-
|
|
2570
|
+
|
|
2571
|
+
// Zoom in (to at least 100%) so the agent is comfortably visible.
|
|
2572
|
+
const { ZOOM_MIN, ZOOM_MAX } = zoomLimits()
|
|
2573
|
+
const targetZoom = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.max(zoomRef.current || 100, 100)))
|
|
2574
|
+
const newScale = targetZoom / 100
|
|
2575
|
+
if (targetZoom !== zoomRef.current) {
|
|
2576
|
+
zoomRef.current = targetZoom
|
|
2577
|
+
const zoomEl = zoomElRef.current
|
|
2578
|
+
if (zoomEl) {
|
|
2579
|
+
zoomEl.style.transform = `scale(${newScale})`
|
|
2580
|
+
zoomEl.style.width = `${Math.max(10000, 100 / newScale)}vw`
|
|
2581
|
+
zoomEl.style.height = `${Math.max(10000, 100 / newScale)}vh`
|
|
2582
|
+
}
|
|
2583
|
+
setZoom(targetZoom)
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2571
2586
|
el.scrollTo({
|
|
2572
|
-
left: (x + w / 2) *
|
|
2573
|
-
top: (y + h / 2) *
|
|
2587
|
+
left: (x + w / 2) * newScale - el.clientWidth / 2,
|
|
2588
|
+
top: (y + h / 2) * newScale - el.clientHeight / 2,
|
|
2574
2589
|
behavior: 'smooth',
|
|
2575
2590
|
})
|
|
2576
2591
|
}
|
|
@@ -52,9 +52,9 @@ function parseDataFile(filePath) {
|
|
|
52
52
|
// Handle .canvas.jsonl files
|
|
53
53
|
const canvasJsonlMatch = base.match(/^(.+)\.canvas\.jsonl$/)
|
|
54
54
|
if (canvasJsonlMatch) {
|
|
55
|
-
if (canvasJsonlMatch[1].startsWith('_')) return null
|
|
55
|
+
if (canvasJsonlMatch[1].startsWith('_') || canvasJsonlMatch[1].startsWith('~')) return null
|
|
56
56
|
const normalized = filePath.replace(/\\/g, '/')
|
|
57
|
-
if (normalized.split('/').some(seg => seg.startsWith('_'))) return null
|
|
57
|
+
if (normalized.split('/').some(seg => seg.startsWith('_') || seg.startsWith('~'))) return null
|
|
58
58
|
|
|
59
59
|
const baseName = canvasJsonlMatch[1]
|
|
60
60
|
let name = baseName
|