WIP: Claude Code progress on #7
Automated salvage commit — agent session ended (exit 124). Work in progress, may need continuation.
This commit is contained in:
154
artifacts/api-server/src/lib/tower-log.ts
Normal file
154
artifacts/api-server/src/lib/tower-log.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { db, towerLog } from "@workspace/db";
|
||||
import { desc } from "drizzle-orm";
|
||||
import { makeLogger } from "./logger.js";
|
||||
|
||||
const logger = makeLogger("tower-log");
|
||||
|
||||
const STUB_MODE =
|
||||
!process.env["AI_INTEGRATIONS_ANTHROPIC_API_KEY"] ||
|
||||
!process.env["AI_INTEGRATIONS_ANTHROPIC_BASE_URL"];
|
||||
|
||||
// Stub narratives for each event type
|
||||
const STUB_NARRATIVES: Record<string, string[]> = {
|
||||
"job:complete": [
|
||||
"Timmy completed a visitor's quest with wizardly precision, sending forth the fruits of his craft.",
|
||||
"The Workshop hums with satisfaction — another task fulfilled by Timmy's capable hands.",
|
||||
"A spell is cast and resolved; Timmy's work is done, Lightning sats exchanged for wisdom.",
|
||||
],
|
||||
"job:evaluating": [
|
||||
"Timmy peers into the crystal ball, studying a new request with keen arcane eyes.",
|
||||
"The gatekeeper stirs — Timmy weighs a visitor's petition before the Workshop gates.",
|
||||
"Beta's scales tip as Timmy evaluates the merit of a new task.",
|
||||
],
|
||||
"job:executing": [
|
||||
"Timmy's workshop blazes with activity as he tackles a visitor's request.",
|
||||
"Gears spin and lightning crackles — Timmy is hard at work on a quest.",
|
||||
"The wizard focuses, channeling deep knowledge into a visitor's commission.",
|
||||
],
|
||||
"visitor:enter": [
|
||||
"A new visitor steps through the Workshop door, drawn by the glow of Timmy's lantern.",
|
||||
"The crystal ball shimmers — someone new has arrived in the Workshop.",
|
||||
"Footsteps echo across the Workshop floor as a curious soul enters.",
|
||||
],
|
||||
"visitor:leave": [
|
||||
"A visitor departs the Workshop, their lantern lit by Timmy's wisdom.",
|
||||
"The door closes softly — another seeker leaves the Workshop enriched.",
|
||||
"A traveler takes their leave, carrying new knowledge into the wider world.",
|
||||
],
|
||||
"visitor:reply": [
|
||||
"Timmy offers a word of wizardly counsel to a curious visitor.",
|
||||
"The crystal ball speaks — Timmy shares his ancient wisdom.",
|
||||
],
|
||||
"default": [
|
||||
"The Workshop stirs with quiet activity.",
|
||||
"Timmy tends to the Workshop's many enchantments.",
|
||||
],
|
||||
};
|
||||
|
||||
function pickStub(eventType: string): string {
|
||||
const pool = STUB_NARRATIVES[eventType] ?? STUB_NARRATIVES["default"]!;
|
||||
return pool[Math.floor(Math.random() * pool.length)]!;
|
||||
}
|
||||
|
||||
interface AnthropicLike {
|
||||
messages: {
|
||||
create(params: Record<string, unknown>): Promise<{
|
||||
content: Array<{ type: string; text?: string }>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
let _anthropic: AnthropicLike | null = null;
|
||||
|
||||
async function getClient(): Promise<AnthropicLike> {
|
||||
if (_anthropic) return _anthropic;
|
||||
// @ts-expect-error -- dynamic import of integrations package
|
||||
const mod = (await import("@workspace/integrations-anthropic-ai")) as { anthropic: AnthropicLike };
|
||||
_anthropic = mod.anthropic;
|
||||
return _anthropic;
|
||||
}
|
||||
|
||||
const NARRATIVE_SYSTEM = `You are the Tower Chronicler — a mystical narrator who records the story of Timmy's Workshop in Timmy's voice: wizardly, warm, slightly epic. When given an event, write exactly 1-2 sentences of prose narrative about what happened. Keep it under 160 characters total. Use third person. No quotation marks.`;
|
||||
|
||||
const NARRATIVE_MODEL = "claude-haiku-4-5";
|
||||
|
||||
export async function generateNarrative(
|
||||
eventType: string,
|
||||
context: Record<string, string>,
|
||||
): Promise<string> {
|
||||
if (STUB_MODE) {
|
||||
return pickStub(eventType);
|
||||
}
|
||||
|
||||
const contextStr = Object.entries(context)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join(", ");
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
const msg = await client.messages.create({
|
||||
model: NARRATIVE_MODEL,
|
||||
max_tokens: 80,
|
||||
system: NARRATIVE_SYSTEM,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `Event: ${eventType}. Context: ${contextStr}. Write the narrative entry.`,
|
||||
},
|
||||
],
|
||||
});
|
||||
const block = msg.content[0];
|
||||
if (block?.type === "text" && block.text) {
|
||||
return block.text.trim().slice(0, 200);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("narrative generation failed, using stub", { err: String(err) });
|
||||
}
|
||||
return pickStub(eventType);
|
||||
}
|
||||
|
||||
export interface StoredEntry {
|
||||
id: string;
|
||||
eventType: string;
|
||||
narrative: string;
|
||||
agentId: string | null;
|
||||
jobId: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export async function addTowerLogEntry(
|
||||
eventType: string,
|
||||
narrative: string,
|
||||
agentId?: string,
|
||||
jobId?: string,
|
||||
): Promise<StoredEntry> {
|
||||
const entry: StoredEntry = {
|
||||
id: randomUUID(),
|
||||
eventType,
|
||||
narrative,
|
||||
agentId: agentId ?? null,
|
||||
jobId: jobId ?? null,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
try {
|
||||
await db.insert(towerLog).values(entry);
|
||||
} catch (err) {
|
||||
logger.warn("failed to insert tower_log entry", { err: String(err) });
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export async function getRecentEntries(limit = 20): Promise<StoredEntry[]> {
|
||||
try {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(towerLog)
|
||||
.orderBy(desc(towerLog.createdAt))
|
||||
.limit(limit);
|
||||
return rows.reverse() as StoredEntry[];
|
||||
} catch (err) {
|
||||
logger.warn("failed to fetch tower_log entries", { err: String(err) });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import { makeLogger } from "../lib/logger.js";
|
||||
import { getWorldState, setAgentStateInWorld } from "../lib/world-state.js";
|
||||
import { agentService } from "../lib/agent.js";
|
||||
import { db, worldEvents } from "@workspace/db";
|
||||
import { generateNarrative, addTowerLogEntry } from "../lib/tower-log.js";
|
||||
|
||||
const logger = makeLogger("ws-events");
|
||||
|
||||
@@ -95,6 +96,22 @@ async function logWorldEvent(
|
||||
}
|
||||
}
|
||||
|
||||
async function emitTowerLogEntry(
|
||||
wss: WebSocketServer,
|
||||
eventType: string,
|
||||
context: Record<string, string>,
|
||||
agentId?: string,
|
||||
jobId?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const narrative = await generateNarrative(eventType, context);
|
||||
const entry = await addTowerLogEntry(eventType, narrative, agentId, jobId);
|
||||
broadcastToAll(wss, { type: "tower_log_entry", entry });
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
function translateEvent(ev: BusEvent): object | null {
|
||||
switch (ev.type) {
|
||||
// ── Mode 1 job lifecycle ─────────────────────────────────────────────────
|
||||
@@ -299,6 +316,17 @@ async function sendWorldStateBootstrap(socket: WebSocket): Promise<void> {
|
||||
export function attachWebSocketServer(server: Server): void {
|
||||
const wss = new WebSocketServer({ server, path: "/api/ws" });
|
||||
|
||||
// Tower Log: generate narrative on key job events (module-level, once per wss)
|
||||
eventBus.on("bus", (ev: BusEvent) => {
|
||||
if (ev.type === "job:state" && ev.state === "complete") {
|
||||
void emitTowerLogEntry(wss, "job:complete", { jobId: ev.jobId.slice(0, 8) }, "alpha", ev.jobId);
|
||||
} else if (ev.type === "job:state" && ev.state === "evaluating") {
|
||||
void emitTowerLogEntry(wss, "job:evaluating", { jobId: ev.jobId.slice(0, 8) }, "beta", ev.jobId);
|
||||
} else if (ev.type === "job:state" && ev.state === "executing") {
|
||||
void emitTowerLogEntry(wss, "job:executing", { jobId: ev.jobId.slice(0, 8) }, "gamma", ev.jobId);
|
||||
}
|
||||
});
|
||||
|
||||
wss.on("connection", (socket: WebSocket, req: IncomingMessage) => {
|
||||
const ip = req.headers["x-forwarded-for"] ?? req.socket.remoteAddress ?? "unknown";
|
||||
logger.info("ws client connected", { ip, clients: wss.clients.size });
|
||||
@@ -326,6 +354,7 @@ export function attachWebSocketServer(server: Server): void {
|
||||
}
|
||||
});
|
||||
send(socket, { type: "visitor_count", count: wss.clients.size });
|
||||
void emitTowerLogEntry(wss, "visitor:enter", {}, "timmy");
|
||||
}
|
||||
if (msg.type === "visitor_leave") {
|
||||
wss.clients.forEach(c => {
|
||||
@@ -333,6 +362,7 @@ export function attachWebSocketServer(server: Server): void {
|
||||
c.send(JSON.stringify({ type: "visitor_count", count: Math.max(0, wss.clients.size - 1) }));
|
||||
}
|
||||
});
|
||||
void emitTowerLogEntry(wss, "visitor:leave", {}, "timmy");
|
||||
}
|
||||
if (msg.type === "visitor_message" && msg.text) {
|
||||
const text = String(msg.text).slice(0, 500);
|
||||
@@ -366,6 +396,7 @@ export function attachWebSocketServer(server: Server): void {
|
||||
broadcastToAll(wss, { type: "chat", agentId: "timmy", text: reply });
|
||||
|
||||
void logWorldEvent("visitor:reply", reply.slice(0, 100), "timmy");
|
||||
void emitTowerLogEntry(wss, "visitor:reply", { reply: reply.slice(0, 60) }, "timmy");
|
||||
} catch (err) {
|
||||
broadcastToAll(wss, { type: "agent_state", agentId: "gamma", state: "idle" });
|
||||
updateAgentWorld("gamma", "idle");
|
||||
|
||||
@@ -17,6 +17,7 @@ import relayRouter from "./relay.js";
|
||||
import adminRelayRouter from "./admin-relay.js";
|
||||
import adminRelayQueueRouter from "./admin-relay-queue.js";
|
||||
import geminiRouter from "./gemini.js";
|
||||
import towerLogRouter from "./tower-log.js";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -36,6 +37,7 @@ router.use(testkitRouter);
|
||||
router.use(uiRouter);
|
||||
router.use(nodeDiagnosticsRouter);
|
||||
router.use(worldRouter);
|
||||
router.use(towerLogRouter);
|
||||
|
||||
// Mount dev routes when NOT in production OR when LNbits is in stub mode.
|
||||
// Stub mode means there is no real Lightning backend — payments are simulated
|
||||
|
||||
18
artifacts/api-server/src/routes/tower-log.ts
Normal file
18
artifacts/api-server/src/routes/tower-log.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { getRecentEntries } from "../lib/tower-log.js";
|
||||
import { makeLogger } from "../lib/logger.js";
|
||||
|
||||
const logger = makeLogger("tower-log-route");
|
||||
const router = Router();
|
||||
|
||||
router.get("/tower-log", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const entries = await getRecentEntries(20);
|
||||
res.json({ entries });
|
||||
} catch (err) {
|
||||
logger.error("GET /api/tower-log failed", { error: String(err) });
|
||||
res.status(500).json({ error: "tower_log_error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
9
lib/db/migrations/0010_tower_log.sql
Normal file
9
lib/db/migrations/0010_tower_log.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Migration: Tower Log narrative event table (#7)
|
||||
CREATE TABLE IF NOT EXISTS tower_log (
|
||||
id TEXT PRIMARY KEY,
|
||||
event_type TEXT NOT NULL,
|
||||
narrative TEXT NOT NULL,
|
||||
agent_id TEXT,
|
||||
job_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -14,3 +14,4 @@ export * from "./relay-accounts";
|
||||
export * from "./relay-event-queue";
|
||||
export * from "./job-debates";
|
||||
export * from "./session-messages";
|
||||
export * from "./tower-log";
|
||||
|
||||
12
lib/db/src/schema/tower-log.ts
Normal file
12
lib/db/src/schema/tower-log.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
export const towerLog = pgTable("tower_log", {
|
||||
id: text("id").primaryKey(),
|
||||
eventType: text("event_type").notNull(),
|
||||
narrative: text("narrative").notNull(),
|
||||
agentId: text("agent_id"),
|
||||
jobId: text("job_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export type TowerLogEntry = typeof towerLog.$inferSelect;
|
||||
@@ -112,6 +112,81 @@
|
||||
color: #88ffcc;
|
||||
}
|
||||
|
||||
/* ── Tower Log toggle button ──────────────────────────────────────── */
|
||||
#open-tower-log-btn {
|
||||
font-family: 'Courier New', monospace; font-size: 11px; font-weight: bold;
|
||||
color: #ddcc88; background: #1a1500; border: 1px solid #aa9922;
|
||||
padding: 7px 18px; cursor: pointer; letter-spacing: 1px;
|
||||
box-shadow: 0 0 14px #55440022;
|
||||
transition: background 0.15s, box-shadow 0.15s, color 0.15s;
|
||||
border-radius: 2px;
|
||||
min-height: 36px;
|
||||
}
|
||||
#open-tower-log-btn:hover, #open-tower-log-btn:active {
|
||||
background: #2a2200;
|
||||
box-shadow: 0 0 20px #88660044;
|
||||
color: #ffee88;
|
||||
}
|
||||
|
||||
/* ── Tower Log panel (bottom slide-up) ────────────────────────────── */
|
||||
#tower-log-panel {
|
||||
position: fixed; bottom: -100%; left: 0; right: 0;
|
||||
height: 320px;
|
||||
background: rgba(10, 8, 2, 0.97);
|
||||
border-top: 1px solid #2a2200;
|
||||
padding: 0;
|
||||
overflow: hidden; z-index: 100;
|
||||
font-family: 'Courier New', monospace;
|
||||
transition: bottom 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 -8px 32px rgba(100, 80, 10, 0.15);
|
||||
}
|
||||
#tower-log-panel.open {
|
||||
bottom: 0;
|
||||
}
|
||||
#tower-log-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 16px 8px;
|
||||
border-bottom: 1px solid #2a2200;
|
||||
}
|
||||
#tower-log-header h2 {
|
||||
font-size: 12px; letter-spacing: 3px; color: #ccaa44;
|
||||
text-shadow: 0 0 8px #88660088;
|
||||
margin: 0;
|
||||
}
|
||||
#tower-log-close {
|
||||
background: transparent; border: 1px solid #2a2200;
|
||||
color: #665522; font-family: 'Courier New', monospace;
|
||||
font-size: 16px; width: 28px; height: 28px;
|
||||
cursor: pointer; transition: color 0.2s, border-color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#tower-log-close:hover { color: #ddcc44; border-color: #aa9922; }
|
||||
#tower-log-entries {
|
||||
height: calc(320px - 46px);
|
||||
overflow-y: auto;
|
||||
padding: 10px 16px;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
#tower-log-entries::-webkit-scrollbar { width: 4px; }
|
||||
#tower-log-entries::-webkit-scrollbar-track { background: transparent; }
|
||||
#tower-log-entries::-webkit-scrollbar-thumb { background: #2a2200; border-radius: 2px; }
|
||||
.tower-log-entry {
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid #1a1200;
|
||||
color: #998855;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.tower-log-entry:last-child { border-bottom: none; }
|
||||
.tower-log-entry .tl-time {
|
||||
font-size: 9px; color: #443300; letter-spacing: 1px; margin-bottom: 2px;
|
||||
}
|
||||
.tower-log-entry .tl-text { color: #ccaa66; }
|
||||
#tower-log-empty {
|
||||
color: #443300; font-size: 11px; text-align: center; padding: 30px 0;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ── Low balance notice ───────────────────────────────────────────── */
|
||||
#low-balance-notice {
|
||||
display: none;
|
||||
@@ -541,6 +616,7 @@
|
||||
<div id="top-buttons">
|
||||
<button id="open-panel-btn">⚡ SUBMIT JOB</button>
|
||||
<button id="open-session-btn">⚡ FUND SESSION</button>
|
||||
<button id="open-tower-log-btn">📜 TOWER LOG</button>
|
||||
<a id="relay-admin-btn" href="/admin/relay">⚙ RELAY ADMIN</a>
|
||||
</div>
|
||||
|
||||
@@ -720,5 +796,16 @@
|
||||
})();
|
||||
</script>
|
||||
<script type="module" src="./js/main.js"></script>
|
||||
|
||||
<!-- ── Tower Log panel (bottom) ───────────────────────────────────── -->
|
||||
<div id="tower-log-panel">
|
||||
<div id="tower-log-header">
|
||||
<h2>📜 TOWER LOG</h2>
|
||||
<button id="tower-log-close">✕</button>
|
||||
</div>
|
||||
<div id="tower-log-entries">
|
||||
<div id="tower-log-empty">NO ENTRIES YET…</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { initTimmyId } from './timmy-id.js';
|
||||
import { AGENT_DEFS } from './agent-defs.js';
|
||||
import { initNavigation, updateNavigation, disposeNavigation } from './navigation.js';
|
||||
import { initHudLabels, updateHudLabels, disposeHudLabels } from './hud-labels.js';
|
||||
import { initTowerLog } from './tower-log.js';
|
||||
|
||||
let running = false;
|
||||
let canvas = null;
|
||||
@@ -46,6 +47,7 @@ function buildWorld(firstInit, stateSnapshot) {
|
||||
initWebSocket(scene);
|
||||
initPaymentPanel();
|
||||
initSessionPanel();
|
||||
initTowerLog();
|
||||
void initNostrIdentity('/api');
|
||||
warmupEdgeWorker();
|
||||
onEdgeWorkerReady(() => setEdgeWorkerReady());
|
||||
|
||||
97
the-matrix/js/tower-log.js
Normal file
97
the-matrix/js/tower-log.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// Tower Log UI — slide-out narrative event feed (#7)
|
||||
|
||||
const MAX_ENTRIES = 20;
|
||||
let _entries = [];
|
||||
let _initialized = false;
|
||||
|
||||
function $panel() { return document.getElementById('tower-log-panel'); }
|
||||
function $entriesEl() { return document.getElementById('tower-log-entries'); }
|
||||
function $emptyEl() { return document.getElementById('tower-log-empty'); }
|
||||
|
||||
function _formatTime(isoOrDate) {
|
||||
const d = new Date(isoOrDate);
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function _renderEntry(entry) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'tower-log-entry';
|
||||
el.dataset.id = entry.id;
|
||||
const timeEl = document.createElement('div');
|
||||
timeEl.className = 'tl-time';
|
||||
timeEl.textContent = _formatTime(entry.createdAt);
|
||||
const textEl = document.createElement('div');
|
||||
textEl.className = 'tl-text';
|
||||
textEl.textContent = entry.narrative;
|
||||
el.appendChild(timeEl);
|
||||
el.appendChild(textEl);
|
||||
return el;
|
||||
}
|
||||
|
||||
function _refresh() {
|
||||
const container = $entriesEl();
|
||||
const empty = $emptyEl();
|
||||
if (!container) return;
|
||||
if (_entries.length === 0) {
|
||||
if (empty) empty.style.display = '';
|
||||
return;
|
||||
}
|
||||
if (empty) empty.style.display = 'none';
|
||||
// Remove entries beyond container's own DOM children (don't double-render)
|
||||
const existing = new Set(
|
||||
Array.from(container.querySelectorAll('.tower-log-entry')).map(el => el.dataset.id)
|
||||
);
|
||||
for (const entry of _entries) {
|
||||
if (!existing.has(entry.id)) {
|
||||
container.appendChild(_renderEntry(entry));
|
||||
}
|
||||
}
|
||||
// Auto-scroll to bottom
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
export function appendTowerLogEntry(entry) {
|
||||
// Deduplicate
|
||||
if (_entries.some(e => e.id === entry.id)) return;
|
||||
_entries.push(entry);
|
||||
if (_entries.length > MAX_ENTRIES) _entries.shift();
|
||||
if ($panel()?.classList.contains('open')) {
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function _fetchHistory() {
|
||||
try {
|
||||
const res = await fetch('/api/tower-log');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.entries)) {
|
||||
_entries = data.entries.slice(-MAX_ENTRIES);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function openTowerLog() {
|
||||
const panel = $panel();
|
||||
if (!panel) return;
|
||||
panel.classList.add('open');
|
||||
if (!_initialized) {
|
||||
_initialized = true;
|
||||
_fetchHistory().then(() => _refresh());
|
||||
} else {
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
export function closeTowerLog() {
|
||||
$panel()?.classList.remove('open');
|
||||
}
|
||||
|
||||
export function initTowerLog() {
|
||||
const openBtn = document.getElementById('open-tower-log-btn');
|
||||
const closeBtn = document.getElementById('tower-log-close');
|
||||
if (openBtn) openBtn.addEventListener('click', openTowerLog);
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeTowerLog);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { setAgentState, setSpeechBubble, applyAgentStates, setMood } from './age
|
||||
import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js';
|
||||
import { sentiment } from './edge-worker-client.js';
|
||||
import { setLabelState } from './hud-labels.js';
|
||||
import { appendTowerLogEntry } from './tower-log.js';
|
||||
|
||||
function resolveWsUrl() {
|
||||
const explicit = import.meta.env.VITE_WS_URL;
|
||||
@@ -151,6 +152,13 @@ function handleMessage(msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'tower_log_entry': {
|
||||
if (msg.entry) {
|
||||
appendTowerLogEntry(msg.entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agent_count':
|
||||
case 'visitor_count':
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user