@alexkroman1/aai-cli 5.1.1 → 5.3.0

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.
Files changed (99) hide show
  1. package/dist/_agent.d.ts +39 -0
  2. package/dist/_api-client.d.ts +37 -0
  3. package/dist/_bundler.d.ts +46 -0
  4. package/dist/_config.d.ts +57 -0
  5. package/dist/_default-html.d.ts +12 -0
  6. package/dist/_deploy.d.ts +28 -0
  7. package/dist/_dev-server.d.ts +74 -0
  8. package/dist/{_init-B-LEVTHx.mjs → _init-DU-sXH6S.mjs} +44 -65
  9. package/dist/_init.d.ts +7 -0
  10. package/dist/_mock-api.d.ts +22 -0
  11. package/dist/_mock-registry.d.ts +17 -0
  12. package/dist/_output.d.ts +50 -0
  13. package/dist/_server-common.d.ts +17 -0
  14. package/dist/_slug-api.d.ts +15 -0
  15. package/dist/_templates.d.ts +21 -0
  16. package/dist/_typecheck-gate.d.ts +7 -0
  17. package/dist/_ui.d.ts +22 -0
  18. package/dist/_utils.d.ts +48 -0
  19. package/dist/_vite-env.d.ts +8 -0
  20. package/dist/build.d.ts +16 -0
  21. package/dist/cli.d.ts +1 -0
  22. package/dist/cli.mjs +1 -1
  23. package/dist/client-bundler.d.ts +21 -0
  24. package/dist/delete.d.ts +18 -0
  25. package/dist/deploy.d.ts +15 -0
  26. package/dist/dev.d.ts +13 -0
  27. package/dist/{init-tc3Rbgrf.mjs → init-BjeK8crW.mjs} +1 -1
  28. package/dist/init.d.ts +24 -0
  29. package/dist/scaffold/.env.example +11 -0
  30. package/dist/scaffold/CLAUDE.md +938 -0
  31. package/dist/scaffold/global.d.ts +1 -0
  32. package/dist/scaffold/package.json +32 -0
  33. package/dist/scaffold/pnpm-workspace.yaml +13 -0
  34. package/dist/scaffold/tsconfig.json +19 -0
  35. package/dist/scaffold/vite.config.ts +14 -0
  36. package/dist/scaffold/vitest.config.ts +24 -0
  37. package/dist/secret.d.ts +25 -0
  38. package/dist/storage.d.ts +22 -0
  39. package/dist/templates/code-interpreter/agent.ts +11 -0
  40. package/dist/templates/code-interpreter/system-prompt.md +18 -0
  41. package/dist/templates/dispatch-center/agent.test.ts +153 -0
  42. package/dist/templates/dispatch-center/agent.ts +50 -0
  43. package/dist/templates/dispatch-center/client.tsx +378 -0
  44. package/dist/templates/dispatch-center/shared.ts +631 -0
  45. package/dist/templates/dispatch-center/system-prompt.md +39 -0
  46. package/dist/templates/dispatch-center/tools/incident_add_note.ts +27 -0
  47. package/dist/templates/dispatch-center/tools/incident_create.ts +79 -0
  48. package/dist/templates/dispatch-center/tools/incident_escalate.ts +88 -0
  49. package/dist/templates/dispatch-center/tools/incident_get.ts +36 -0
  50. package/dist/templates/dispatch-center/tools/incident_triage.ts +78 -0
  51. package/dist/templates/dispatch-center/tools/incident_update_status.ts +68 -0
  52. package/dist/templates/dispatch-center/tools/ops_dashboard.ts +52 -0
  53. package/dist/templates/dispatch-center/tools/ops_protocols.ts +27 -0
  54. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +149 -0
  55. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +97 -0
  56. package/dist/templates/dispatch-center/tools/resources_get_available.ts +36 -0
  57. package/dist/templates/dispatch-center/tools/resources_update_status.ts +62 -0
  58. package/dist/templates/embedded-assets/agent.ts +55 -0
  59. package/dist/templates/embedded-assets/knowledge.json +20 -0
  60. package/dist/templates/health-assistant/agent.ts +174 -0
  61. package/dist/templates/health-assistant/system-prompt.md +17 -0
  62. package/dist/templates/infocom-adventure/agent.ts +119 -0
  63. package/dist/templates/infocom-adventure/client.tsx +279 -0
  64. package/dist/templates/infocom-adventure/shared.ts +38 -0
  65. package/dist/templates/infocom-adventure/system-prompt.md +43 -0
  66. package/dist/templates/math-buddy/agent.ts +16 -0
  67. package/dist/templates/math-buddy/system-prompt.md +12 -0
  68. package/dist/templates/night-owl/agent.ts +76 -0
  69. package/dist/templates/night-owl/client.tsx +130 -0
  70. package/dist/templates/night-owl/shared.ts +5 -0
  71. package/dist/templates/personal-finance/agent.ts +11 -0
  72. package/dist/templates/personal-finance/system-prompt.md +17 -0
  73. package/dist/templates/pipeline-simple/agent.test.ts +32 -0
  74. package/dist/templates/pipeline-simple/agent.ts +13 -0
  75. package/dist/templates/pizza-ordering/agent.test.ts +235 -0
  76. package/dist/templates/pizza-ordering/agent.ts +174 -0
  77. package/dist/templates/pizza-ordering/client.tsx +120 -0
  78. package/dist/templates/pizza-ordering/shared.ts +142 -0
  79. package/dist/templates/pizza-ordering/system-prompt.md +17 -0
  80. package/dist/templates/simple/agent.ts +5 -0
  81. package/dist/templates/solo-rpg/agent.test.ts +477 -0
  82. package/dist/templates/solo-rpg/agent.ts +37 -0
  83. package/dist/templates/solo-rpg/client.tsx +809 -0
  84. package/dist/templates/solo-rpg/shared.ts +747 -0
  85. package/dist/templates/solo-rpg/system-prompt.md +115 -0
  86. package/dist/templates/solo-rpg/tools/action_roll.ts +98 -0
  87. package/dist/templates/solo-rpg/tools/burn_momentum.ts +69 -0
  88. package/dist/templates/solo-rpg/tools/check_state.ts +11 -0
  89. package/dist/templates/solo-rpg/tools/load_game.ts +25 -0
  90. package/dist/templates/solo-rpg/tools/oracle.ts +248 -0
  91. package/dist/templates/solo-rpg/tools/save_game.ts +20 -0
  92. package/dist/templates/solo-rpg/tools/setup_character.ts +208 -0
  93. package/dist/templates/solo-rpg/tools/update_state.ts +170 -0
  94. package/dist/templates/web-researcher/agent.ts +11 -0
  95. package/dist/test.d.ts +39 -0
  96. package/dist/tsdown.config.d.ts +2 -0
  97. package/dist/typecheck.d.ts +26 -0
  98. package/dist/worker-bundler.d.ts +29 -0
  99. package/package.json +9 -6
@@ -0,0 +1,809 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import { ChatView, client, SidebarLayout, StartScreen, useAgentState } from "@alexkroman1/aai-ui";
3
+ import type { ReactNode } from "react";
4
+ import {
5
+ type Clock,
6
+ DEFAULT_STATE,
7
+ type Disposition,
8
+ type GameState,
9
+ GENRES,
10
+ MAX_RESOURCE,
11
+ MIN_MOMENTUM,
12
+ type NPC,
13
+ type StoryBlueprint,
14
+ type TIME_PHASES,
15
+ } from "./shared.ts";
16
+
17
+ // ── Color Palette ────────────────────────────────────────────────────────────
18
+ const C = {
19
+ bg: "#0a0a0c",
20
+ surface: "#0f0f12",
21
+ surfaceLight: "#16161b",
22
+ border: "#1e1e26",
23
+ borderLight: "#2a2a36",
24
+ accent: "#c9a84c",
25
+ accentDim: "#8a7232",
26
+ accentGlow: "rgba(201,168,76,0.15)",
27
+ text: "#e0dcd0",
28
+ textMuted: "rgba(224,220,208,0.5)",
29
+ textDim: "rgba(224,220,208,0.25)",
30
+ health: "#8b3030",
31
+ healthBright: "#c44040",
32
+ spirit: "#3a5a8a",
33
+ spiritBright: "#5a8acd",
34
+ supply: "#4a6a3a",
35
+ supplyBright: "#6a9a4a",
36
+ chaos: { low: "#3a6a3a", mid: "#8a7a3a", high: "#8a4a2a", critical: "#8b2020" },
37
+ disposition: {
38
+ hostile: "#c44040",
39
+ distrustful: "#c47a30",
40
+ neutral: "#888888",
41
+ friendly: "#4a9a4a",
42
+ loyal: "#c9a84c",
43
+ },
44
+ threat: "#8b2020",
45
+ progress: "#3a7a9a",
46
+ scheme: "#7a4a8a",
47
+ };
48
+
49
+ const DISP_ICON: Record<Disposition, string> = {
50
+ hostile: "\u2620",
51
+ distrustful: "\u26A0",
52
+ neutral: "\u25CB",
53
+ friendly: "\u2665",
54
+ loyal: "\u2726",
55
+ };
56
+
57
+ // `satisfies` pins this map to TIME_PHASES, so a renamed or added phase is a
58
+ // compile error here instead of a silently unlabeled time of day.
59
+ const TIME_LABELS: Record<string, string> = {
60
+ early_morning: "Dawn",
61
+ morning: "Morning",
62
+ midday: "Midday",
63
+ afternoon: "Afternoon",
64
+ evening: "Dusk",
65
+ late_evening: "Twilight",
66
+ night: "Night",
67
+ deep_night: "Witching Hour",
68
+ } satisfies Record<(typeof TIME_PHASES)[number], string>;
69
+
70
+ const PHASE_LABELS: Record<string, string> = {
71
+ setup: "Act I",
72
+ confrontation: "Act II",
73
+ climax: "Act III",
74
+ ki_introduction: "Ki",
75
+ sho_development: "Sho",
76
+ ten_twist: "Ten",
77
+ ketsu_resolution: "Ketsu",
78
+ };
79
+
80
+ // ── Components ───────────────────────────────────────────────────────────────
81
+
82
+ /** Section header row: dim label on the left, a value element on the right. */
83
+ function SectionHeader({ label, right }: { label: string; right: ReactNode }) {
84
+ return (
85
+ <div
86
+ style={{
87
+ display: "flex",
88
+ justifyContent: "space-between",
89
+ alignItems: "center",
90
+ marginBottom: "3px",
91
+ }}
92
+ >
93
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>{label}</span>
94
+ {right}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ function ResourceBar({
100
+ label,
101
+ current,
102
+ max,
103
+ color,
104
+ colorBright,
105
+ icon,
106
+ }: {
107
+ label: string;
108
+ current: number;
109
+ max: number;
110
+ color: string;
111
+ colorBright: string;
112
+ icon: string;
113
+ }) {
114
+ const pips: ReactNode[] = [];
115
+ for (let i = 0; i < max; i++) {
116
+ const filled = i < current;
117
+ pips.push(
118
+ <div
119
+ key={i}
120
+ style={{
121
+ width: "18px",
122
+ height: "10px",
123
+ borderRadius: "2px",
124
+ background: filled
125
+ ? `linear-gradient(135deg, ${color}, ${colorBright})`
126
+ : "rgba(255,255,255,0.03)",
127
+ border: `1px solid ${filled ? colorBright : "rgba(255,255,255,0.06)"}`,
128
+ boxShadow: filled ? `0 0 4px ${color}66` : "none",
129
+ transition: "all 0.4s ease",
130
+ }}
131
+ />,
132
+ );
133
+ }
134
+ return (
135
+ <div style={{ marginBottom: "8px" }}>
136
+ <SectionHeader
137
+ label={`${icon} ${label}`}
138
+ right={
139
+ <span
140
+ style={{
141
+ fontSize: "11px",
142
+ fontWeight: 700,
143
+ color: current > 0 ? colorBright : C.threat,
144
+ }}
145
+ >
146
+ {current}
147
+ </span>
148
+ }
149
+ />
150
+ <div style={{ display: "flex", gap: "2px" }}>{pips}</div>
151
+ </div>
152
+ );
153
+ }
154
+
155
+ function MomentumTrack({ momentum, max }: { momentum: number; max: number }) {
156
+ const range: number[] = [];
157
+ for (let i = MIN_MOMENTUM; i <= 10; i++) range.push(i);
158
+ return (
159
+ <div style={{ marginBottom: "8px" }}>
160
+ <SectionHeader
161
+ label="Momentum"
162
+ right={
163
+ <span
164
+ style={{
165
+ fontSize: "11px",
166
+ fontWeight: 700,
167
+ color: momentum > 0 ? C.spiritBright : momentum < 0 ? C.healthBright : C.textMuted,
168
+ }}
169
+ >
170
+ {momentum > 0 ? "+" : ""}
171
+ {momentum}
172
+ </span>
173
+ }
174
+ />
175
+ <div style={{ display: "flex", gap: "1px" }}>
176
+ {range.map((v) => (
177
+ <div
178
+ key={v}
179
+ style={{
180
+ flex: 1,
181
+ height: "6px",
182
+ borderRadius: "1px",
183
+ background:
184
+ v > max
185
+ ? "rgba(255,255,255,0.01)"
186
+ : v <= momentum && v > 0
187
+ ? C.spiritBright
188
+ : v >= momentum && v < 0
189
+ ? C.healthBright
190
+ : v === 0
191
+ ? "rgba(255,255,255,0.12)"
192
+ : "rgba(255,255,255,0.03)",
193
+ boxShadow:
194
+ v <= momentum && v > 0
195
+ ? `0 0 3px ${C.spirit}`
196
+ : v >= momentum && v < 0
197
+ ? `0 0 3px ${C.health}`
198
+ : "none",
199
+ transition: "all 0.3s ease",
200
+ }}
201
+ />
202
+ ))}
203
+ </div>
204
+ <div style={{ display: "flex", justifyContent: "space-between", marginTop: "1px" }}>
205
+ <span style={{ fontSize: "7px", color: C.textDim }}>-6</span>
206
+ <span style={{ fontSize: "7px", color: C.textDim }}>0</span>
207
+ <span style={{ fontSize: "7px", color: C.textDim }}>+10</span>
208
+ </div>
209
+ </div>
210
+ );
211
+ }
212
+
213
+ function ChaosGauge({ chaos }: { chaos: number }) {
214
+ const pct = ((chaos - 3) / 6) * 100;
215
+ const color =
216
+ chaos <= 4
217
+ ? C.chaos.low
218
+ : chaos <= 6
219
+ ? C.chaos.mid
220
+ : chaos <= 8
221
+ ? C.chaos.high
222
+ : C.chaos.critical;
223
+ const label = chaos <= 4 ? "Calm" : chaos <= 6 ? "Tense" : chaos <= 8 ? "Volatile" : "Critical";
224
+ return (
225
+ <div style={{ marginBottom: "8px" }}>
226
+ <SectionHeader
227
+ label="Chaos"
228
+ right={
229
+ <span
230
+ style={{
231
+ fontSize: "9px",
232
+ fontWeight: 600,
233
+ color,
234
+ textTransform: "uppercase",
235
+ letterSpacing: "0.08em",
236
+ }}
237
+ >
238
+ {label} ({chaos})
239
+ </span>
240
+ }
241
+ />
242
+ <div
243
+ style={{
244
+ height: "4px",
245
+ borderRadius: "2px",
246
+ background: "rgba(255,255,255,0.04)",
247
+ overflow: "hidden",
248
+ }}
249
+ >
250
+ <div
251
+ style={{
252
+ height: "100%",
253
+ width: `${pct}%`,
254
+ borderRadius: "2px",
255
+ background: `linear-gradient(90deg, ${C.chaos.low}, ${color})`,
256
+ boxShadow: `0 0 6px ${color}66`,
257
+ transition: "width 0.5s ease, background 0.5s ease",
258
+ }}
259
+ />
260
+ </div>
261
+ </div>
262
+ );
263
+ }
264
+
265
+ function StatPip({ label, value }: { label: string; value: number }) {
266
+ return (
267
+ <div style={{ textAlign: "center" }}>
268
+ <div
269
+ style={{
270
+ fontSize: "8px",
271
+ color: C.textDim,
272
+ textTransform: "uppercase",
273
+ letterSpacing: "0.1em",
274
+ }}
275
+ >
276
+ {label}
277
+ </div>
278
+ <div
279
+ style={{
280
+ fontSize: "18px",
281
+ fontWeight: 700,
282
+ color: C.accent,
283
+ lineHeight: 1,
284
+ textShadow: `0 0 8px ${C.accentGlow}`,
285
+ }}
286
+ >
287
+ {value}
288
+ </div>
289
+ </div>
290
+ );
291
+ }
292
+
293
+ function ClockDisplay({ clock }: { clock: Clock }) {
294
+ const typeColor =
295
+ clock.clockType === "threat"
296
+ ? C.threat
297
+ : clock.clockType === "progress"
298
+ ? C.progress
299
+ : C.scheme;
300
+ const segments: ReactNode[] = [];
301
+ for (let i = 0; i < clock.segments; i++) {
302
+ segments.push(
303
+ <div
304
+ key={i}
305
+ style={{
306
+ width: "10px",
307
+ height: "10px",
308
+ borderRadius: "50%",
309
+ background: i < clock.filled ? typeColor : "rgba(255,255,255,0.04)",
310
+ border: `1px solid ${i < clock.filled ? typeColor : "rgba(255,255,255,0.08)"}`,
311
+ boxShadow: i < clock.filled ? `0 0 4px ${typeColor}66` : "none",
312
+ transition: "all 0.3s ease",
313
+ }}
314
+ />,
315
+ );
316
+ }
317
+ const isFull = clock.filled >= clock.segments;
318
+ return (
319
+ <div
320
+ style={{
321
+ marginBottom: "8px",
322
+ padding: "6px 8px",
323
+ borderRadius: "4px",
324
+ background: "rgba(255,255,255,0.015)",
325
+ border: `1px solid ${isFull ? typeColor : "rgba(255,255,255,0.04)"}`,
326
+ opacity: isFull ? 0.5 : 1,
327
+ }}
328
+ >
329
+ <div
330
+ style={{
331
+ display: "flex",
332
+ justifyContent: "space-between",
333
+ alignItems: "center",
334
+ marginBottom: "4px",
335
+ }}
336
+ >
337
+ <span style={{ fontSize: "10px", fontWeight: 600, color: C.text }}>{clock.name}</span>
338
+ <span
339
+ style={{
340
+ fontSize: "8px",
341
+ padding: "1px 4px",
342
+ borderRadius: "2px",
343
+ background: `${typeColor}22`,
344
+ color: typeColor,
345
+ textTransform: "uppercase",
346
+ letterSpacing: "0.06em",
347
+ border: `1px solid ${typeColor}44`,
348
+ }}
349
+ >
350
+ {clock.clockType}
351
+ </span>
352
+ </div>
353
+ <div style={{ display: "flex", gap: "3px", flexWrap: "wrap" }}>{segments}</div>
354
+ </div>
355
+ );
356
+ }
357
+
358
+ function NpcCard({ npc }: { npc: NPC }) {
359
+ const dispColor = C.disposition[npc.disposition] || C.textMuted;
360
+ const icon = DISP_ICON[npc.disposition] || "\u25CB";
361
+ return (
362
+ <div
363
+ style={{
364
+ marginBottom: "6px",
365
+ padding: "6px 8px",
366
+ borderRadius: "4px",
367
+ background: "rgba(255,255,255,0.015)",
368
+ borderLeft: `2px solid ${dispColor}`,
369
+ }}
370
+ >
371
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
372
+ <span style={{ fontSize: "11px", fontWeight: 600, color: C.text }}>{npc.name}</span>
373
+ <span style={{ fontSize: "10px", color: dispColor }}>{icon}</span>
374
+ </div>
375
+ {npc.agenda && (
376
+ <div style={{ fontSize: "9px", color: C.textMuted, marginTop: "2px", fontStyle: "italic" }}>
377
+ {npc.agenda}
378
+ </div>
379
+ )}
380
+ <div style={{ display: "flex", gap: "8px", marginTop: "3px" }}>
381
+ <span style={{ fontSize: "8px", color: C.textDim }}>{npc.disposition}</span>
382
+ {npc.bond !== 0 && (
383
+ <span style={{ fontSize: "8px", color: npc.bond > 0 ? C.supplyBright : C.healthBright }}>
384
+ bond {npc.bond > 0 ? "+" : ""}
385
+ {npc.bond}
386
+ </span>
387
+ )}
388
+ </div>
389
+ </div>
390
+ );
391
+ }
392
+
393
+ function StoryArc({ story }: { story: StoryBlueprint }) {
394
+ const totalActs = story.acts.length;
395
+ const currentPhase = story.acts[story.currentAct - 1]?.phase ?? "";
396
+ const pct = totalActs > 0 ? ((story.currentAct - 1) / totalActs) * 100 : 0;
397
+ const phaseLabel = PHASE_LABELS[currentPhase] || currentPhase;
398
+ return (
399
+ <div style={{ marginBottom: "8px" }}>
400
+ <SectionHeader
401
+ label="Story Arc"
402
+ right={
403
+ <span style={{ fontSize: "9px", color: C.accent }}>
404
+ {phaseLabel} ({story.currentAct}/{totalActs})
405
+ </span>
406
+ }
407
+ />
408
+ <div
409
+ style={{
410
+ height: "3px",
411
+ borderRadius: "2px",
412
+ background: "rgba(255,255,255,0.04)",
413
+ overflow: "hidden",
414
+ }}
415
+ >
416
+ <div
417
+ style={{
418
+ height: "100%",
419
+ width: `${pct}%`,
420
+ borderRadius: "2px",
421
+ background: `linear-gradient(90deg, ${C.accentDim}, ${C.accent})`,
422
+ transition: "width 0.5s ease",
423
+ }}
424
+ />
425
+ </div>
426
+ {story.storyComplete && (
427
+ <div
428
+ style={{
429
+ fontSize: "8px",
430
+ color: C.accent,
431
+ textTransform: "uppercase",
432
+ letterSpacing: "0.1em",
433
+ marginTop: "3px",
434
+ textAlign: "center",
435
+ }}
436
+ >
437
+ Story Complete
438
+ </div>
439
+ )}
440
+ </div>
441
+ );
442
+ }
443
+
444
+ // ── Sidebar ──────────────────────────────────────────────────────────────────
445
+
446
+ function Sidebar({ game }: { game: GameState }) {
447
+ return (
448
+ <div
449
+ style={{
450
+ height: "100%",
451
+ overflowY: "auto",
452
+ background: C.bg,
453
+ fontFamily: "'Crimson Text', 'Georgia', serif",
454
+ }}
455
+ >
456
+ <style>{`
457
+ @import url('https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap');
458
+ .et-section { border-bottom: 1px solid ${C.border}; padding: 10px 12px; }
459
+ .et-section:last-child { border-bottom: none; }
460
+ .et-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.14em; color: ${C.textDim}; margin-bottom: 6px; font-family: sans-serif; }
461
+ .et-gold { color: ${C.accent}; text-shadow: 0 0 8px ${C.accentGlow}; }
462
+ .et-crisis-pulse { animation: crisisPulse 2s ease-in-out infinite; }
463
+ @keyframes crisisPulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
464
+ .et-scroll::-webkit-scrollbar { width: 3px; }
465
+ .et-scroll::-webkit-scrollbar-thumb { background: ${C.borderLight}; border-radius: 3px; }
466
+ .et-scroll::-webkit-scrollbar-track { background: transparent; }
467
+ `}</style>
468
+
469
+ {/* Header */}
470
+ <div
471
+ className="et-section"
472
+ style={{ textAlign: "center", paddingTop: "16px", paddingBottom: "12px" }}
473
+ >
474
+ <div
475
+ style={{
476
+ fontSize: "8px",
477
+ letterSpacing: "0.3em",
478
+ color: C.textDim,
479
+ textTransform: "uppercase",
480
+ fontFamily: "sans-serif",
481
+ }}
482
+ >
483
+ Solo RPG
484
+ </div>
485
+ {game.initialized ? (
486
+ <>
487
+ <div
488
+ className="et-gold"
489
+ style={{ fontSize: "16px", fontWeight: 700, marginTop: "4px" }}
490
+ >
491
+ {game.playerName}
492
+ </div>
493
+ {game.characterConcept && (
494
+ <div
495
+ style={{
496
+ fontSize: "11px",
497
+ color: C.textMuted,
498
+ fontStyle: "italic",
499
+ marginTop: "2px",
500
+ }}
501
+ >
502
+ {game.characterConcept}
503
+ </div>
504
+ )}
505
+ {game.settingGenre && (
506
+ <div
507
+ style={{
508
+ fontSize: "8px",
509
+ color: C.accentDim,
510
+ marginTop: "4px",
511
+ textTransform: "uppercase",
512
+ letterSpacing: "0.1em",
513
+ fontFamily: "sans-serif",
514
+ }}
515
+ >
516
+ {GENRES[game.settingGenre as keyof typeof GENRES] || game.settingGenre}
517
+ </div>
518
+ )}
519
+ </>
520
+ ) : (
521
+ <div
522
+ style={{ fontSize: "11px", color: C.textDim, marginTop: "8px", fontStyle: "italic" }}
523
+ >
524
+ Creating your story...
525
+ </div>
526
+ )}
527
+ </div>
528
+
529
+ {/* Pre-init placeholder */}
530
+ {!game.initialized && (
531
+ <div className="et-section" style={{ textAlign: "center", padding: "32px 12px" }}>
532
+ {/* biome-ignore lint/style/useConsistentCurlyBraces: unicode escape */}
533
+ <div style={{ fontSize: "36px", opacity: 0.08 }}>{"\u2726"}</div>
534
+ <div
535
+ style={{
536
+ fontSize: "10px",
537
+ color: C.textDim,
538
+ marginTop: "10px",
539
+ lineHeight: 1.7,
540
+ fontStyle: "italic",
541
+ }}
542
+ >
543
+ Choose your world.
544
+ <br />
545
+ Shape your character.
546
+ <br />
547
+ Begin your tale.
548
+ </div>
549
+ </div>
550
+ )}
551
+
552
+ {game.initialized && (
553
+ <>
554
+ {/* Crisis / Game Over Banner */}
555
+ {(game.crisisMode || game.gameOver) && (
556
+ <div
557
+ className="et-section et-crisis-pulse"
558
+ style={{
559
+ textAlign: "center",
560
+ padding: "8px 12px",
561
+ background: `${C.threat}15`,
562
+ borderBottom: `1px solid ${C.threat}33`,
563
+ }}
564
+ >
565
+ <div
566
+ style={{
567
+ fontSize: "10px",
568
+ fontWeight: 700,
569
+ color: C.healthBright,
570
+ textTransform: "uppercase",
571
+ letterSpacing: "0.15em",
572
+ fontFamily: "sans-serif",
573
+ }}
574
+ >
575
+ {game.kidMode ? "In Trouble" : game.gameOver ? "Finale" : "Crisis"}
576
+ </div>
577
+ </div>
578
+ )}
579
+
580
+ {/* Stats */}
581
+ <div className="et-section">
582
+ <div className="et-label">Attributes</div>
583
+ <div style={{ display: "flex", justifyContent: "space-between", padding: "0 4px" }}>
584
+ <StatPip label="Edge" value={game.edge} />
585
+ <StatPip label="Heart" value={game.heart} />
586
+ <StatPip label="Iron" value={game.iron} />
587
+ <StatPip label="Shadow" value={game.shadow} />
588
+ <StatPip label="Wits" value={game.wits} />
589
+ </div>
590
+ </div>
591
+
592
+ {/* Resources */}
593
+ <div className="et-section">
594
+ <ResourceBar
595
+ label="Health"
596
+ current={game.health}
597
+ max={MAX_RESOURCE}
598
+ color={C.health}
599
+ colorBright={C.healthBright}
600
+ icon="\u2665"
601
+ />
602
+ <ResourceBar
603
+ label="Spirit"
604
+ current={game.spirit}
605
+ max={MAX_RESOURCE}
606
+ color={C.spirit}
607
+ colorBright={C.spiritBright}
608
+ icon="\u25C6"
609
+ />
610
+ <ResourceBar
611
+ label="Supply"
612
+ current={game.supply}
613
+ max={MAX_RESOURCE}
614
+ color={C.supply}
615
+ colorBright={C.supplyBright}
616
+ icon="\u25A0"
617
+ />
618
+ <MomentumTrack momentum={game.momentum} max={game.maxMomentum} />
619
+ <ChaosGauge chaos={game.chaosFactor} />
620
+ </div>
621
+
622
+ {/* Location & Time */}
623
+ <div className="et-section">
624
+ <div className="et-label">Location</div>
625
+ <div className="et-gold" style={{ fontSize: "13px", fontWeight: 600 }}>
626
+ {game.currentLocation || "Unknown"}
627
+ </div>
628
+ {game.currentSceneContext && (
629
+ <div
630
+ style={{
631
+ fontSize: "10px",
632
+ color: C.textMuted,
633
+ marginTop: "3px",
634
+ fontStyle: "italic",
635
+ lineHeight: 1.4,
636
+ }}
637
+ >
638
+ {game.currentSceneContext}
639
+ </div>
640
+ )}
641
+ {game.timeOfDay && (
642
+ <div
643
+ style={{
644
+ fontSize: "9px",
645
+ color: C.accentDim,
646
+ marginTop: "4px",
647
+ textTransform: "uppercase",
648
+ letterSpacing: "0.08em",
649
+ fontFamily: "sans-serif",
650
+ }}
651
+ >
652
+ {TIME_LABELS[game.timeOfDay] || game.timeOfDay}
653
+ </div>
654
+ )}
655
+ </div>
656
+
657
+ {/* Story Arc */}
658
+ {game.storyBlueprint && (
659
+ <div className="et-section">
660
+ <StoryArc story={game.storyBlueprint} />
661
+ </div>
662
+ )}
663
+
664
+ {/* Clocks */}
665
+ {game.clocks.length > 0 && (
666
+ <div className="et-section">
667
+ <div className="et-label">Clocks</div>
668
+ {game.clocks.map((clock) => (
669
+ <ClockDisplay key={clock.id || clock.name} clock={clock} />
670
+ ))}
671
+ </div>
672
+ )}
673
+
674
+ {/* NPCs */}
675
+ {(() => {
676
+ const active = game.npcs.filter((n) => n.status === "active");
677
+ const background = game.npcs.filter((n) => n.status === "background");
678
+ if (active.length === 0 && background.length === 0) return null;
679
+ return (
680
+ <div className="et-section">
681
+ <div className="et-label">Characters</div>
682
+ {active.map((npc) => (
683
+ <NpcCard key={npc.id} npc={npc} />
684
+ ))}
685
+ {background.length > 0 && (
686
+ <>
687
+ <div
688
+ style={{
689
+ fontSize: "8px",
690
+ color: C.textDim,
691
+ textTransform: "uppercase",
692
+ letterSpacing: "0.1em",
693
+ margin: "6px 0 4px",
694
+ fontFamily: "sans-serif",
695
+ }}
696
+ >
697
+ Known
698
+ </div>
699
+ {background.map((npc) => (
700
+ <div
701
+ key={npc.id}
702
+ style={{
703
+ fontSize: "10px",
704
+ color: C.textMuted,
705
+ marginBottom: "2px",
706
+ paddingLeft: "8px",
707
+ borderLeft: `1px solid ${C.border}`,
708
+ }}
709
+ >
710
+ {npc.name}
711
+ <span style={{ fontSize: "8px", color: C.textDim, marginLeft: "4px" }}>
712
+ {npc.disposition}
713
+ </span>
714
+ </div>
715
+ ))}
716
+ </>
717
+ )}
718
+ </div>
719
+ );
720
+ })()}
721
+
722
+ {/* Session Log */}
723
+ {game.sessionLog.length > 0 && (
724
+ <div className="et-section">
725
+ <div className="et-label">Chronicle</div>
726
+ {game.sessionLog.slice(-5).map((entry, i) => (
727
+ <div
728
+ key={i}
729
+ style={{
730
+ fontSize: "10px",
731
+ color: C.textDim,
732
+ fontStyle: "italic",
733
+ lineHeight: 1.5,
734
+ marginBottom: "4px",
735
+ paddingLeft: "8px",
736
+ borderLeft: `1px solid ${C.border}`,
737
+ }}
738
+ >
739
+ <span style={{ color: C.textMuted, fontStyle: "normal", fontSize: "8px" }}>
740
+ {entry.scene}.{" "}
741
+ </span>
742
+ {entry.summary}
743
+ </div>
744
+ ))}
745
+ </div>
746
+ )}
747
+
748
+ {/* Scene Counter */}
749
+ <div className="et-section" style={{ textAlign: "center", padding: "8px 12px" }}>
750
+ <span
751
+ style={{
752
+ fontSize: "8px",
753
+ color: C.textDim,
754
+ letterSpacing: "0.15em",
755
+ fontFamily: "sans-serif",
756
+ }}
757
+ >
758
+ {game.kidMode ? "\u2726 " : ""}SCENE {game.sceneCount}
759
+ {game.kidMode ? " \u2726" : ""}
760
+ </span>
761
+ </div>
762
+ </>
763
+ )}
764
+ </div>
765
+ );
766
+ }
767
+
768
+ // ── App ──────────────────────────────────────────────────────────────────────
769
+
770
+ function SoloRPGApp() {
771
+ // The agent's own session state, projected by `syncState` and pushed after
772
+ // every tool call — no per-tool `ctx.send`, and nothing to keep in step
773
+ // when a new tool starts mutating the game.
774
+ const game = useAgentState<GameState>() ?? DEFAULT_STATE;
775
+
776
+ return (
777
+ <StartScreen
778
+ icon={
779
+ <span style={{ fontSize: "28px", color: C.accent }}>
780
+ {/* biome-ignore lint/style/useConsistentCurlyBraces: unicode escape */}
781
+ {"\u2726"}
782
+ </span>
783
+ }
784
+ title="Solo RPG"
785
+ subtitle="A Narrative Solo-RPG Engine"
786
+ buttonText="Begin Your Story"
787
+ >
788
+ <SidebarLayout sidebar={<Sidebar game={game} />} sidebarWidth="260px" sidebarPosition="right">
789
+ <ChatView />
790
+ </SidebarLayout>
791
+ </StartScreen>
792
+ );
793
+ }
794
+
795
+ const SESSION_KEY = "solo-rpg:sessionId";
796
+ const savedSessionId = localStorage.getItem(SESSION_KEY);
797
+
798
+ client({
799
+ component: SoloRPGApp,
800
+ theme: {
801
+ bg: C.bg,
802
+ primary: C.accent,
803
+ text: C.text,
804
+ surface: C.surface,
805
+ border: C.border,
806
+ },
807
+ onSessionId: (id: string) => localStorage.setItem(SESSION_KEY, id),
808
+ ...(savedSessionId ? { resumeSessionId: savedSessionId } : {}),
809
+ });