oficina-opencode 3.1.0 → 3.1.1
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/README.md +1 -1
- package/commands/oficina.md +7 -6
- package/dashboard/.dom-check.html +1257 -0
- package/dashboard/README-DASHBOARD.md +64 -0
- package/dashboard/assets/factory.css +1543 -0
- package/dashboard/assets/tokens.css +208 -0
- package/dashboard/build-kanban-data.mjs +279 -0
- package/dashboard/dashboard.css +753 -0
- package/dashboard/dashboard.html +121 -0
- package/dashboard/dashboard.js +610 -0
- package/dashboard/data/snapshot.json +883 -0
- package/dashboard/debate.html +134 -0
- package/dashboard/factory/build-snapshot.mjs +89 -0
- package/dashboard/factory/factory-app.js +1185 -0
- package/dashboard/factory/factory-core.js +541 -0
- package/dashboard/factory/snapshot.js +850 -0
- package/dashboard/factory.html +350 -0
- package/dashboard/kanban-data.json +671 -0
- package/dashboard/kanban-source.js +643 -0
- package/dashboard/kanban.check.mjs +454 -0
- package/dashboard/kanban.html +1257 -0
- package/dashboard/leadership.html +1008 -0
- package/dashboard/lib/debate-app.js +1149 -0
- package/dashboard/lib/debate-core.js +698 -0
- package/dashboard/lib/debate-data.js +163 -0
- package/dashboard/lib/debate-demo.js +110 -0
- package/dashboard/lib/debate.css +1155 -0
- package/dashboard/lib/leadership-model.js +946 -0
- package/dashboard/probe.mjs +35 -0
- package/dashboard/scripts/serve.mjs +122 -0
- package/dashboard/scripts/snapshot-data.mjs +93 -0
- package/dashboard/scripts/snapshot.mjs +28 -0
- package/dashboard/server.mjs +242 -0
- package/dashboard/shot.mjs +54 -0
- package/dashboard/shots/desktop-gates.png +0 -0
- package/dashboard/shots/desktop-light.png +0 -0
- package/dashboard/shots/desktop-real.png +0 -0
- package/dashboard/shots/mobile.png +0 -0
- package/dashboard/test/factory-core.test.mjs +244 -0
- package/dashboard/tests/debate-core.test.mjs +336 -0
- package/dashboard/tests/leadership-model.test.mjs +404 -0
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/dashboard.d.ts +33 -0
- package/dist/core/dashboard.d.ts.map +1 -0
- package/dist/core/dashboard.js +115 -0
- package/dist/core/dashboard.js.map +1 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +11 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/environment.d.ts +28 -0
- package/dist/core/environment.d.ts.map +1 -0
- package/dist/core/environment.js +80 -0
- package/dist/core/environment.js.map +1 -0
- package/dist/core/systemPrompt.d.ts.map +1 -1
- package/dist/core/systemPrompt.js +3 -0
- package/dist/core/systemPrompt.js.map +1 -1
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +26 -22
- package/dist/tools/index.js.bak +972 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/v2.d.ts +5 -0
- package/dist/v2.d.ts.map +1 -1
- package/dist/v2.js +57 -5
- package/dist/v2.js.bak +517 -0
- package/dist/v2.js.map +1 -1
- package/docs/QUE-ES.md +1 -1
- package/package.json +4 -1
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
dashboard/factory.html — app layer (classic script, no build step)
|
|
3
|
+
Depends on: globalThis.FactoryCore (dashboard/factory/factory-core.js)
|
|
4
|
+
Data: live -> /api/snapshot (dashboard/serve.mjs)
|
|
5
|
+
static-> ../.oficina/*.json (any static server at repo root)
|
|
6
|
+
offline-> window.__FACTORY_SNAPSHOT__ (dashboard/factory/snapshot.js)
|
|
7
|
+
The panel is strictly read-only: "registering" an agent is a local
|
|
8
|
+
projection so the animation can be watched without faking a disk write.
|
|
9
|
+
========================================================================== */
|
|
10
|
+
|
|
11
|
+
;(function factoryApp() {
|
|
12
|
+
"use strict"
|
|
13
|
+
|
|
14
|
+
var Core = globalThis.FactoryCore
|
|
15
|
+
var doc = document
|
|
16
|
+
var root = doc.documentElement
|
|
17
|
+
|
|
18
|
+
if (!Core) {
|
|
19
|
+
doc.body.insertAdjacentHTML(
|
|
20
|
+
"afterbegin",
|
|
21
|
+
'<p style="padding:2rem;font-family:monospace;color:#f88">' +
|
|
22
|
+
"factory-core.js no cargó: revisá la ruta del script.</p>",
|
|
23
|
+
)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------- helpers
|
|
28
|
+
var $ = function (sel, ctx) {
|
|
29
|
+
return (ctx || doc).querySelector(sel)
|
|
30
|
+
}
|
|
31
|
+
var $$ = function (sel, ctx) {
|
|
32
|
+
return Array.prototype.slice.call((ctx || doc).querySelectorAll(sel))
|
|
33
|
+
}
|
|
34
|
+
var sleep = function (ms) {
|
|
35
|
+
return new Promise(function (resolve) {
|
|
36
|
+
setTimeout(resolve, ms)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
var esc = function (value) {
|
|
40
|
+
return String(value == null ? "" : value)
|
|
41
|
+
.replace(/&/g, "&")
|
|
42
|
+
.replace(/</g, "<")
|
|
43
|
+
.replace(/>/g, ">")
|
|
44
|
+
.replace(/"/g, """)
|
|
45
|
+
.replace(/'/g, "'")
|
|
46
|
+
}
|
|
47
|
+
var reducedMotion = window.matchMedia(
|
|
48
|
+
"(prefers-reduced-motion: reduce)",
|
|
49
|
+
).matches
|
|
50
|
+
var clamp = function (n, min, max) {
|
|
51
|
+
return Math.max(min, Math.min(max, n))
|
|
52
|
+
}
|
|
53
|
+
var initials = function (name) {
|
|
54
|
+
var words = String(name || "?")
|
|
55
|
+
.replace(/[^A-Za-z0-9áéíóúñ ]/gi, " ")
|
|
56
|
+
.split(/\s+/)
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
if (words.length === 0) return "??"
|
|
59
|
+
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
|
60
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase()
|
|
61
|
+
}
|
|
62
|
+
var timeOf = function (iso) {
|
|
63
|
+
var date = iso ? new Date(iso) : new Date()
|
|
64
|
+
if (isNaN(date.getTime())) return "--:--:--"
|
|
65
|
+
return date.toLocaleTimeString("es-AR", { hour12: false })
|
|
66
|
+
}
|
|
67
|
+
var STORAGE_THEME = "oficina.factory.theme"
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------- elements
|
|
70
|
+
var els = {
|
|
71
|
+
sourceChip: $("#source-chip"),
|
|
72
|
+
sourceText: $("#source-text"),
|
|
73
|
+
pollChip: $("#poll-chip"),
|
|
74
|
+
clockChip: $("#clock-chip"),
|
|
75
|
+
countsChip: $("#counts-chip"),
|
|
76
|
+
btnFabricate: $("#btn-fabricate"),
|
|
77
|
+
btnPause: $("#btn-pause"),
|
|
78
|
+
btnResync: $("#btn-resync"),
|
|
79
|
+
btnTheme: $("#btn-theme"),
|
|
80
|
+
fabricateHint: $("#fabricate-hint"),
|
|
81
|
+
coverageValue: $("#coverage-value"),
|
|
82
|
+
coverageFill: $("#coverage-fill"),
|
|
83
|
+
coverageThreshold: $("#coverage-threshold"),
|
|
84
|
+
coverageNote: $("#coverage-note"),
|
|
85
|
+
gapList: $("#gap-list"),
|
|
86
|
+
gapCount: $("#gap-count"),
|
|
87
|
+
stage: $("#stage"),
|
|
88
|
+
sceneMoldLabel: $("#scene-mold-label"),
|
|
89
|
+
sceneAgent: $("#scene-agent"),
|
|
90
|
+
sceneOutLabel: $("#scene-out-label"),
|
|
91
|
+
steps: $$("#steps .step"),
|
|
92
|
+
termPath: $("#term-path"),
|
|
93
|
+
termPathText: $("#term-path-text"),
|
|
94
|
+
termTag: $("#term-tag"),
|
|
95
|
+
termBody: $("#term-body"),
|
|
96
|
+
rosterGrid: $("#roster-grid"),
|
|
97
|
+
rosterCount: $("#roster-count"),
|
|
98
|
+
matchBody: $("#match-body"),
|
|
99
|
+
matchCount: $("#match-count"),
|
|
100
|
+
toasts: $("#toasts"),
|
|
101
|
+
dialog: $("#agent-dialog"),
|
|
102
|
+
dialogAvatar: $("#dialog-avatar"),
|
|
103
|
+
dialogTitle: $("#dialog-title"),
|
|
104
|
+
dialogSub: $("#dialog-sub"),
|
|
105
|
+
dialogMeta: $("#dialog-meta"),
|
|
106
|
+
dialogBody: $("#dialog-body"),
|
|
107
|
+
dialogOrigin: $("#dialog-origin"),
|
|
108
|
+
btnCopy: $("#btn-copy"),
|
|
109
|
+
live: $("#live-region"),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------- state
|
|
113
|
+
var app = {
|
|
114
|
+
snapshot: null,
|
|
115
|
+
sourceKind: "none",
|
|
116
|
+
need: null,
|
|
117
|
+
focusId: null,
|
|
118
|
+
preview: null,
|
|
119
|
+
fabricated: null,
|
|
120
|
+
projection: null,
|
|
121
|
+
running: false,
|
|
122
|
+
runToken: 0,
|
|
123
|
+
paused: false,
|
|
124
|
+
tick: 0,
|
|
125
|
+
interval: 800,
|
|
126
|
+
unchanged: 0,
|
|
127
|
+
lastSignature: "",
|
|
128
|
+
startedAt: null,
|
|
129
|
+
confirmed: false,
|
|
130
|
+
fichaOrigin: "proyectada",
|
|
131
|
+
pendingAuto: false,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var STEPS = [
|
|
135
|
+
{ id: "detect", label: "Detectar hueco de cobertura", end: "GAP" },
|
|
136
|
+
{ id: "synthesize", label: "Sintetizar rol y capabilities", end: "ROL" },
|
|
137
|
+
{ id: "write", label: "Escribir la ficha .md", end: "FICHA" },
|
|
138
|
+
{ id: "register", label: "Registrar en el catálogo dinámico", end: "ROSTER" },
|
|
139
|
+
{ id: "match", label: "Match en la próxima card", end: "MATCH" },
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------- announce
|
|
143
|
+
function announce(message) {
|
|
144
|
+
if (!els.live) return
|
|
145
|
+
els.live.textContent = ""
|
|
146
|
+
// force SR re-announcement for identical strings
|
|
147
|
+
setTimeout(function () {
|
|
148
|
+
els.live.textContent = message
|
|
149
|
+
}, 30)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------- data
|
|
153
|
+
async function fetchJson(url) {
|
|
154
|
+
var response = await fetch(url, { cache: "no-store" })
|
|
155
|
+
if (!response.ok) throw new Error(url + " -> " + response.status)
|
|
156
|
+
return response.json()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function loadFromApi() {
|
|
160
|
+
var data = await fetchJson("/api/snapshot")
|
|
161
|
+
if (!data || !data.state || !data.roster) throw new Error("payload inválido")
|
|
162
|
+
return {
|
|
163
|
+
source: "live",
|
|
164
|
+
capturedAt: data.capturedAt,
|
|
165
|
+
state: data.state,
|
|
166
|
+
roster: data.roster,
|
|
167
|
+
catalogo: data.catalogo || [],
|
|
168
|
+
fichas: data.fichas || {},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadFromFiles() {
|
|
173
|
+
var pair = await Promise.all([
|
|
174
|
+
fetchJson("../.oficina/state.json"),
|
|
175
|
+
fetchJson("../.oficina/roster.json"),
|
|
176
|
+
])
|
|
177
|
+
return {
|
|
178
|
+
source: "static",
|
|
179
|
+
capturedAt: new Date().toISOString(),
|
|
180
|
+
state: pair[0],
|
|
181
|
+
roster: pair[1],
|
|
182
|
+
catalogo: [],
|
|
183
|
+
fichas: {},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function loadFromEmbedded() {
|
|
188
|
+
var snap = window.__FACTORY_SNAPSHOT__
|
|
189
|
+
if (!snap || !snap.state || !snap.roster) throw new Error("sin snapshot embebido")
|
|
190
|
+
return {
|
|
191
|
+
source: "snapshot",
|
|
192
|
+
capturedAt: snap.capturedAt,
|
|
193
|
+
state: snap.state,
|
|
194
|
+
roster: snap.roster,
|
|
195
|
+
catalogo: snap.catalogo || [],
|
|
196
|
+
fichas: snap.fichas || {},
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function acquireData() {
|
|
201
|
+
var errors = []
|
|
202
|
+
var loaders = [
|
|
203
|
+
["live", loadFromApi],
|
|
204
|
+
["static", loadFromFiles],
|
|
205
|
+
["snapshot", loadFromEmbedded],
|
|
206
|
+
]
|
|
207
|
+
for (var i = 0; i < loaders.length; i += 1) {
|
|
208
|
+
try {
|
|
209
|
+
return await loaders[i][1]()
|
|
210
|
+
} catch (error) {
|
|
211
|
+
errors.push(loaders[i][0] + ": " + error.message)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw new Error("sin fuente de datos — " + errors.join(" · "))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function signatureOf(data) {
|
|
218
|
+
var rosterUpdated = (data.roster && data.roster.updatedAt) || ""
|
|
219
|
+
var stateUpdated = (data.state && data.state.updatedAt) || ""
|
|
220
|
+
var cards = Object.keys((data.state && data.state.cards) || {}).length
|
|
221
|
+
var agents = Object.keys((data.roster && data.roster.agents) || {}).length
|
|
222
|
+
return [rosterUpdated, stateUpdated, cards, agents].join("|")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Merge the local projection into the real roster so the UI can show the
|
|
227
|
+
* fabricated agent without pretending roster.json changed on disk.
|
|
228
|
+
*/
|
|
229
|
+
function effectiveRoster() {
|
|
230
|
+
if (!app.snapshot) return { agents: {} }
|
|
231
|
+
if (!app.fabricated) return app.snapshot.roster
|
|
232
|
+
return Core.registerAgent(app.snapshot.roster, app.fabricated, {
|
|
233
|
+
now: app.fabricated.fabricatedAt,
|
|
234
|
+
cardTitle: cardById(app.fabricated.fabricatedFor)
|
|
235
|
+
? cardById(app.fabricated.fabricatedFor).title
|
|
236
|
+
: null,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function cardById(id) {
|
|
241
|
+
if (!app.snapshot) return null
|
|
242
|
+
return (
|
|
243
|
+
Core.parseCards(app.snapshot.state).filter(function (card) {
|
|
244
|
+
return card.id === id
|
|
245
|
+
})[0] || null
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------- sync
|
|
250
|
+
async function sync(options) {
|
|
251
|
+
var opts = options || {}
|
|
252
|
+
if (app.running) return
|
|
253
|
+
try {
|
|
254
|
+
var data = await acquireData()
|
|
255
|
+
var signature = signatureOf(data)
|
|
256
|
+
var changed = signature !== app.lastSignature
|
|
257
|
+
app.snapshot = data
|
|
258
|
+
app.sourceKind = data.source
|
|
259
|
+
app.lastSignature = signature
|
|
260
|
+
app.startedAt = data.capturedAt || new Date().toISOString()
|
|
261
|
+
|
|
262
|
+
// Confirm a projection that actually landed in roster.json.
|
|
263
|
+
if (
|
|
264
|
+
app.fabricated &&
|
|
265
|
+
data.roster &&
|
|
266
|
+
data.roster.agents &&
|
|
267
|
+
data.roster.agents[app.fabricated.id]
|
|
268
|
+
) {
|
|
269
|
+
app.confirmed = true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
recompute()
|
|
273
|
+
renderAll()
|
|
274
|
+
setSourceChip(data.source)
|
|
275
|
+
els.clockChip.textContent = "sync " + timeOf(app.startedAt)
|
|
276
|
+
els.clockChip.title = "última lectura: " + app.startedAt
|
|
277
|
+
|
|
278
|
+
if (changed) {
|
|
279
|
+
app.unchanged = 0
|
|
280
|
+
app.interval = 800
|
|
281
|
+
} else {
|
|
282
|
+
app.unchanged += 1
|
|
283
|
+
if (app.unchanged > 8) app.interval = clamp(Math.round(app.interval * 1.5), 800, 4000)
|
|
284
|
+
}
|
|
285
|
+
els.pollChip.textContent = "poll " + app.interval + "ms"
|
|
286
|
+
if (opts.manual) announce("Datos sincronizados desde " + data.source + ".")
|
|
287
|
+
} catch (error) {
|
|
288
|
+
app.sourceKind = "error"
|
|
289
|
+
setSourceChip("error")
|
|
290
|
+
els.clockChip.textContent = "sin datos"
|
|
291
|
+
if (opts.manual) announce("No se pudo leer .oficina/: " + error.message)
|
|
292
|
+
console.error("[factory]", error)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function setSourceChip(kind) {
|
|
297
|
+
var labels = {
|
|
298
|
+
live: "LIVE · api",
|
|
299
|
+
static: "STATIC · http",
|
|
300
|
+
snapshot: "OFFLINE · snapshot",
|
|
301
|
+
error: "SIN DATOS",
|
|
302
|
+
}
|
|
303
|
+
els.sourceChip.setAttribute("data-live", kind)
|
|
304
|
+
els.sourceText.textContent = labels[kind] || kind
|
|
305
|
+
els.sourceChip.title =
|
|
306
|
+
kind === "live"
|
|
307
|
+
? "leyendo .oficina/ vía dashboard/serve.mjs"
|
|
308
|
+
: kind === "static"
|
|
309
|
+
? "leyendo ../.oficina/*.json por HTTP"
|
|
310
|
+
: kind === "snapshot"
|
|
311
|
+
? "snapshot embebido: regeneralo con node dashboard/factory/build-snapshot.mjs"
|
|
312
|
+
: "no se pudo leer .oficina/"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------- compute
|
|
316
|
+
function recompute() {
|
|
317
|
+
if (!app.snapshot) return
|
|
318
|
+
var roster = effectiveRoster()
|
|
319
|
+
app.need = Core.detectFactoryNeed(app.snapshot.state, roster, {
|
|
320
|
+
threshold: Core.COVERAGE_THRESHOLD,
|
|
321
|
+
})
|
|
322
|
+
app.preview = buildPreview()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function focusGap() {
|
|
326
|
+
if (!app.need) return null
|
|
327
|
+
var gaps = app.need.gaps
|
|
328
|
+
var chosen = gaps.filter(function (gap) {
|
|
329
|
+
return gap.cardId === app.focusId
|
|
330
|
+
})[0]
|
|
331
|
+
if (chosen) return chosen
|
|
332
|
+
var working = gaps.filter(function (gap) {
|
|
333
|
+
return gap.state === "in_progress"
|
|
334
|
+
})[0]
|
|
335
|
+
return working || gaps[0] || null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildPreview() {
|
|
339
|
+
var gap = focusGap()
|
|
340
|
+
if (!gap) return null
|
|
341
|
+
var card = cardById(gap.cardId)
|
|
342
|
+
if (!card) return null
|
|
343
|
+
var agent = Core.synthesizeAgent(card, app.need.agents)
|
|
344
|
+
var projection = Core.projectMatch(app.snapshot.state, effectiveRoster(), {
|
|
345
|
+
fabricated: agent,
|
|
346
|
+
threshold: Core.COVERAGE_THRESHOLD,
|
|
347
|
+
})
|
|
348
|
+
var self = projection.rows.filter(function (row) {
|
|
349
|
+
return row.cardId === card.id
|
|
350
|
+
})[0]
|
|
351
|
+
var next = projection.nextMatch
|
|
352
|
+
return {
|
|
353
|
+
card: card,
|
|
354
|
+
agent: agent,
|
|
355
|
+
row: self,
|
|
356
|
+
next: next,
|
|
357
|
+
projection: projection,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------- render
|
|
362
|
+
function renderAll() {
|
|
363
|
+
renderDetection()
|
|
364
|
+
renderRoster()
|
|
365
|
+
renderMatch()
|
|
366
|
+
renderHero()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function renderHero() {
|
|
370
|
+
var preview = app.preview
|
|
371
|
+
var gaps = app.need ? app.need.gaps.length : 0
|
|
372
|
+
els.countsChip.textContent =
|
|
373
|
+
(app.need ? app.need.cardCount : 0) +
|
|
374
|
+
" cards · " +
|
|
375
|
+
(app.need ? app.need.agentCount : 0) +
|
|
376
|
+
" agentes"
|
|
377
|
+
|
|
378
|
+
if (app.running) return
|
|
379
|
+
|
|
380
|
+
if (!preview) {
|
|
381
|
+
els.btnFabricate.disabled = true
|
|
382
|
+
els.btnFabricate.classList.remove("btn-armed")
|
|
383
|
+
els.btnFabricate.querySelector(".btn-label").textContent =
|
|
384
|
+
"Catálogo cubre el plan"
|
|
385
|
+
els.fabricateHint.textContent =
|
|
386
|
+
"Ninguna card abierta cae por debajo del umbral."
|
|
387
|
+
els.sceneMoldLabel.textContent = "—"
|
|
388
|
+
els.sceneAgent.textContent = "sin huecos"
|
|
389
|
+
els.sceneOutLabel.textContent = "—"
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
els.btnFabricate.disabled = false
|
|
394
|
+
els.btnFabricate.classList.add("btn-armed")
|
|
395
|
+
els.btnFabricate.querySelector(".btn-label").textContent =
|
|
396
|
+
gaps > 1 ? "Fabricar agente (" + gaps + " huecos)" : "Fabricar agente"
|
|
397
|
+
els.fabricateHint.textContent =
|
|
398
|
+
preview.agent.displayName + " → " + preview.card.id + " · " + preview.card.title
|
|
399
|
+
els.sceneMoldLabel.textContent = preview.card.id
|
|
400
|
+
els.sceneAgent.textContent = app.fabricated
|
|
401
|
+
? app.fabricated.displayName
|
|
402
|
+
: preview.agent.displayName
|
|
403
|
+
els.sceneOutLabel.textContent = app.fabricated
|
|
404
|
+
? app.fabricated.id.replace(/^e-factory-/, "")
|
|
405
|
+
: preview.agent.role
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function renderDetection() {
|
|
409
|
+
if (!app.need) return
|
|
410
|
+
var need = app.need
|
|
411
|
+
var pct = need.overallPct
|
|
412
|
+
els.coverageValue.innerHTML = pct + "<small>%</small>"
|
|
413
|
+
els.coverageFill.style.width = clamp(pct, 0, 100) + "%"
|
|
414
|
+
els.coverageFill.setAttribute(
|
|
415
|
+
"data-tone",
|
|
416
|
+
pct >= need.thresholdPct ? "ok" : "bad",
|
|
417
|
+
)
|
|
418
|
+
els.coverageFill.parentElement.setAttribute("aria-valuenow", String(pct))
|
|
419
|
+
els.coverageThreshold.style.left = need.thresholdPct + "%"
|
|
420
|
+
els.coverageThreshold.setAttribute(
|
|
421
|
+
"data-label",
|
|
422
|
+
"umbral " + need.thresholdPct + "%",
|
|
423
|
+
)
|
|
424
|
+
els.gapCount.textContent = need.gaps.length + " gaps"
|
|
425
|
+
els.coverageNote.textContent = app.fabricated
|
|
426
|
+
? "incluye 1 agente fabricado (proyección local)"
|
|
427
|
+
: need.agentCount + " agentes en catálogo · " + need.cardCount + " cards"
|
|
428
|
+
|
|
429
|
+
var gap = focusGap()
|
|
430
|
+
if (gap && !app.focusId) app.focusId = gap.cardId
|
|
431
|
+
|
|
432
|
+
if (need.gaps.length === 0) {
|
|
433
|
+
els.gapList.innerHTML =
|
|
434
|
+
'<div class="empty"><strong>Catálogo completo</strong>' +
|
|
435
|
+
"Todas las cards abiertas superan el " +
|
|
436
|
+
need.thresholdPct +
|
|
437
|
+
"% de cobertura. La factory queda en espera.</div>"
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
var html = need.gaps
|
|
442
|
+
.map(function (item) {
|
|
443
|
+
var missing = item.missing
|
|
444
|
+
var chips = missing
|
|
445
|
+
.slice(0, 4)
|
|
446
|
+
.map(function (cap) {
|
|
447
|
+
return '<span class="cap" data-kind="missing">' + esc(cap) + "</span>"
|
|
448
|
+
})
|
|
449
|
+
.join("")
|
|
450
|
+
if (missing.length > 4) {
|
|
451
|
+
chips += '<span class="cap">+' + (missing.length - 4) + "</span>"
|
|
452
|
+
}
|
|
453
|
+
if (missing.length === 0) {
|
|
454
|
+
chips = '<span class="cap">sin requisitos</span>'
|
|
455
|
+
}
|
|
456
|
+
return (
|
|
457
|
+
'<button type="button" class="gap-card" data-card="' +
|
|
458
|
+
esc(item.cardId) +
|
|
459
|
+
'" aria-pressed="' +
|
|
460
|
+
(app.focusId === item.cardId ? "true" : "false") +
|
|
461
|
+
'" aria-label="Card ' +
|
|
462
|
+
esc(item.cardId) +
|
|
463
|
+
", cobertura " +
|
|
464
|
+
item.pct +
|
|
465
|
+
"%. " +
|
|
466
|
+
esc(item.title) +
|
|
467
|
+
'">' +
|
|
468
|
+
'<span class="gap-top">' +
|
|
469
|
+
'<span class="gap-id">' +
|
|
470
|
+
esc(item.cardId) +
|
|
471
|
+
"</span>" +
|
|
472
|
+
'<span class="badge" data-state="' +
|
|
473
|
+
esc(item.state) +
|
|
474
|
+
'">' +
|
|
475
|
+
esc(item.state) +
|
|
476
|
+
"</span>" +
|
|
477
|
+
(item.owner ? '<span class="cap">' + esc(item.owner) + "</span>" : "") +
|
|
478
|
+
'<span class="gap-pct" data-tone="' +
|
|
479
|
+
(item.pct >= need.thresholdPct ? "ok" : "bad") +
|
|
480
|
+
'">' +
|
|
481
|
+
item.pct +
|
|
482
|
+
"%</span></span>" +
|
|
483
|
+
'<span class="gap-title">' +
|
|
484
|
+
esc(item.title) +
|
|
485
|
+
"</span>" +
|
|
486
|
+
'<span class="gap-missing">' +
|
|
487
|
+
chips +
|
|
488
|
+
"</span>" +
|
|
489
|
+
"</button>"
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
.join("")
|
|
493
|
+
els.gapList.innerHTML = html
|
|
494
|
+
|
|
495
|
+
$$(".gap-card", els.gapList).forEach(function (button) {
|
|
496
|
+
button.addEventListener("click", function () {
|
|
497
|
+
if (app.running) return
|
|
498
|
+
selectCard(button.getAttribute("data-card"))
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function selectCard(cardId) {
|
|
504
|
+
if (!cardId || cardId === app.focusId) return
|
|
505
|
+
app.focusId = cardId
|
|
506
|
+
resetSequence()
|
|
507
|
+
recompute()
|
|
508
|
+
renderDetection()
|
|
509
|
+
renderMatch()
|
|
510
|
+
renderHero()
|
|
511
|
+
announce("Card " + cardId + " seleccionada como objetivo de fabricación.")
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function renderRoster(options) {
|
|
515
|
+
var opts = options || {}
|
|
516
|
+
var agents = Core.parseAgents(effectiveRoster())
|
|
517
|
+
agents.sort(function (a, b) {
|
|
518
|
+
var fabricatedA = a.source && a.source.kind === "factory" ? 0 : 1
|
|
519
|
+
var fabricatedB = b.source && b.source.kind === "factory" ? 0 : 1
|
|
520
|
+
if (fabricatedA !== fabricatedB) return fabricatedA - fabricatedB
|
|
521
|
+
if (a.state !== b.state) return a.state === "working" ? -1 : 1
|
|
522
|
+
return a.displayName.localeCompare(b.displayName)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
els.rosterCount.textContent =
|
|
526
|
+
agents.length + (app.fabricated ? " · proyección" : "")
|
|
527
|
+
els.rosterGrid.innerHTML = agents
|
|
528
|
+
.map(function (agent) {
|
|
529
|
+
var fabricated = agent.source && agent.source.kind === "factory"
|
|
530
|
+
return (
|
|
531
|
+
'<button type="button" class="agent-chip" data-agent="' +
|
|
532
|
+
esc(agent.id) +
|
|
533
|
+
'" data-fabricated="' +
|
|
534
|
+
(fabricated ? "true" : "false") +
|
|
535
|
+
(opts.newId === agent.id ? '" data-new="true' : "") +
|
|
536
|
+
'" aria-label="Abrir ficha de ' +
|
|
537
|
+
esc(agent.displayName) +
|
|
538
|
+
", " +
|
|
539
|
+
esc(agent.role) +
|
|
540
|
+
'">' +
|
|
541
|
+
'<span class="agent-avatar" aria-hidden="true">' +
|
|
542
|
+
esc(initials(agent.displayName)) +
|
|
543
|
+
"</span>" +
|
|
544
|
+
'<span class="agent-main"><span class="agent-name">' +
|
|
545
|
+
esc(agent.displayName) +
|
|
546
|
+
'</span><span class="agent-role">' +
|
|
547
|
+
esc(agent.role) +
|
|
548
|
+
"</span></span>" +
|
|
549
|
+
'<span class="agent-state" data-state="' +
|
|
550
|
+
esc(agent.state) +
|
|
551
|
+
'"><i></i>' +
|
|
552
|
+
esc(agent.state.replace(/_/g, " ")) +
|
|
553
|
+
"</span>" +
|
|
554
|
+
"</button>"
|
|
555
|
+
)
|
|
556
|
+
})
|
|
557
|
+
.join("")
|
|
558
|
+
|
|
559
|
+
$$(".agent-chip[data-new]", els.rosterGrid).forEach(function (chip) {
|
|
560
|
+
chip.classList.add("is-new")
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
$$(".agent-chip", els.rosterGrid).forEach(function (chip) {
|
|
564
|
+
chip.addEventListener("click", function () {
|
|
565
|
+
var agent = agents.filter(function (item) {
|
|
566
|
+
return item.id === chip.getAttribute("data-agent")
|
|
567
|
+
})[0]
|
|
568
|
+
if (agent) openAgent(agent)
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function renderMatch(options) {
|
|
574
|
+
var opts = options || {}
|
|
575
|
+
if (!app.preview) {
|
|
576
|
+
els.matchBody.innerHTML =
|
|
577
|
+
'<div class="empty">Sin proyección: el catálogo ya cubre las cards abiertas.</div>'
|
|
578
|
+
els.matchCount.textContent = "0"
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
var preview = app.preview
|
|
582
|
+
var rows = []
|
|
583
|
+
rows.push({ row: preview.row, label: "dispara", card: preview.card })
|
|
584
|
+
if (preview.next && preview.next.cardId !== preview.card.id) {
|
|
585
|
+
rows.push({ row: preview.next, label: "próxima card", card: null })
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
els.matchCount.textContent = rows.length + " cards"
|
|
589
|
+
els.matchBody.innerHTML = rows
|
|
590
|
+
.map(function (entry, index) {
|
|
591
|
+
var row = entry.row
|
|
592
|
+
var revealed = opts.revealed || app.fabricated
|
|
593
|
+
var afterPct = revealed ? row.after.pct : row.before.pct
|
|
594
|
+
var matched = revealed && row.after.covered
|
|
595
|
+
return (
|
|
596
|
+
'<div class="match-card" data-matched="' +
|
|
597
|
+
(matched ? "true" : "false") +
|
|
598
|
+
'">' +
|
|
599
|
+
'<span class="match-stamp">MATCH</span>' +
|
|
600
|
+
'<span class="match-id">' +
|
|
601
|
+
esc(row.cardId) +
|
|
602
|
+
'</span><span class="match-name">' +
|
|
603
|
+
esc(entry.card ? entry.card.title : row.title) +
|
|
604
|
+
"</span>" +
|
|
605
|
+
'<div class="match-bars"><span>' +
|
|
606
|
+
(index === 0 ? "actual" : "match") +
|
|
607
|
+
'</span><div class="meter"><span class="meter-fill" data-tone="' +
|
|
608
|
+
(row.before.covered ? "ok" : "bad") +
|
|
609
|
+
'" style="width:' +
|
|
610
|
+
row.before.pct +
|
|
611
|
+
'%"></span></div><span class="match-value">' +
|
|
612
|
+
row.before.pct +
|
|
613
|
+
"%</span></div>" +
|
|
614
|
+
'<div class="match-bars"><span>post</span><div class="meter">' +
|
|
615
|
+
'<span class="meter-fill" data-tone="' +
|
|
616
|
+
(row.after.covered ? "ok" : "bad") +
|
|
617
|
+
'" style="width:' +
|
|
618
|
+
afterPct +
|
|
619
|
+
'%"></span></div><span class="match-value">' +
|
|
620
|
+
afterPct +
|
|
621
|
+
"%</span></div>" +
|
|
622
|
+
"</div>"
|
|
623
|
+
)
|
|
624
|
+
})
|
|
625
|
+
.join("")
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ---------------------------------------------------------------- steps
|
|
629
|
+
function setStep(index, status, meta) {
|
|
630
|
+
var step = els.steps[index]
|
|
631
|
+
if (!step) return
|
|
632
|
+
step.setAttribute("data-status", status)
|
|
633
|
+
if (status === "active") step.setAttribute("aria-current", "step")
|
|
634
|
+
else step.removeAttribute("aria-current")
|
|
635
|
+
var icon = $(".step-icon", step)
|
|
636
|
+
if (icon) {
|
|
637
|
+
if (status === "done") icon.textContent = "✓"
|
|
638
|
+
else if (status === "active") icon.textContent = "◆"
|
|
639
|
+
else if (status === "skipped") icon.textContent = "–"
|
|
640
|
+
else icon.textContent = String(index + 1).padStart(2, "0")
|
|
641
|
+
}
|
|
642
|
+
var end = $(".step-end", step)
|
|
643
|
+
if (end) {
|
|
644
|
+
end.textContent =
|
|
645
|
+
status === "done"
|
|
646
|
+
? "ok"
|
|
647
|
+
: status === "active"
|
|
648
|
+
? "run"
|
|
649
|
+
: status === "skipped"
|
|
650
|
+
? "skip"
|
|
651
|
+
: STEPS[index].end
|
|
652
|
+
}
|
|
653
|
+
if (meta != null) {
|
|
654
|
+
var metaEl = $(".step-meta", step)
|
|
655
|
+
if (metaEl) metaEl.textContent = meta
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function resetSequence() {
|
|
660
|
+
app.runToken += 1
|
|
661
|
+
app.running = false
|
|
662
|
+
app.fabricated = null
|
|
663
|
+
app.projection = null
|
|
664
|
+
app.confirmed = false
|
|
665
|
+
els.steps.forEach(function (_, index) {
|
|
666
|
+
setStep(index, "pending", "")
|
|
667
|
+
})
|
|
668
|
+
setScene("idle")
|
|
669
|
+
els.termPathText.textContent = "sin ficha en fabricación"
|
|
670
|
+
els.termTag.textContent = "idle"
|
|
671
|
+
els.termBody.innerHTML =
|
|
672
|
+
'<span class="term-placeholder">La ficha .md se escribe acá cuando la factory sintetiza un agente.</span>'
|
|
673
|
+
setStep(0, "pending", "esperando disparo")
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function setScene(name) {
|
|
677
|
+
els.stage.setAttribute("data-scene", name)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function kindOf(line) {
|
|
681
|
+
if (/^#{1,2} /.test(line)) return "head"
|
|
682
|
+
if (/^---$/.test(line)) return "front"
|
|
683
|
+
if (/^(description|mode|capabilities):/.test(line)) return "front"
|
|
684
|
+
if (/^#/.test(line)) return "dim"
|
|
685
|
+
return ""
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function scrollTerm() {
|
|
689
|
+
els.termBody.scrollTop = els.termBody.scrollHeight
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function typeFicha(lines, token) {
|
|
693
|
+
els.termBody.innerHTML = ""
|
|
694
|
+
var cursor = doc.createElement("span")
|
|
695
|
+
cursor.className = "cursor"
|
|
696
|
+
cursor.setAttribute("aria-hidden", "true")
|
|
697
|
+
var total = lines.length
|
|
698
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
699
|
+
if (token !== app.runToken) return
|
|
700
|
+
var text = lines[i]
|
|
701
|
+
var lineEl = doc.createElement("span")
|
|
702
|
+
lineEl.className = "term-line"
|
|
703
|
+
var kind = kindOf(text)
|
|
704
|
+
if (kind) lineEl.setAttribute("data-kind", kind)
|
|
705
|
+
lineEl.textContent = text || "\u00a0"
|
|
706
|
+
els.termBody.appendChild(lineEl)
|
|
707
|
+
els.termBody.appendChild(cursor)
|
|
708
|
+
scrollTerm()
|
|
709
|
+
setStep(2, "active", "escribiendo " + (i + 1) + "/" + total + " líneas")
|
|
710
|
+
if (!reducedMotion && text.length > 0) {
|
|
711
|
+
var perTick = Math.max(2, Math.ceil(text.length / 60))
|
|
712
|
+
for (var pos = perTick; pos < text.length; pos += perTick) {
|
|
713
|
+
if (token !== app.runToken) return
|
|
714
|
+
lineEl.textContent = text.slice(0, pos)
|
|
715
|
+
scrollTerm()
|
|
716
|
+
await sleep(text.length > 70 ? 5 : 9)
|
|
717
|
+
}
|
|
718
|
+
lineEl.textContent = text
|
|
719
|
+
}
|
|
720
|
+
if (i % 3 === 0) {
|
|
721
|
+
// yield so the UI can paint during long fichas
|
|
722
|
+
await sleep(reducedMotion ? 0 : 12)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (token !== app.runToken) return
|
|
726
|
+
setStep(2, "done", total + " líneas · " + app.fabricated.fichaPath)
|
|
727
|
+
els.termTag.textContent = "escrita"
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ---------------------------------------------------------------- fabricate
|
|
731
|
+
async function fabricate(cardId) {
|
|
732
|
+
if (app.running || !app.preview) return
|
|
733
|
+
var token = ++app.runToken
|
|
734
|
+
var preview = app.preview
|
|
735
|
+
var card = preview.card
|
|
736
|
+
var agent = Core.synthesizeAgent(card, app.need.agents, {
|
|
737
|
+
now: new Date().toISOString(),
|
|
738
|
+
})
|
|
739
|
+
var projection = Core.projectMatch(app.snapshot.state, effectiveRoster(), {
|
|
740
|
+
fabricated: agent,
|
|
741
|
+
threshold: Core.COVERAGE_THRESHOLD,
|
|
742
|
+
})
|
|
743
|
+
var speed = reducedMotion ? 0.05 : 1
|
|
744
|
+
var wait = function (ms) {
|
|
745
|
+
return sleep(ms * speed)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
app.running = true
|
|
749
|
+
app.fabricated = agent
|
|
750
|
+
app.projection = projection
|
|
751
|
+
app.confirmed = false
|
|
752
|
+
els.btnFabricate.disabled = true
|
|
753
|
+
els.fabricateHint.textContent = "fabricando " + agent.id + "…"
|
|
754
|
+
els.sceneMoldLabel.textContent = card.id
|
|
755
|
+
els.sceneAgent.textContent = agent.displayName
|
|
756
|
+
els.sceneOutLabel.textContent = agent.role
|
|
757
|
+
els.steps.forEach(function (_, index) {
|
|
758
|
+
setStep(index, "pending", "")
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
// 1 · detection
|
|
762
|
+
setScene("detect")
|
|
763
|
+
setStep(0, "active", "cobertura " + preview.row.before.pct + "% < 60% · " + card.id)
|
|
764
|
+
announce("Factory detectó cobertura insuficiente en " + card.id + ".")
|
|
765
|
+
await wait(620)
|
|
766
|
+
if (token !== app.runToken) return
|
|
767
|
+
setStep(0, "done", card.id + " · " + preview.row.before.pct + "% · faltan " + preview.row.before.missing.length)
|
|
768
|
+
|
|
769
|
+
// 2 · synthesize
|
|
770
|
+
setScene("synthesize")
|
|
771
|
+
setStep(1, "active", "rol: " + agent.role)
|
|
772
|
+
await wait(780)
|
|
773
|
+
if (token !== app.runToken) return
|
|
774
|
+
setStep(1, "done", agent.capabilities.length + " capabilities · " + agent.id)
|
|
775
|
+
|
|
776
|
+
// 3 · write the ficha
|
|
777
|
+
setScene("write")
|
|
778
|
+
els.termPathText.textContent = agent.fichaPath
|
|
779
|
+
els.termTag.textContent = "escribiendo"
|
|
780
|
+
els.termPath.title = agent.fichaPath
|
|
781
|
+
var lines = Core.renderFichaLines(agent, card)
|
|
782
|
+
var dirLine = "# oficina factory --card " + card.id + " --role " + agent.role
|
|
783
|
+
await typeFicha([dirLine].concat(lines), token)
|
|
784
|
+
if (token !== app.runToken) return
|
|
785
|
+
|
|
786
|
+
// 4 · register in the catalog
|
|
787
|
+
setScene("register")
|
|
788
|
+
setStep(3, "active", "roster.json ← " + agent.id)
|
|
789
|
+
renderRoster({ newId: agent.id })
|
|
790
|
+
announce(agent.displayName + " registrado en el catálogo.")
|
|
791
|
+
await wait(760)
|
|
792
|
+
if (token !== app.runToken) return
|
|
793
|
+
setStep(3, "done", (app.need.agentCount + 1) + " agentes en catálogo")
|
|
794
|
+
|
|
795
|
+
// 5 · next card matches
|
|
796
|
+
setScene("match")
|
|
797
|
+
setStep(4, "active", "recalculando cobertura…")
|
|
798
|
+
recompute()
|
|
799
|
+
renderDetection()
|
|
800
|
+
renderMatch({ revealed: true })
|
|
801
|
+
await wait(760)
|
|
802
|
+
if (token !== app.runToken) return
|
|
803
|
+
var next = projection.nextMatch
|
|
804
|
+
setStep(
|
|
805
|
+
4,
|
|
806
|
+
"done",
|
|
807
|
+
next
|
|
808
|
+
? next.cardId + " · " + next.before.pct + "% → " + next.after.pct + "%"
|
|
809
|
+
: card.id + " · " + preview.row.before.pct + "% → " + preview.row.after.pct + "%",
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
setScene("done")
|
|
813
|
+
app.running = false
|
|
814
|
+
els.termTag.textContent = "lista"
|
|
815
|
+
renderHero()
|
|
816
|
+
showToast(agent, { next: next, card: card })
|
|
817
|
+
announce(
|
|
818
|
+
"Factory completa: " +
|
|
819
|
+
agent.displayName +
|
|
820
|
+
" fabricado y agregado al roster. Click en la notificación para abrir su ficha.",
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ---------------------------------------------------------------- toasts
|
|
825
|
+
var toastTimers = []
|
|
826
|
+
|
|
827
|
+
function showToast(agent, options) {
|
|
828
|
+
var opts = options || {}
|
|
829
|
+
var next = opts.next
|
|
830
|
+
var toast = doc.createElement("div")
|
|
831
|
+
toast.className = "toast"
|
|
832
|
+
toast.setAttribute("role", "group")
|
|
833
|
+
toast.setAttribute("aria-label", "Factory completa: " + agent.displayName)
|
|
834
|
+
|
|
835
|
+
var detail = next
|
|
836
|
+
? "match en " + next.cardId + " · " + next.before.pct + "% → " + next.after.pct + "%"
|
|
837
|
+
: "registrado en el roster"
|
|
838
|
+
|
|
839
|
+
toast.innerHTML =
|
|
840
|
+
'<button type="button" class="toast-open">' +
|
|
841
|
+
'<span class="toast-kicker">factory · completa</span>' +
|
|
842
|
+
'<span class="toast-name">' +
|
|
843
|
+
esc(agent.displayName) +
|
|
844
|
+
"</span>" +
|
|
845
|
+
'<span class="toast-body">' +
|
|
846
|
+
esc(detail) +
|
|
847
|
+
"</span>" +
|
|
848
|
+
'<span class="toast-cta">Abrir ficha <span aria-hidden="true">→</span></span>' +
|
|
849
|
+
"</button>" +
|
|
850
|
+
'<button type="button" class="toast-close" aria-label="Cerrar notificación">×</button>' +
|
|
851
|
+
'<span class="toast-timer" aria-hidden="true"></span>'
|
|
852
|
+
|
|
853
|
+
var dismiss = function () {
|
|
854
|
+
if (toast.classList.contains("is-leaving")) return
|
|
855
|
+
toast.classList.add("is-leaving")
|
|
856
|
+
setTimeout(function () {
|
|
857
|
+
if (toast.parentNode) toast.parentNode.removeChild(toast)
|
|
858
|
+
}, 240)
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
$(".toast-open", toast).addEventListener("click", function () {
|
|
862
|
+
openAgent(agent)
|
|
863
|
+
dismiss()
|
|
864
|
+
})
|
|
865
|
+
$(".toast-close", toast).addEventListener("click", dismiss)
|
|
866
|
+
|
|
867
|
+
els.toasts.appendChild(toast)
|
|
868
|
+
while (els.toasts.children.length > 3) {
|
|
869
|
+
els.toasts.removeChild(els.toasts.firstChild)
|
|
870
|
+
}
|
|
871
|
+
var timer = setTimeout(dismiss, 12000)
|
|
872
|
+
toastTimers.push(timer)
|
|
873
|
+
toast.addEventListener("mouseenter", function () {
|
|
874
|
+
clearTimeout(timer)
|
|
875
|
+
})
|
|
876
|
+
toast.addEventListener("focusin", function () {
|
|
877
|
+
clearTimeout(timer)
|
|
878
|
+
})
|
|
879
|
+
announce("Notificación: " + agent.displayName + " listo. Enter para abrir su ficha.")
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// ---------------------------------------------------------------- modal
|
|
883
|
+
function toRepoRelative(path) {
|
|
884
|
+
if (!path) return ""
|
|
885
|
+
var normalized = String(path).replace(/\\/g, "/")
|
|
886
|
+
var marker = normalized.indexOf(".opencode/")
|
|
887
|
+
if (marker >= 0) return normalized.slice(marker)
|
|
888
|
+
var agentMarker = normalized.indexOf("agents/")
|
|
889
|
+
if (agentMarker >= 0) return ".opencode/" + normalized.slice(agentMarker)
|
|
890
|
+
return normalized.replace(/^\.?\//, "")
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
async function resolveFicha(agent) {
|
|
894
|
+
var rel = toRepoRelative(agent.fichaPath)
|
|
895
|
+
var snap = app.snapshot
|
|
896
|
+
if (snap && snap.fichas && rel && snap.fichas[rel]) {
|
|
897
|
+
return { content: snap.fichas[rel].content, origin: "snapshot" }
|
|
898
|
+
}
|
|
899
|
+
if (app.sourceKind === "live" && rel) {
|
|
900
|
+
try {
|
|
901
|
+
var response = await fetch("/api/ficha?path=" + encodeURIComponent(rel), {
|
|
902
|
+
cache: "no-store",
|
|
903
|
+
})
|
|
904
|
+
if (response.ok) {
|
|
905
|
+
var data = await response.json()
|
|
906
|
+
if (data && data.content) return { content: data.content, origin: "disco" }
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
/* fall through */
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (rel) {
|
|
913
|
+
try {
|
|
914
|
+
var raw = await fetch("../" + rel, { cache: "no-store" })
|
|
915
|
+
if (raw.ok) {
|
|
916
|
+
var text = await raw.text()
|
|
917
|
+
if (text && text.trim().length > 0) return { content: text, origin: "disco" }
|
|
918
|
+
}
|
|
919
|
+
} catch (error) {
|
|
920
|
+
/* fall through */
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
var card = cardById(agent.source && agent.source.fabricatedFor)
|
|
924
|
+
return {
|
|
925
|
+
content: Core.renderFichaMarkdown(agent, card),
|
|
926
|
+
origin: "proyectada",
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function inlineCode(text) {
|
|
931
|
+
return esc(text).replace(/`([^`]+)`/g, function (_, code) {
|
|
932
|
+
return "<code>" + code + "</code>"
|
|
933
|
+
})
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function renderMarkdown(container, markdown) {
|
|
937
|
+
container.innerHTML = ""
|
|
938
|
+
var lines = String(markdown || "").split("\n")
|
|
939
|
+
var i = 0
|
|
940
|
+
if (lines[0] === "---") {
|
|
941
|
+
var front = []
|
|
942
|
+
i = 1
|
|
943
|
+
while (i < lines.length && lines[i] !== "---") {
|
|
944
|
+
front.push(lines[i])
|
|
945
|
+
i += 1
|
|
946
|
+
}
|
|
947
|
+
i += 1
|
|
948
|
+
var pre = doc.createElement("pre")
|
|
949
|
+
pre.className = "md-front"
|
|
950
|
+
pre.textContent = front.join("\n")
|
|
951
|
+
container.appendChild(pre)
|
|
952
|
+
}
|
|
953
|
+
var list = null
|
|
954
|
+
for (; i < lines.length; i += 1) {
|
|
955
|
+
var line = lines[i]
|
|
956
|
+
if (/^\s*$/.test(line)) {
|
|
957
|
+
list = null
|
|
958
|
+
continue
|
|
959
|
+
}
|
|
960
|
+
if (/^#\s/.test(line)) {
|
|
961
|
+
var h3 = doc.createElement("h3")
|
|
962
|
+
h3.textContent = line.replace(/^#\s+/, "")
|
|
963
|
+
container.appendChild(h3)
|
|
964
|
+
list = null
|
|
965
|
+
continue
|
|
966
|
+
}
|
|
967
|
+
if (/^##\s/.test(line)) {
|
|
968
|
+
var h4 = doc.createElement("h4")
|
|
969
|
+
h4.textContent = line.replace(/^##\s+/, "")
|
|
970
|
+
container.appendChild(h4)
|
|
971
|
+
list = null
|
|
972
|
+
continue
|
|
973
|
+
}
|
|
974
|
+
if (/^\s*[-*]\s/.test(line)) {
|
|
975
|
+
if (!list) {
|
|
976
|
+
list = doc.createElement("ul")
|
|
977
|
+
container.appendChild(list)
|
|
978
|
+
}
|
|
979
|
+
var li = doc.createElement("li")
|
|
980
|
+
li.innerHTML = inlineCode(line.replace(/^\s*[-*]\s+/, ""))
|
|
981
|
+
list.appendChild(li)
|
|
982
|
+
continue
|
|
983
|
+
}
|
|
984
|
+
var p = doc.createElement("p")
|
|
985
|
+
p.innerHTML = inlineCode(line)
|
|
986
|
+
container.appendChild(p)
|
|
987
|
+
list = null
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function openAgent(agent) {
|
|
992
|
+
var card = cardById(agent.source && agent.source.fabricatedFor)
|
|
993
|
+
els.dialogAvatar.textContent = initials(agent.displayName)
|
|
994
|
+
els.dialogTitle.textContent = agent.displayName
|
|
995
|
+
els.dialogSub.textContent = agent.id + " · " + agent.role
|
|
996
|
+
els.dialogMeta.innerHTML =
|
|
997
|
+
agent.capabilities
|
|
998
|
+
.slice(0, 8)
|
|
999
|
+
.map(function (cap) {
|
|
1000
|
+
return '<span class="cap">' + esc(cap) + "</span>"
|
|
1001
|
+
})
|
|
1002
|
+
.join("") +
|
|
1003
|
+
(card
|
|
1004
|
+
? '<span class="cap" data-kind="missing">card ' + esc(card.id) + "</span>"
|
|
1005
|
+
: "") +
|
|
1006
|
+
(agent.source && agent.source.kind === "factory"
|
|
1007
|
+
? '<span class="badge" data-state="in_progress">fabricado</span>'
|
|
1008
|
+
: "")
|
|
1009
|
+
els.dialogBody.innerHTML = '<p class="md-front">cargando ficha…</p>'
|
|
1010
|
+
els.dialogOrigin.textContent = ""
|
|
1011
|
+
if (typeof els.dialog.showModal === "function") els.dialog.showModal()
|
|
1012
|
+
else els.dialog.setAttribute("open", "open")
|
|
1013
|
+
|
|
1014
|
+
var resolved = await resolveFicha(agent)
|
|
1015
|
+
app.fichaOrigin = resolved.origin
|
|
1016
|
+
renderMarkdown(els.dialogBody, resolved.content)
|
|
1017
|
+
els.dialogOrigin.textContent =
|
|
1018
|
+
resolved.origin === "proyectada"
|
|
1019
|
+
? "ficha proyectada (no escrita en disco)"
|
|
1020
|
+
: "ficha desde " + resolved.origin + " · " + toRepoRelative(agent.fichaPath)
|
|
1021
|
+
els.btnCopy.setAttribute(
|
|
1022
|
+
"data-copy",
|
|
1023
|
+
resolved.content,
|
|
1024
|
+
)
|
|
1025
|
+
announce("Ficha de " + agent.displayName + " abierta.")
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ---------------------------------------------------------------- controls
|
|
1029
|
+
function togglePause() {
|
|
1030
|
+
app.paused = !app.paused
|
|
1031
|
+
els.btnPause.setAttribute("aria-pressed", String(app.paused))
|
|
1032
|
+
els.btnPause.querySelector(".btn-label").textContent = app.paused
|
|
1033
|
+
? "Reanudar"
|
|
1034
|
+
: "Pausar"
|
|
1035
|
+
els.pollChip.textContent = app.paused ? "poll pausado" : "poll " + app.interval + "ms"
|
|
1036
|
+
announce(app.paused ? "Polling pausado." : "Polling reanudado.")
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function applyTheme(theme) {
|
|
1040
|
+
root.setAttribute("data-theme", theme)
|
|
1041
|
+
try {
|
|
1042
|
+
localStorage.setItem(STORAGE_THEME, theme)
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
/* private mode */
|
|
1045
|
+
}
|
|
1046
|
+
var dark = theme === "dark"
|
|
1047
|
+
els.btnTheme.setAttribute("aria-pressed", String(dark))
|
|
1048
|
+
els.btnTheme.setAttribute(
|
|
1049
|
+
"aria-label",
|
|
1050
|
+
dark ? "Cambiar a tema claro" : "Cambiar a tema oscuro",
|
|
1051
|
+
)
|
|
1052
|
+
els.btnTheme.querySelector(".btn-label").textContent = dark ? "Claro" : "Oscuro"
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function initTheme() {
|
|
1056
|
+
var stored = null
|
|
1057
|
+
try {
|
|
1058
|
+
stored = localStorage.getItem(STORAGE_THEME)
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
stored = null
|
|
1061
|
+
}
|
|
1062
|
+
var prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches
|
|
1063
|
+
applyTheme(stored || (prefersLight ? "light" : "dark"))
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function copyFicha() {
|
|
1067
|
+
var content = els.btnCopy.getAttribute("data-copy") || ""
|
|
1068
|
+
var done = function () {
|
|
1069
|
+
els.btnCopy.querySelector(".btn-label").textContent = "Copiada"
|
|
1070
|
+
setTimeout(function () {
|
|
1071
|
+
els.btnCopy.querySelector(".btn-label").textContent = "Copiar ficha"
|
|
1072
|
+
}, 1800)
|
|
1073
|
+
}
|
|
1074
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1075
|
+
navigator.clipboard.writeText(content).then(done, function () {
|
|
1076
|
+
announce("No se pudo copiar la ficha.")
|
|
1077
|
+
})
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
announce("El navegador no permite copiar; seleccioná el texto a mano.")
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ---------------------------------------------------------------- polling
|
|
1084
|
+
function startPolling() {
|
|
1085
|
+
setInterval(function () {
|
|
1086
|
+
if (app.paused || app.running) return
|
|
1087
|
+
app.tick += 1
|
|
1088
|
+
var rosterTurn = app.tick % 3 === 0
|
|
1089
|
+
sync({ roster: rosterTurn }).catch(function () {})
|
|
1090
|
+
}, 800)
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function wire() {
|
|
1094
|
+
els.btnFabricate.addEventListener("click", function () {
|
|
1095
|
+
var gap = focusGap()
|
|
1096
|
+
fabricate(gap ? gap.cardId : null)
|
|
1097
|
+
})
|
|
1098
|
+
els.btnPause.addEventListener("click", togglePause)
|
|
1099
|
+
els.btnResync.addEventListener("click", function () {
|
|
1100
|
+
sync({ manual: true })
|
|
1101
|
+
})
|
|
1102
|
+
els.btnTheme.addEventListener("click", function () {
|
|
1103
|
+
var next = root.getAttribute("data-theme") === "dark" ? "light" : "dark"
|
|
1104
|
+
applyTheme(next)
|
|
1105
|
+
announce("Tema " + next + " aplicado.")
|
|
1106
|
+
})
|
|
1107
|
+
els.btnCopy.addEventListener("click", copyFicha)
|
|
1108
|
+
$("#btn-dialog-close").addEventListener("click", function () {
|
|
1109
|
+
els.dialog.close()
|
|
1110
|
+
})
|
|
1111
|
+
els.dialog.addEventListener("click", function (event) {
|
|
1112
|
+
if (event.target === els.dialog) els.dialog.close()
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
doc.addEventListener("keydown", function (event) {
|
|
1116
|
+
var tag = (event.target && event.target.tagName) || ""
|
|
1117
|
+
var typing = /INPUT|TEXTAREA|SELECT/.test(tag)
|
|
1118
|
+
if (typing) return
|
|
1119
|
+
if (event.key === "Escape") {
|
|
1120
|
+
if (els.dialog.open) return
|
|
1121
|
+
var keys = $(".keys[open]")
|
|
1122
|
+
if (keys) keys.removeAttribute("open")
|
|
1123
|
+
return
|
|
1124
|
+
}
|
|
1125
|
+
if (els.dialog.open) return
|
|
1126
|
+
if (event.key === " " && !/BUTTON|SUMMARY|A/.test(tag)) {
|
|
1127
|
+
event.preventDefault()
|
|
1128
|
+
togglePause()
|
|
1129
|
+
return
|
|
1130
|
+
}
|
|
1131
|
+
if (event.key === "r" || event.key === "R") {
|
|
1132
|
+
event.preventDefault()
|
|
1133
|
+
sync({ manual: true })
|
|
1134
|
+
return
|
|
1135
|
+
}
|
|
1136
|
+
if (event.key === "f" || event.key === "F") {
|
|
1137
|
+
event.preventDefault()
|
|
1138
|
+
if (!els.btnFabricate.disabled) els.btnFabricate.click()
|
|
1139
|
+
return
|
|
1140
|
+
}
|
|
1141
|
+
if (event.key === "?") {
|
|
1142
|
+
event.preventDefault()
|
|
1143
|
+
var details = $(".keys")
|
|
1144
|
+
var open = details.hasAttribute("open")
|
|
1145
|
+
if (open) details.removeAttribute("open")
|
|
1146
|
+
else details.setAttribute("open", "open")
|
|
1147
|
+
}
|
|
1148
|
+
})
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ---------------------------------------------------------------- boot
|
|
1152
|
+
async function boot() {
|
|
1153
|
+
initTheme()
|
|
1154
|
+
wire()
|
|
1155
|
+
resetSequence()
|
|
1156
|
+
startPolling()
|
|
1157
|
+
|
|
1158
|
+
await sync({ manual: false })
|
|
1159
|
+
if (!app.snapshot) {
|
|
1160
|
+
els.btnFabricate.disabled = true
|
|
1161
|
+
return
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
var params = new URLSearchParams(window.location.search)
|
|
1165
|
+
var requested = params.get("card")
|
|
1166
|
+
if (requested) {
|
|
1167
|
+
app.focusId = requested
|
|
1168
|
+
recompute()
|
|
1169
|
+
renderAll()
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
var autoRun = params.get("auto") === "1" || params.get("fabricate") === "1"
|
|
1173
|
+
if (autoRun && app.preview) {
|
|
1174
|
+
setTimeout(function () {
|
|
1175
|
+
fabricate(app.focusId)
|
|
1176
|
+
}, 700)
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (document.readyState === "loading") {
|
|
1181
|
+
document.addEventListener("DOMContentLoaded", boot)
|
|
1182
|
+
} else {
|
|
1183
|
+
boot()
|
|
1184
|
+
}
|
|
1185
|
+
})()
|