@alexkroman1/aai 0.8.2 → 0.8.4

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 (166) hide show
  1. package/dist/cli/tsconfig.tsbuildinfo +1 -0
  2. package/dist/cli.js +1368 -1904
  3. package/dist/sdk/_mock_ws.js +2 -2
  4. package/dist/sdk/_mock_ws.js.map +1 -1
  5. package/dist/sdk/_render_check.d.ts.map +1 -1
  6. package/dist/sdk/_render_check.js +30 -0
  7. package/dist/sdk/_render_check.js.map +1 -1
  8. package/dist/sdk/_utils.d.ts +4 -0
  9. package/dist/sdk/_utils.d.ts.map +1 -0
  10. package/dist/sdk/_utils.js +7 -0
  11. package/dist/sdk/_utils.js.map +1 -0
  12. package/dist/sdk/builtin_tools.d.ts +35 -11
  13. package/dist/sdk/builtin_tools.d.ts.map +1 -1
  14. package/dist/sdk/builtin_tools.js +118 -76
  15. package/dist/sdk/builtin_tools.js.map +1 -1
  16. package/dist/sdk/capnweb.d.ts +76 -47
  17. package/dist/sdk/capnweb.d.ts.map +1 -1
  18. package/dist/sdk/capnweb.js +99 -242
  19. package/dist/sdk/capnweb.js.map +1 -1
  20. package/dist/sdk/direct_executor.d.ts.map +1 -1
  21. package/dist/sdk/direct_executor.js +0 -2
  22. package/dist/sdk/direct_executor.js.map +1 -1
  23. package/dist/sdk/host.d.ts +59 -0
  24. package/dist/sdk/host.d.ts.map +1 -0
  25. package/dist/sdk/host.js +131 -0
  26. package/dist/sdk/host.js.map +1 -0
  27. package/dist/sdk/mod.d.ts +2 -4
  28. package/dist/sdk/mod.d.ts.map +1 -1
  29. package/dist/sdk/mod.js +2 -3
  30. package/dist/sdk/mod.js.map +1 -1
  31. package/dist/sdk/protocol.d.ts +33 -135
  32. package/dist/sdk/protocol.d.ts.map +1 -1
  33. package/dist/sdk/protocol.js +49 -51
  34. package/dist/sdk/protocol.js.map +1 -1
  35. package/dist/sdk/runtime.d.ts +0 -1
  36. package/dist/sdk/runtime.d.ts.map +1 -1
  37. package/dist/sdk/runtime.js +5 -24
  38. package/dist/sdk/runtime.js.map +1 -1
  39. package/dist/sdk/s2s.d.ts +14 -3
  40. package/dist/sdk/s2s.d.ts.map +1 -1
  41. package/dist/sdk/s2s.js +72 -113
  42. package/dist/sdk/s2s.js.map +1 -1
  43. package/dist/sdk/server.d.ts +1 -1
  44. package/dist/sdk/server.d.ts.map +1 -1
  45. package/dist/sdk/server.js +51 -92
  46. package/dist/sdk/server.js.map +1 -1
  47. package/dist/sdk/session.d.ts +5 -1
  48. package/dist/sdk/session.d.ts.map +1 -1
  49. package/dist/sdk/session.js +131 -137
  50. package/dist/sdk/session.js.map +1 -1
  51. package/dist/sdk/tsconfig.tsbuildinfo +1 -0
  52. package/dist/sdk/types.d.ts +30 -3
  53. package/dist/sdk/types.d.ts.map +1 -1
  54. package/dist/sdk/types.js +37 -0
  55. package/dist/sdk/types.js.map +1 -1
  56. package/dist/sdk/winterc_server.d.ts.map +1 -1
  57. package/dist/sdk/winterc_server.js +10 -15
  58. package/dist/sdk/winterc_server.js.map +1 -1
  59. package/dist/sdk/worker_entry.d.ts +3 -11
  60. package/dist/sdk/worker_entry.d.ts.map +1 -1
  61. package/dist/sdk/worker_entry.js +8 -18
  62. package/dist/sdk/worker_entry.js.map +1 -1
  63. package/dist/sdk/worker_shim.d.ts +5 -6
  64. package/dist/sdk/worker_shim.d.ts.map +1 -1
  65. package/dist/sdk/worker_shim.js +93 -136
  66. package/dist/sdk/worker_shim.js.map +1 -1
  67. package/dist/sdk/ws_handler.d.ts +1 -1
  68. package/dist/sdk/ws_handler.d.ts.map +1 -1
  69. package/dist/sdk/ws_handler.js +13 -22
  70. package/dist/sdk/ws_handler.js.map +1 -1
  71. package/dist/ui/_cn.d.ts +5 -0
  72. package/dist/ui/_cn.d.ts.map +1 -0
  73. package/dist/ui/_cn.js +22 -0
  74. package/dist/ui/_cn.js.map +1 -0
  75. package/dist/ui/_components/app.d.ts +3 -1
  76. package/dist/ui/_components/app.d.ts.map +1 -1
  77. package/dist/ui/_components/app.js +2 -2
  78. package/dist/ui/_components/app.js.map +1 -1
  79. package/dist/ui/_components/button.d.ts +11 -0
  80. package/dist/ui/_components/button.d.ts.map +1 -0
  81. package/dist/ui/_components/button.js +17 -0
  82. package/dist/ui/_components/button.js.map +1 -0
  83. package/dist/ui/_components/chat_view.d.ts +3 -1
  84. package/dist/ui/_components/chat_view.d.ts.map +1 -1
  85. package/dist/ui/_components/chat_view.js +4 -2
  86. package/dist/ui/_components/chat_view.js.map +1 -1
  87. package/dist/ui/_components/controls.d.ts +3 -1
  88. package/dist/ui/_components/controls.d.ts.map +1 -1
  89. package/dist/ui/_components/controls.js +4 -5
  90. package/dist/ui/_components/controls.js.map +1 -1
  91. package/dist/ui/_components/error_banner.d.ts +2 -1
  92. package/dist/ui/_components/error_banner.d.ts.map +1 -1
  93. package/dist/ui/_components/error_banner.js +3 -2
  94. package/dist/ui/_components/error_banner.js.map +1 -1
  95. package/dist/ui/_components/message_bubble.d.ts +2 -1
  96. package/dist/ui/_components/message_bubble.d.ts.map +1 -1
  97. package/dist/ui/_components/message_bubble.js +5 -3
  98. package/dist/ui/_components/message_bubble.js.map +1 -1
  99. package/dist/ui/_components/message_list.d.ts +3 -1
  100. package/dist/ui/_components/message_list.d.ts.map +1 -1
  101. package/dist/ui/_components/message_list.js +7 -15
  102. package/dist/ui/_components/message_list.js.map +1 -1
  103. package/dist/ui/_components/sidebar_layout.d.ts +2 -1
  104. package/dist/ui/_components/sidebar_layout.d.ts.map +1 -1
  105. package/dist/ui/_components/sidebar_layout.js +5 -7
  106. package/dist/ui/_components/sidebar_layout.js.map +1 -1
  107. package/dist/ui/_components/start_screen.d.ts +2 -1
  108. package/dist/ui/_components/start_screen.d.ts.map +1 -1
  109. package/dist/ui/_components/start_screen.js +5 -2
  110. package/dist/ui/_components/start_screen.js.map +1 -1
  111. package/dist/ui/_components/state_indicator.d.ts +2 -1
  112. package/dist/ui/_components/state_indicator.d.ts.map +1 -1
  113. package/dist/ui/_components/state_indicator.js +3 -2
  114. package/dist/ui/_components/state_indicator.js.map +1 -1
  115. package/dist/ui/_components/thinking_indicator.d.ts +3 -1
  116. package/dist/ui/_components/thinking_indicator.d.ts.map +1 -1
  117. package/dist/ui/_components/thinking_indicator.js +4 -2
  118. package/dist/ui/_components/thinking_indicator.js.map +1 -1
  119. package/dist/ui/_components/tool_call_block.d.ts +2 -1
  120. package/dist/ui/_components/tool_call_block.d.ts.map +1 -1
  121. package/dist/ui/_components/tool_call_block.js +13 -25
  122. package/dist/ui/_components/tool_call_block.js.map +1 -1
  123. package/dist/ui/_components/transcript.d.ts +2 -1
  124. package/dist/ui/_components/transcript.d.ts.map +1 -1
  125. package/dist/ui/_components/transcript.js +3 -2
  126. package/dist/ui/_components/transcript.js.map +1 -1
  127. package/dist/ui/_jsdom_setup.d.ts +1 -0
  128. package/dist/ui/_jsdom_setup.d.ts.map +1 -0
  129. package/dist/ui/_jsdom_setup.js +6 -0
  130. package/dist/ui/_jsdom_setup.js.map +1 -0
  131. package/dist/ui/audio.d.ts.map +1 -1
  132. package/dist/ui/audio.js +4 -4
  133. package/dist/ui/audio.js.map +1 -1
  134. package/dist/ui/components.d.ts +13 -55
  135. package/dist/ui/components.d.ts.map +1 -1
  136. package/dist/ui/components.js +13 -42
  137. package/dist/ui/components.js.map +1 -1
  138. package/dist/ui/components_mod.d.ts +14 -3
  139. package/dist/ui/components_mod.d.ts.map +1 -1
  140. package/dist/ui/components_mod.js +14 -3
  141. package/dist/ui/components_mod.js.map +1 -1
  142. package/dist/ui/mod.d.ts +1 -8
  143. package/dist/ui/mod.d.ts.map +1 -1
  144. package/dist/ui/mod.js +1 -5
  145. package/dist/ui/mod.js.map +1 -1
  146. package/dist/ui/mount.d.ts +1 -1
  147. package/dist/ui/mount.d.ts.map +1 -1
  148. package/dist/ui/mount.js +1 -0
  149. package/dist/ui/mount.js.map +1 -1
  150. package/dist/ui/session.d.ts +0 -2
  151. package/dist/ui/session.d.ts.map +1 -1
  152. package/dist/ui/session.js +9 -6
  153. package/dist/ui/session.js.map +1 -1
  154. package/dist/ui/signals.d.ts +8 -3
  155. package/dist/ui/signals.d.ts.map +1 -1
  156. package/dist/ui/signals.js +22 -11
  157. package/dist/ui/signals.js.map +1 -1
  158. package/dist/ui/tsconfig.tsbuildinfo +1 -0
  159. package/dist/ui/worklets/playback-processor.js +3 -3
  160. package/package.json +39 -16
  161. package/templates/_shared/CLAUDE.md +50 -30
  162. package/templates/_shared/global.d.ts +1 -0
  163. package/templates/_shared/package.json +2 -1
  164. package/templates/dispatch-center/agent.ts +85 -397
  165. package/templates/solo-rpg/agent.ts +1240 -0
  166. package/templates/solo-rpg/client.tsx +698 -0
@@ -0,0 +1,698 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
2
+ import { useState } from "preact/hooks";
3
+ import {
4
+ ChatView,
5
+ SidebarLayout,
6
+ StartScreen,
7
+ mount,
8
+ useToolResult,
9
+ } from "@alexkroman1/aai/ui";
10
+
11
+ // ── Types ────────────────────────────────────────────────────────────────────
12
+
13
+ type Disposition = "hostile" | "distrustful" | "neutral" | "friendly" | "loyal";
14
+
15
+ interface NPC {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ disposition: Disposition;
20
+ bond: number;
21
+ agenda: string;
22
+ status: "active" | "background" | "deceased";
23
+ }
24
+
25
+ interface ClockData {
26
+ id: string;
27
+ name: string;
28
+ clockType: "threat" | "progress" | "scheme";
29
+ segments: number;
30
+ filled: number;
31
+ triggerDescription: string;
32
+ }
33
+
34
+ interface StoryInfo {
35
+ structureType: string;
36
+ currentAct: number;
37
+ totalActs: number;
38
+ centralConflict: string;
39
+ thematicThread: string;
40
+ storyComplete: boolean;
41
+ currentPhase: string;
42
+ }
43
+
44
+ interface SessionLogEntry {
45
+ scene: number;
46
+ summary: string;
47
+ location: string;
48
+ }
49
+
50
+ interface GameState {
51
+ initialized: boolean;
52
+ phase: string;
53
+ settingGenre: string;
54
+ settingTone: string;
55
+ settingArchetype: string;
56
+ settingDescription: string;
57
+ playerName: string;
58
+ characterConcept: string;
59
+ edge: number; heart: number; iron: number; shadow: number; wits: number;
60
+ health: number; spirit: number; supply: number;
61
+ momentum: number; maxMomentum: number;
62
+ currentLocation: string;
63
+ currentSceneContext: string;
64
+ timeOfDay: string;
65
+ chaosFactor: number;
66
+ crisisMode: boolean;
67
+ gameOver: boolean;
68
+ sceneCount: number;
69
+ npcs: NPC[];
70
+ clocks: ClockData[];
71
+ storyBlueprint: StoryInfo | null;
72
+ kidMode: boolean;
73
+ sessionLog: SessionLogEntry[];
74
+ }
75
+
76
+ const INITIAL: GameState = {
77
+ initialized: false,
78
+ phase: "genre",
79
+ settingGenre: "", settingTone: "", settingArchetype: "",
80
+ settingDescription: "",
81
+ playerName: "", characterConcept: "",
82
+ edge: 1, heart: 1, iron: 1, shadow: 1, wits: 1,
83
+ health: 5, spirit: 5, supply: 5,
84
+ momentum: 2, maxMomentum: 10,
85
+ currentLocation: "", currentSceneContext: "", timeOfDay: "",
86
+ chaosFactor: 5, crisisMode: false, gameOver: false,
87
+ sceneCount: 0,
88
+ npcs: [], clocks: [],
89
+ storyBlueprint: null,
90
+ kidMode: false,
91
+ sessionLog: [],
92
+ };
93
+
94
+ // ── Color Palette ────────────────────────────────────────────────────────────
95
+ const C = {
96
+ bg: "#0a0a0c",
97
+ surface: "#0f0f12",
98
+ surfaceLight: "#16161b",
99
+ border: "#1e1e26",
100
+ borderLight: "#2a2a36",
101
+ accent: "#c9a84c", // gold
102
+ accentDim: "#8a7232",
103
+ accentGlow: "rgba(201,168,76,0.15)",
104
+ text: "#e0dcd0",
105
+ textMuted: "rgba(224,220,208,0.5)",
106
+ textDim: "rgba(224,220,208,0.25)",
107
+ health: "#8b3030",
108
+ healthBright: "#c44040",
109
+ spirit: "#3a5a8a",
110
+ spiritBright: "#5a8acd",
111
+ supply: "#4a6a3a",
112
+ supplyBright: "#6a9a4a",
113
+ chaos: {
114
+ low: "#3a6a3a",
115
+ mid: "#8a7a3a",
116
+ high: "#8a4a2a",
117
+ critical: "#8b2020",
118
+ },
119
+ disposition: {
120
+ hostile: "#c44040",
121
+ distrustful: "#c47a30",
122
+ neutral: "#888888",
123
+ friendly: "#4a9a4a",
124
+ loyal: "#c9a84c",
125
+ },
126
+ threat: "#8b2020",
127
+ progress: "#3a7a9a",
128
+ scheme: "#7a4a8a",
129
+ };
130
+
131
+ // ── Disposition Icons ────────────────────────────────────────────────────────
132
+ const DISP_ICON: Record<Disposition, string> = {
133
+ hostile: "\u2620", distrustful: "\u26A0", neutral: "\u25CB",
134
+ friendly: "\u2665", loyal: "\u2726",
135
+ };
136
+
137
+ // ── Time Labels ──────────────────────────────────────────────────────────────
138
+ const TIME_LABELS: Record<string, string> = {
139
+ early_morning: "Dawn", morning: "Morning", midday: "Midday",
140
+ afternoon: "Afternoon", evening: "Dusk", late_evening: "Twilight",
141
+ night: "Night", deep_night: "Witching Hour",
142
+ };
143
+
144
+ // ── Genre Labels ─────────────────────────────────────────────────────────────
145
+ const GENRE_LABELS: Record<string, string> = {
146
+ dark_fantasy: "Dark Fantasy", high_fantasy: "High Fantasy",
147
+ science_fiction: "Sci-Fi", horror_mystery: "Horror / Mystery",
148
+ steampunk: "Steampunk", cyberpunk: "Cyberpunk",
149
+ urban_fantasy: "Urban Fantasy", victorian_crime: "Victorian Crime",
150
+ historical_roman: "Historical", fairy_tale: "Fairy Tale",
151
+ slice_of_life_90s: "Slice of Life", outdoor_survival: "Survival",
152
+ };
153
+
154
+ // ── Phase Labels ─────────────────────────────────────────────────────────────
155
+ const PHASE_LABELS: Record<string, string> = {
156
+ setup: "Act I", confrontation: "Act II", climax: "Act III",
157
+ ki_introduction: "Ki", sho_development: "Sho",
158
+ ten_twist: "Ten", ketsu_resolution: "Ketsu",
159
+ };
160
+
161
+ // ── Components ───────────────────────────────────────────────────────────────
162
+
163
+ function ResourceBar({ label, current, max, color, colorBright, icon }: {
164
+ label: string; current: number; max: number; color: string; colorBright: string; icon: string;
165
+ }) {
166
+ const pips = [];
167
+ for (let i = 0; i < max; i++) {
168
+ const filled = i < current;
169
+ pips.push(
170
+ <div
171
+ key={i}
172
+ style={{
173
+ width: "18px", height: "10px", borderRadius: "2px",
174
+ background: filled
175
+ ? `linear-gradient(135deg, ${color}, ${colorBright})`
176
+ : "rgba(255,255,255,0.03)",
177
+ border: `1px solid ${filled ? colorBright : "rgba(255,255,255,0.06)"}`,
178
+ boxShadow: filled ? `0 0 4px ${color}66` : "none",
179
+ transition: "all 0.4s ease",
180
+ }}
181
+ />,
182
+ );
183
+ }
184
+ return (
185
+ <div style={{ marginBottom: "8px" }}>
186
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
187
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>
188
+ {icon} {label}
189
+ </span>
190
+ <span style={{ fontSize: "11px", fontWeight: 700, color: current > 0 ? colorBright : C.threat }}>
191
+ {current}
192
+ </span>
193
+ </div>
194
+ <div style={{ display: "flex", gap: "2px" }}>{pips}</div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function MomentumTrack({ momentum, max }: { momentum: number; max: number }) {
200
+ const range: number[] = [];
201
+ for (let i = -6; i <= 10; i++) range.push(i);
202
+ return (
203
+ <div style={{ marginBottom: "8px" }}>
204
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
205
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Momentum</span>
206
+ <span style={{
207
+ fontSize: "11px", fontWeight: 700,
208
+ color: momentum > 0 ? C.spiritBright : momentum < 0 ? C.healthBright : C.textMuted,
209
+ }}>
210
+ {momentum > 0 ? "+" : ""}{momentum}
211
+ </span>
212
+ </div>
213
+ <div style={{ display: "flex", gap: "1px" }}>
214
+ {range.map((v) => (
215
+ <div
216
+ key={v}
217
+ style={{
218
+ flex: 1, height: "6px", borderRadius: "1px",
219
+ background:
220
+ v > max ? "rgba(255,255,255,0.01)"
221
+ : v <= momentum && v > 0 ? C.spiritBright
222
+ : v >= momentum && v < 0 ? C.healthBright
223
+ : v === 0 ? "rgba(255,255,255,0.12)"
224
+ : "rgba(255,255,255,0.03)",
225
+ boxShadow:
226
+ (v <= momentum && v > 0) ? `0 0 3px ${C.spirit}` :
227
+ (v >= momentum && v < 0) ? `0 0 3px ${C.health}` : "none",
228
+ transition: "all 0.3s ease",
229
+ }}
230
+ />
231
+ ))}
232
+ </div>
233
+ <div style={{ display: "flex", justifyContent: "space-between", marginTop: "1px" }}>
234
+ <span style={{ fontSize: "7px", color: C.textDim }}>-6</span>
235
+ <span style={{ fontSize: "7px", color: C.textDim }}>0</span>
236
+ <span style={{ fontSize: "7px", color: C.textDim }}>+10</span>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ function ChaosGauge({ chaos }: { chaos: number }) {
243
+ const pct = ((chaos - 3) / 6) * 100;
244
+ const color = chaos <= 4 ? C.chaos.low : chaos <= 6 ? C.chaos.mid : chaos <= 8 ? C.chaos.high : C.chaos.critical;
245
+ const label = chaos <= 4 ? "Calm" : chaos <= 6 ? "Tense" : chaos <= 8 ? "Volatile" : "Critical";
246
+ return (
247
+ <div style={{ marginBottom: "8px" }}>
248
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
249
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Chaos</span>
250
+ <span style={{ fontSize: "9px", fontWeight: 600, color, textTransform: "uppercase", letterSpacing: "0.08em" }}>
251
+ {label} ({chaos})
252
+ </span>
253
+ </div>
254
+ <div style={{ height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.04)", overflow: "hidden" }}>
255
+ <div style={{
256
+ height: "100%", width: `${pct}%`, borderRadius: "2px",
257
+ background: `linear-gradient(90deg, ${C.chaos.low}, ${color})`,
258
+ boxShadow: `0 0 6px ${color}66`,
259
+ transition: "width 0.5s ease, background 0.5s ease",
260
+ }} />
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ function StatPip({ label, value }: { label: string; value: number }) {
267
+ return (
268
+ <div style={{ textAlign: "center" }}>
269
+ <div style={{ fontSize: "8px", color: C.textDim, textTransform: "uppercase", letterSpacing: "0.1em" }}>
270
+ {label}
271
+ </div>
272
+ <div style={{
273
+ fontSize: "18px", fontWeight: 700, color: C.accent, lineHeight: 1,
274
+ textShadow: `0 0 8px ${C.accentGlow}`,
275
+ }}>
276
+ {value}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ function ClockDisplay({ clock }: { clock: ClockData }) {
283
+ const typeColor = clock.clockType === "threat" ? C.threat :
284
+ clock.clockType === "progress" ? C.progress : C.scheme;
285
+ const segments = [];
286
+ for (let i = 0; i < clock.segments; i++) {
287
+ segments.push(
288
+ <div
289
+ key={i}
290
+ style={{
291
+ width: "10px", height: "10px", borderRadius: "50%",
292
+ background: i < clock.filled
293
+ ? typeColor
294
+ : "rgba(255,255,255,0.04)",
295
+ border: `1px solid ${i < clock.filled ? typeColor : "rgba(255,255,255,0.08)"}`,
296
+ boxShadow: i < clock.filled ? `0 0 4px ${typeColor}66` : "none",
297
+ transition: "all 0.3s ease",
298
+ }}
299
+ />,
300
+ );
301
+ }
302
+ const isFull = clock.filled >= clock.segments;
303
+ return (
304
+ <div style={{
305
+ marginBottom: "8px", padding: "6px 8px",
306
+ borderRadius: "4px", background: "rgba(255,255,255,0.015)",
307
+ border: `1px solid ${isFull ? typeColor : "rgba(255,255,255,0.04)"}`,
308
+ opacity: isFull ? 0.5 : 1,
309
+ }}>
310
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
311
+ <span style={{ fontSize: "10px", fontWeight: 600, color: C.text }}>{clock.name}</span>
312
+ <span style={{
313
+ fontSize: "8px", padding: "1px 4px", borderRadius: "2px",
314
+ background: `${typeColor}22`, color: typeColor, textTransform: "uppercase",
315
+ letterSpacing: "0.06em", border: `1px solid ${typeColor}44`,
316
+ }}>
317
+ {clock.clockType}
318
+ </span>
319
+ </div>
320
+ <div style={{ display: "flex", gap: "3px", flexWrap: "wrap" }}>{segments}</div>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function NpcCard({ npc }: { npc: NPC }) {
326
+ const dispColor = C.disposition[npc.disposition] || C.textMuted;
327
+ const icon = DISP_ICON[npc.disposition] || "\u25CB";
328
+ return (
329
+ <div style={{
330
+ marginBottom: "6px", padding: "6px 8px",
331
+ borderRadius: "4px", background: "rgba(255,255,255,0.015)",
332
+ borderLeft: `2px solid ${dispColor}`,
333
+ }}>
334
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
335
+ <span style={{ fontSize: "11px", fontWeight: 600, color: C.text }}>{npc.name}</span>
336
+ <span style={{ fontSize: "10px", color: dispColor }}>{icon}</span>
337
+ </div>
338
+ {npc.agenda && (
339
+ <div style={{ fontSize: "9px", color: C.textMuted, marginTop: "2px", fontStyle: "italic" }}>
340
+ {npc.agenda}
341
+ </div>
342
+ )}
343
+ <div style={{ display: "flex", gap: "8px", marginTop: "3px" }}>
344
+ <span style={{ fontSize: "8px", color: C.textDim }}>
345
+ {npc.disposition}
346
+ </span>
347
+ {npc.bond !== 0 && (
348
+ <span style={{ fontSize: "8px", color: npc.bond > 0 ? C.supplyBright : C.healthBright }}>
349
+ bond {npc.bond > 0 ? "+" : ""}{npc.bond}
350
+ </span>
351
+ )}
352
+ </div>
353
+ </div>
354
+ );
355
+ }
356
+
357
+ function StoryArc({ story }: { story: StoryInfo }) {
358
+ const pct = story.totalActs > 0 ? ((story.currentAct - 1) / story.totalActs) * 100 : 0;
359
+ const phaseLabel = PHASE_LABELS[story.currentPhase] || story.currentPhase;
360
+ return (
361
+ <div style={{ marginBottom: "8px" }}>
362
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
363
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Story Arc</span>
364
+ <span style={{ fontSize: "9px", color: C.accent }}>
365
+ {phaseLabel} ({story.currentAct}/{story.totalActs})
366
+ </span>
367
+ </div>
368
+ <div style={{ height: "3px", borderRadius: "2px", background: "rgba(255,255,255,0.04)", overflow: "hidden" }}>
369
+ <div style={{
370
+ height: "100%", width: `${pct}%`, borderRadius: "2px",
371
+ background: `linear-gradient(90deg, ${C.accentDim}, ${C.accent})`,
372
+ transition: "width 0.5s ease",
373
+ }} />
374
+ </div>
375
+ {story.storyComplete && (
376
+ <div style={{
377
+ fontSize: "8px", color: C.accent, textTransform: "uppercase",
378
+ letterSpacing: "0.1em", marginTop: "3px", textAlign: "center",
379
+ }}>
380
+ Story Complete
381
+ </div>
382
+ )}
383
+ </div>
384
+ );
385
+ }
386
+
387
+ // ── Sidebar ──────────────────────────────────────────────────────────────────
388
+
389
+ function Sidebar({ game }: { game: GameState }) {
390
+ return (
391
+ <div style={{
392
+ height: "100%", overflowY: "auto", background: C.bg,
393
+ fontFamily: "'Crimson Text', 'Georgia', serif",
394
+ }}>
395
+ <style>
396
+ {`
397
+ @import url('https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap');
398
+ .et-section { border-bottom: 1px solid ${C.border}; padding: 10px 12px; }
399
+ .et-section:last-child { border-bottom: none; }
400
+ .et-label {
401
+ font-size: 9px; text-transform: uppercase; letter-spacing: 0.14em;
402
+ color: ${C.textDim}; margin-bottom: 6px; font-family: sans-serif;
403
+ }
404
+ .et-gold { color: ${C.accent}; text-shadow: 0 0 8px ${C.accentGlow}; }
405
+ .et-crisis-pulse { animation: crisisPulse 2s ease-in-out infinite; }
406
+ @keyframes crisisPulse {
407
+ 0%,100% { opacity: 0.6; } 50% { opacity: 1; }
408
+ }
409
+ .et-scroll::-webkit-scrollbar { width: 3px; }
410
+ .et-scroll::-webkit-scrollbar-thumb { background: ${C.borderLight}; border-radius: 3px; }
411
+ .et-scroll::-webkit-scrollbar-track { background: transparent; }
412
+ `}
413
+ </style>
414
+
415
+ {/* Header */}
416
+ <div class="et-section" style={{ textAlign: "center", paddingTop: "16px", paddingBottom: "12px" }}>
417
+ <div style={{
418
+ fontSize: "8px", letterSpacing: "0.3em", color: C.textDim,
419
+ textTransform: "uppercase", fontFamily: "sans-serif",
420
+ }}>
421
+ Solo RPG
422
+ </div>
423
+ {game.initialized ? (
424
+ <>
425
+ <div class="et-gold" style={{ fontSize: "16px", fontWeight: 700, marginTop: "4px" }}>
426
+ {game.playerName}
427
+ </div>
428
+ {game.characterConcept && (
429
+ <div style={{ fontSize: "11px", color: C.textMuted, fontStyle: "italic", marginTop: "2px" }}>
430
+ {game.characterConcept}
431
+ </div>
432
+ )}
433
+ {game.settingGenre && (
434
+ <div style={{
435
+ fontSize: "8px", color: C.accentDim, marginTop: "4px",
436
+ textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "sans-serif",
437
+ }}>
438
+ {GENRE_LABELS[game.settingGenre] || game.settingGenre}
439
+ </div>
440
+ )}
441
+ </>
442
+ ) : (
443
+ <div style={{ fontSize: "11px", color: C.textDim, marginTop: "8px", fontStyle: "italic" }}>
444
+ Creating your story...
445
+ </div>
446
+ )}
447
+ </div>
448
+
449
+ {/* Pre-init placeholder */}
450
+ {!game.initialized && (
451
+ <div class="et-section" style={{ textAlign: "center", padding: "32px 12px" }}>
452
+ <div style={{ fontSize: "36px", opacity: 0.08 }}>{"\u2726"}</div>
453
+ <div style={{
454
+ fontSize: "10px", color: C.textDim, marginTop: "10px",
455
+ lineHeight: 1.7, fontStyle: "italic",
456
+ }}>
457
+ Choose your world.<br />Shape your character.<br />Begin your tale.
458
+ </div>
459
+ </div>
460
+ )}
461
+
462
+ {game.initialized && (
463
+ <>
464
+ {/* Crisis / Game Over Banner */}
465
+ {(game.crisisMode || game.gameOver) && (
466
+ <div class="et-section et-crisis-pulse" style={{
467
+ textAlign: "center", padding: "8px 12px",
468
+ background: `${C.threat}15`, borderBottom: `1px solid ${C.threat}33`,
469
+ }}>
470
+ <div style={{
471
+ fontSize: "10px", fontWeight: 700, color: C.healthBright,
472
+ textTransform: "uppercase", letterSpacing: "0.15em", fontFamily: "sans-serif",
473
+ }}>
474
+ {game.gameOver ? (game.kidMode ? "In Trouble" : "Finale") :
475
+ (game.kidMode ? "In Trouble" : "Crisis")}
476
+ </div>
477
+ </div>
478
+ )}
479
+
480
+ {/* Stats */}
481
+ <div class="et-section">
482
+ <div class="et-label">Attributes</div>
483
+ <div style={{ display: "flex", justifyContent: "space-between", padding: "0 4px" }}>
484
+ <StatPip label="Edge" value={game.edge} />
485
+ <StatPip label="Heart" value={game.heart} />
486
+ <StatPip label="Iron" value={game.iron} />
487
+ <StatPip label="Shadow" value={game.shadow} />
488
+ <StatPip label="Wits" value={game.wits} />
489
+ </div>
490
+ </div>
491
+
492
+ {/* Resources */}
493
+ <div class="et-section">
494
+ <ResourceBar label="Health" current={game.health} max={5} color={C.health} colorBright={C.healthBright} icon={"\u2665"} />
495
+ <ResourceBar label="Spirit" current={game.spirit} max={5} color={C.spirit} colorBright={C.spiritBright} icon={"\u25C6"} />
496
+ <ResourceBar label="Supply" current={game.supply} max={5} color={C.supply} colorBright={C.supplyBright} icon={"\u25A0"} />
497
+ <MomentumTrack momentum={game.momentum} max={game.maxMomentum} />
498
+ <ChaosGauge chaos={game.chaosFactor} />
499
+ </div>
500
+
501
+ {/* Location & Time */}
502
+ <div class="et-section">
503
+ <div class="et-label">Location</div>
504
+ <div class="et-gold" style={{ fontSize: "13px", fontWeight: 600 }}>
505
+ {game.currentLocation || "Unknown"}
506
+ </div>
507
+ {game.currentSceneContext && (
508
+ <div style={{ fontSize: "10px", color: C.textMuted, marginTop: "3px", fontStyle: "italic", lineHeight: 1.4 }}>
509
+ {game.currentSceneContext}
510
+ </div>
511
+ )}
512
+ {game.timeOfDay && (
513
+ <div style={{
514
+ fontSize: "9px", color: C.accentDim, marginTop: "4px",
515
+ textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "sans-serif",
516
+ }}>
517
+ {TIME_LABELS[game.timeOfDay] || game.timeOfDay}
518
+ </div>
519
+ )}
520
+ </div>
521
+
522
+ {/* Story Arc */}
523
+ {game.storyBlueprint && (
524
+ <div class="et-section">
525
+ <StoryArc story={game.storyBlueprint} />
526
+ </div>
527
+ )}
528
+
529
+ {/* Clocks */}
530
+ {game.clocks.length > 0 && (
531
+ <div class="et-section">
532
+ <div class="et-label">Clocks</div>
533
+ {game.clocks.map((clock) => (
534
+ <ClockDisplay key={clock.id || clock.name} clock={clock} />
535
+ ))}
536
+ </div>
537
+ )}
538
+
539
+ {/* NPCs */}
540
+ {game.npcs.filter(n => n.status !== "deceased").length > 0 && (
541
+ <div class="et-section">
542
+ <div class="et-label">Characters</div>
543
+ {game.npcs.filter(n => n.status === "active").map((npc) => (
544
+ <NpcCard key={npc.id} npc={npc} />
545
+ ))}
546
+ {game.npcs.filter(n => n.status === "background").length > 0 && (
547
+ <>
548
+ <div style={{
549
+ fontSize: "8px", color: C.textDim, textTransform: "uppercase",
550
+ letterSpacing: "0.1em", margin: "6px 0 4px", fontFamily: "sans-serif",
551
+ }}>
552
+ Known
553
+ </div>
554
+ {game.npcs.filter(n => n.status === "background").map((npc) => (
555
+ <div key={npc.id} style={{
556
+ fontSize: "10px", color: C.textMuted, marginBottom: "2px",
557
+ paddingLeft: "8px", borderLeft: `1px solid ${C.border}`,
558
+ }}>
559
+ {npc.name}
560
+ <span style={{ fontSize: "8px", color: C.textDim, marginLeft: "4px" }}>
561
+ {npc.disposition}
562
+ </span>
563
+ </div>
564
+ ))}
565
+ </>
566
+ )}
567
+ </div>
568
+ )}
569
+
570
+ {/* Session Log */}
571
+ {game.sessionLog.length > 0 && (
572
+ <div class="et-section">
573
+ <div class="et-label">Chronicle</div>
574
+ {game.sessionLog.slice(-5).map((entry, i) => (
575
+ <div
576
+ key={i}
577
+ style={{
578
+ fontSize: "10px", color: C.textDim,
579
+ fontStyle: "italic", lineHeight: 1.5, marginBottom: "4px",
580
+ paddingLeft: "8px", borderLeft: `1px solid ${C.border}`,
581
+ }}
582
+ >
583
+ <span style={{ color: C.textMuted, fontStyle: "normal", fontSize: "8px" }}>
584
+ {entry.scene}.{" "}
585
+ </span>
586
+ {entry.summary}
587
+ </div>
588
+ ))}
589
+ </div>
590
+ )}
591
+
592
+ {/* Scene Counter */}
593
+ <div class="et-section" style={{ textAlign: "center", padding: "8px 12px" }}>
594
+ <span style={{ fontSize: "8px", color: C.textDim, letterSpacing: "0.15em", fontFamily: "sans-serif" }}>
595
+ {game.kidMode ? "\u2726 " : ""}SCENE {game.sceneCount}{game.kidMode ? " \u2726" : ""}
596
+ </span>
597
+ </div>
598
+ </>
599
+ )}
600
+ </div>
601
+ );
602
+ }
603
+
604
+ // ── App ──────────────────────────────────────────────────────────────────────
605
+
606
+ function SoloRPGApp() {
607
+ const [game, setGame] = useState<GameState>(structuredClone(INITIAL));
608
+
609
+ // Merge a full-state result into the game
610
+ const mergeState = (result: any, prev: GameState): GameState => ({
611
+ initialized: result.initialized ?? prev.initialized,
612
+ phase: result.phase ?? prev.phase,
613
+ settingGenre: result.settingGenre ?? prev.settingGenre,
614
+ settingTone: result.settingTone ?? prev.settingTone,
615
+ settingArchetype: result.settingArchetype ?? prev.settingArchetype,
616
+ settingDescription: result.settingDescription ?? prev.settingDescription,
617
+ playerName: result.playerName ?? prev.playerName,
618
+ characterConcept: result.characterConcept ?? prev.characterConcept,
619
+ edge: result.edge ?? result.stats?.edge ?? prev.edge,
620
+ heart: result.heart ?? result.stats?.heart ?? prev.heart,
621
+ iron: result.iron ?? result.stats?.iron ?? prev.iron,
622
+ shadow: result.shadow ?? result.stats?.shadow ?? prev.shadow,
623
+ wits: result.wits ?? result.stats?.wits ?? prev.wits,
624
+ health: result.health ?? prev.health,
625
+ spirit: result.spirit ?? prev.spirit,
626
+ supply: result.supply ?? prev.supply,
627
+ momentum: result.momentum ?? prev.momentum,
628
+ maxMomentum: result.maxMomentum ?? prev.maxMomentum,
629
+ currentLocation: result.currentLocation ?? prev.currentLocation,
630
+ currentSceneContext: result.currentSceneContext ?? prev.currentSceneContext,
631
+ timeOfDay: result.timeOfDay ?? prev.timeOfDay,
632
+ chaosFactor: result.chaosFactor ?? prev.chaosFactor,
633
+ crisisMode: result.crisisMode ?? prev.crisisMode,
634
+ gameOver: result.gameOver ?? prev.gameOver,
635
+ sceneCount: result.sceneCount ?? prev.sceneCount,
636
+ npcs: result.npcs ?? prev.npcs,
637
+ clocks: result.clocks ?? prev.clocks,
638
+ storyBlueprint: result.storyBlueprint ?? prev.storyBlueprint,
639
+ kidMode: result.kidMode ?? prev.kidMode,
640
+ sessionLog: result.sessionLog ?? prev.sessionLog,
641
+ });
642
+
643
+ useToolResult((toolName, result: any) => {
644
+ // setup_character and update_state both return full state
645
+ if ((toolName === "setup_character" || toolName === "update_state") && result.success) {
646
+ setGame((prev) => mergeState(result, prev));
647
+ }
648
+
649
+ // action_roll auto-applies consequences — sync sidebar
650
+ if (toolName === "action_roll" && result.currentHealth !== undefined) {
651
+ setGame((prev) => ({
652
+ ...prev,
653
+ health: result.currentHealth,
654
+ spirit: result.currentSpirit ?? prev.spirit,
655
+ supply: result.currentSupply ?? prev.supply,
656
+ momentum: result.currentMomentum ?? prev.momentum,
657
+ chaosFactor: result.chaosFactor ?? prev.chaosFactor,
658
+ crisisMode: result.crisisMode ?? prev.crisisMode,
659
+ gameOver: result.gameOver ?? prev.gameOver,
660
+ sceneCount: result.sceneCount ?? prev.sceneCount,
661
+ }));
662
+ }
663
+
664
+ // burn_momentum resets momentum
665
+ if (toolName === "burn_momentum" && result.burned) {
666
+ setGame((prev) => ({ ...prev, momentum: result.newMomentum }));
667
+ }
668
+
669
+ // load_game restores full state
670
+ if (toolName === "load_game" && result.loaded) {
671
+ setGame((prev) => mergeState(result, prev));
672
+ }
673
+ });
674
+
675
+ return (
676
+ <StartScreen
677
+ icon={<span style={{ fontSize: "28px", color: C.accent }}>{"\u2726"}</span>}
678
+ title="Solo RPG"
679
+ subtitle="A Narrative Solo-RPG Engine"
680
+ buttonText="Begin Your Story"
681
+ >
682
+ <SidebarLayout sidebar={<Sidebar game={game} />} width="260px" side="right">
683
+ <ChatView />
684
+ </SidebarLayout>
685
+ </StartScreen>
686
+ );
687
+ }
688
+
689
+ mount(SoloRPGApp, {
690
+ title: "Solo RPG",
691
+ theme: {
692
+ bg: C.bg,
693
+ primary: C.accent,
694
+ text: C.text,
695
+ surface: C.surface,
696
+ border: C.border,
697
+ },
698
+ });