@alexkroman1/aai-cli 6.11.0 → 7.0.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 (162) hide show
  1. package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
  2. package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
  3. package/dist/_dev-env.d.ts +22 -2
  4. package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
  5. package/dist/_dev-server.d.ts +0 -17
  6. package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
  7. package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
  8. package/dist/_preflight.d.ts +1 -1
  9. package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
  10. package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
  11. package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
  12. package/dist/_workflow-scan.d.ts +108 -0
  13. package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
  14. package/dist/cli.mjs +30 -25
  15. package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
  16. package/dist/client-bundler.mjs +1 -1
  17. package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
  18. package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
  19. package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
  20. package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
  21. package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
  22. package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
  23. package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
  24. package/dist/project-config.mjs +1 -1
  25. package/dist/scaffold/CLAUDE.md +40 -19
  26. package/dist/scaffold/package.json +5 -3
  27. package/dist/scaffold/server.mjs +1 -1
  28. package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
  29. package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
  30. package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
  31. package/dist/templates/call-audit/agent.test.ts +57 -204
  32. package/dist/templates/call-audit/agent.ts +32 -19
  33. package/dist/templates/call-audit/client.tsx +17 -54
  34. package/dist/templates/call-audit/workflows/audit.ts +11 -18
  35. package/dist/templates/call-audit/workflows/ingest.ts +106 -114
  36. package/dist/templates/call-audit/workflows/media.ts +2 -12
  37. package/dist/templates/call-audit/workflows/summarize.ts +47 -52
  38. package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
  39. package/dist/templates/dispatch-center/agent.test.ts +149 -25
  40. package/dist/templates/dispatch-center/client.tsx +239 -129
  41. package/dist/templates/dispatch-center/shared.ts +99 -1
  42. package/dist/templates/dispatch-center/system-prompt.md +3 -1
  43. package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
  44. package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
  45. package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
  46. package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
  47. package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
  48. package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
  49. package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
  50. package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
  51. package/dist/templates/embedded-assets/agent.test.ts +17 -14
  52. package/dist/templates/health-assistant/agent.test.ts +22 -4
  53. package/dist/templates/infocom-adventure/agent.test.ts +4 -6
  54. package/dist/templates/infocom-adventure/client.tsx +246 -164
  55. package/dist/templates/link-digest/agent.test.ts +24 -19
  56. package/dist/templates/link-digest/client.tsx +47 -61
  57. package/dist/templates/link-digest/workflows/digest.ts +19 -31
  58. package/dist/templates/night-owl/agent.test.ts +70 -19
  59. package/dist/templates/night-owl/agent.ts +5 -0
  60. package/dist/templates/night-owl/client.tsx +56 -56
  61. package/dist/templates/night-owl/shared.ts +24 -0
  62. package/dist/templates/night-owl/tools/recommend.ts +18 -6
  63. package/dist/templates/pizza-ordering/agent.test.ts +27 -27
  64. package/dist/templates/pizza-ordering/client.tsx +9 -26
  65. package/dist/templates/plan-and-execute/agent.test.ts +99 -58
  66. package/dist/templates/plan-and-execute/agent.ts +1 -1
  67. package/dist/templates/plan-and-execute/client.tsx +12 -15
  68. package/dist/templates/plan-and-execute/shared.ts +71 -2
  69. package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
  70. package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
  71. package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
  72. package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
  73. package/dist/templates/podcast-digest/agent.test.ts +746 -0
  74. package/dist/templates/podcast-digest/agent.ts +139 -0
  75. package/dist/templates/podcast-digest/client.tsx +154 -0
  76. package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
  77. package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
  78. package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
  79. package/dist/templates/recap-workflow/agent.test.ts +97 -94
  80. package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
  81. package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
  82. package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
  83. package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
  84. package/dist/templates/redline/agent.test.ts +17 -15
  85. package/dist/templates/redline/client.tsx +12 -12
  86. package/dist/templates/redline/workflows/redline.ts +19 -31
  87. package/dist/templates/research-workflow/agent.test.ts +60 -59
  88. package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
  89. package/dist/templates/research-workflow/tools/research_status.ts +9 -11
  90. package/dist/templates/research-workflow/workflows/research.ts +44 -61
  91. package/dist/templates/retail/agent.test.ts +26 -23
  92. package/dist/templates/retail/client.tsx +226 -117
  93. package/dist/templates/retail/registry.test.ts +38 -6
  94. package/dist/templates/retail/store.test.ts +82 -15
  95. package/dist/templates/retail/store.ts +174 -47
  96. package/dist/templates/retail/system-prompt.md +11 -2
  97. package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
  98. package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
  99. package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
  100. package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
  101. package/dist/templates/retail/tools/get_item_details.ts +3 -4
  102. package/dist/templates/retail/tools/get_order_details.ts +3 -4
  103. package/dist/templates/retail/tools/get_product_details.ts +3 -4
  104. package/dist/templates/retail/tools/get_user_details.ts +2 -2
  105. package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
  106. package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
  107. package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
  108. package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
  109. package/dist/templates/retail/tools/modify_user_address.ts +2 -2
  110. package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
  111. package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
  112. package/dist/templates/solo-rpg/agent.test.ts +227 -48
  113. package/dist/templates/solo-rpg/client.tsx +38 -37
  114. package/dist/templates/solo-rpg/shared.ts +145 -19
  115. package/dist/templates/solo-rpg/system-prompt.md +3 -2
  116. package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
  117. package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
  118. package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
  119. package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
  120. package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
  121. package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
  122. package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
  123. package/dist/templates/spoken-summary/agent.test.ts +68 -97
  124. package/dist/templates/spoken-summary/agent.ts +30 -17
  125. package/dist/templates/spoken-summary/client.tsx +10 -50
  126. package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
  127. package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
  128. package/dist/templates/support-line/agent.test.ts +11 -16
  129. package/dist/templates/support-line/agent.ts +1 -1
  130. package/dist/templates/support-line/client.tsx +9 -9
  131. package/dist/templates/support-line/nodes.ts +100 -0
  132. package/dist/templates/support-line/procedure.ts +407 -0
  133. package/dist/templates/support-line/tools/answer_question.ts +17 -4
  134. package/dist/templates/transcription-workflow/agent.test.ts +94 -194
  135. package/dist/templates/transcription-workflow/agent.ts +1 -1
  136. package/dist/templates/transcription-workflow/client.tsx +17 -42
  137. package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
  138. package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
  139. package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
  140. package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
  141. package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
  142. package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
  143. package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
  144. package/dist/templates/travel-concierge/agent.test.ts +64 -33
  145. package/dist/templates/travel-concierge/client.tsx +11 -23
  146. package/dist/templates/travel-concierge/routing.ts +34 -15
  147. package/dist/templates/travel-concierge/shared.ts +70 -3
  148. package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
  149. package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
  150. package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
  151. package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
  152. package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
  153. package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
  154. package/dist/worker-bundler.mjs +1 -1
  155. package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
  156. package/dist/workflow-bundler.d.ts +6 -1
  157. package/dist/workflow.d.ts +1 -1
  158. package/package.json +5 -4
  159. package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
  160. package/dist/templates/support-line/graph.ts +0 -224
  161. package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
  162. /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
@@ -1,6 +1,14 @@
1
1
  import "@alexkroman1/aai-ui/styles.css";
2
- import type { AgentState, ChatMessage } from "@alexkroman1/aai-ui";
3
- import { AutoScroll, client, useSession, useUserTranscript } from "@alexkroman1/aai-ui";
2
+ import type { AgentState } from "@alexkroman1/aai-ui";
3
+ import {
4
+ AutoScroll,
5
+ client,
6
+ useConversation,
7
+ useSession,
8
+ useSessionSelector,
9
+ useTheme,
10
+ } from "@alexkroman1/aai-ui";
11
+ import type { ReactNode } from "react";
4
12
 
5
13
  const CSS = `
6
14
  @keyframes ic-flicker {
@@ -71,89 +79,187 @@ const STATE_COLORS = {
71
79
  error: GREEN_DARK,
72
80
  } satisfies Record<AgentState, string>;
73
81
 
74
- function InfocomAdventure() {
75
- const session = useSession();
76
- // `speaking` and `text` in place of a `userTranscript !== null` check: `null`
77
- // is silence and `""` is speech detected with no words back yet, and reading
78
- // them as one falsy value hides the indicator at the start of every turn.
79
- const { speaking, text: userTranscript } = useUserTranscript();
82
+ /**
83
+ * The exchange, in the CRT idiom.
84
+ *
85
+ * `useConversation()` rather than `session.messages`: the four decisions
86
+ * `<MessageList>` makes the message/tool-call interleave, the streaming
87
+ * narrator line, the transcript's `null`-vs-`""` distinction and the thinking
88
+ * rule — are the hook's now, so this renders them instead of shipping a
89
+ * conversation missing all four. The old list read `messages` alone, so the
90
+ * eight `game_state_*` calls and every partial of the narrator's reply were
91
+ * invisible: the screen sat still until a whole utterance finalized.
92
+ *
93
+ * It also subscribes per FIELD, so a partial transcript no longer re-renders
94
+ * the status bar, the footer and the CRT overlays with it.
95
+ */
96
+ function Transcript() {
97
+ const { items, streaming, transcript, thinking } = useConversation();
98
+ // The one theme read left in this file, and the case `useTheme()` is still
99
+ // for: `scrollbar-color` takes TWO values and has no utility class, so it has
100
+ // to be a style — and reading it off the theme beats re-pinning the two hex
101
+ // codes the `client({ theme })` block below already declares.
102
+ const theme = useTheme();
103
+
104
+ return (
105
+ <AutoScroll
106
+ scrollClassName="ic-messages overflow-y-auto"
107
+ contentClassName="p-5"
108
+ style={{ scrollbarWidth: "thin", scrollbarColor: `${theme.primary} ${theme.surface}` }}
109
+ >
110
+ {items.map((item) =>
111
+ item.kind === "message" ? (
112
+ <div
113
+ key={item.message.id}
114
+ className={`mb-4 ${item.message.role === "user" ? "ic-user-msg" : ""}`}
115
+ style={{
116
+ textShadow:
117
+ item.message.role === "user"
118
+ ? "0 0 5px rgba(0,204,255,0.3)"
119
+ : "0 0 5px rgba(0,255,65,0.3)",
120
+ color: item.message.role === "user" ? CYAN : GREEN,
121
+ }}
122
+ >
123
+ {item.message.content}
124
+ </div>
125
+ ) : (
126
+ // The game engine's own bookkeeping, in the idiom the machine would
127
+ // have printed it in. Dim, because it is beneath the narration and
128
+ // not instead of it.
129
+ <div key={item.toolCall.callId} className="mb-4 text-[13px]" style={{ color: GREEN_DIM }}>
130
+ {`[ ${item.toolCall.name.replace(/^game_state_/, "").replace(/_/g, " ")}${
131
+ item.toolCall.status === "pending" ? " …" : ""
132
+ } ]`}
133
+ </div>
134
+ ),
135
+ )}
136
+ {streaming !== null && (
137
+ <div className="mb-4" style={{ color: GREEN, textShadow: "0 0 5px rgba(0,255,65,0.3)" }}>
138
+ {streaming}
139
+ </div>
140
+ )}
141
+ {transcript.speaking && (
142
+ <div
143
+ className="ic-transcript italic"
144
+ style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
145
+ >
146
+ {transcript.text}
147
+ </div>
148
+ )}
149
+ {thinking && (
150
+ <div className="animate-pulse" style={{ color: GREEN_DIM }}>
151
+ &#9612;
152
+ </div>
153
+ )}
154
+ </AutoScroll>
155
+ );
156
+ }
80
157
 
158
+ /** The turn counter — a NUMBER out of the selector, so a new message array with
159
+ * the same user-message count re-renders nothing. */
160
+ function TurnCount() {
161
+ const turns = useSessionSelector(
162
+ (snapshot) => snapshot.messages.filter((message) => message.role === "user").length,
163
+ );
164
+ return <span>Turns: {turns}</span>;
165
+ }
166
+
167
+ /** The live state dot and its label. */
168
+ function StatusDot() {
169
+ const state = useSessionSelector((snapshot) => snapshot.state);
170
+ const dotColor = STATE_COLORS[state];
81
171
  const stateLabel =
82
- session.state === "listening"
172
+ state === "listening"
83
173
  ? "Listening"
84
- : session.state === "speaking"
174
+ : state === "speaking"
85
175
  ? "Narrating"
86
- : session.state === "thinking"
176
+ : state === "thinking"
87
177
  ? "Thinking"
88
- : session.state === "connecting"
178
+ : state === "connecting"
89
179
  ? "Connecting"
90
- : session.state === "ready"
180
+ : state === "ready"
91
181
  ? "Ready"
92
182
  : "Idle";
93
183
 
94
- const msgCount = session.messages.filter((m: ChatMessage) => m.role === "user").length;
184
+ return (
185
+ <div
186
+ className="flex items-center gap-2.5 text-xs uppercase tracking-wider"
187
+ style={{ color: GREEN_DIM }}
188
+ >
189
+ <div
190
+ className="w-2 h-2 rounded-full"
191
+ style={{
192
+ background: dotColor,
193
+ boxShadow: dotColor !== GREEN_DARK ? `0 0 6px ${dotColor}` : "none",
194
+ }}
195
+ />
196
+ <span>{stateLabel}</span>
197
+ </div>
198
+ );
199
+ }
95
200
 
96
- const dotColor = STATE_COLORS[session.state];
201
+ /**
202
+ * The fault line.
203
+ *
204
+ * `role="alert"` for the reason `ConsoleShell` gives: once `session-core`
205
+ * latches a fatal error the state eyebrow goes back to reading like a live
206
+ * session, so this banner is the only remaining signal — and a screen reader is
207
+ * never told an unannounced one appeared.
208
+ */
209
+ function ErrorBanner() {
210
+ const error = useSessionSelector((snapshot) => snapshot.error);
211
+ if (!error) return null;
212
+ return (
213
+ <div
214
+ role="alert"
215
+ className="px-5 py-2 text-xs"
216
+ style={{ background: "#3a0000", color: "#ff4141" }}
217
+ >
218
+ ERROR: {error.message}
219
+ </div>
220
+ );
221
+ }
97
222
 
98
- if (!session.started) {
99
- return (
100
- <>
101
- <style>{CSS}</style>
102
- <div
103
- className="ic-crt fixed inset-0 overflow-hidden"
104
- style={{
105
- background: CRT_BG,
106
- color: GREEN,
107
- fontFamily: "monospace",
108
- fontSize: "15px",
109
- lineHeight: 1.6,
110
- animation: "ic-flicker 4s infinite",
111
- }}
223
+ /** Pause/resume and hang-up. The only place a whole-session subscription is
224
+ * still needed: the ACTIONS live on `useSession()`, and `useSessionCore` — the
225
+ * narrow way `<Controls>` reaches them — is not on the public surface. */
226
+ function Footer() {
227
+ const session = useSession();
228
+ return (
229
+ <div
230
+ className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
231
+ style={{ borderTop: `1px solid ${GREEN_DARK}`, background: "#001100" }}
232
+ >
233
+ <StatusDot />
234
+ <div className="flex gap-2">
235
+ <button
236
+ type="button"
237
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
238
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
239
+ onClick={session.toggle}
112
240
  >
113
- <div
114
- className="flex flex-col items-center justify-center h-full text-center p-10"
115
- style={{ animation: "ic-boot 1.5s ease-out" }}
116
- >
117
- <div
118
- className="text-[11px] whitespace-pre mb-8"
119
- style={{ textShadow: "0 0 10px rgba(0,255,65,0.5)" }}
120
- >
121
- {ASCII_LOGO}
122
- </div>
123
- <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
124
- AN INTERACTIVE FICTION
125
- </div>
126
- <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
127
- In the style of the classic text adventures.
128
- </div>
129
- <div className="text-[13px] mt-4" style={{ color: GREEN }}>
130
- VOICE-ENABLED EDITION
131
- </div>
132
- <button
133
- type="button"
134
- className="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
135
- style={{
136
- color: GREEN,
137
- border: `1px solid ${GREEN}`,
138
- animation: "ic-pulse 2s ease-in-out infinite",
139
- }}
140
- onClick={session.start}
141
- >
142
- Begin Adventure
143
- </button>
144
- </div>
145
- <div
146
- className="fixed inset-0 pointer-events-none z-12"
147
- style={{
148
- background:
149
- "radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%)",
150
- }}
151
- />
152
- </div>
153
- </>
154
- );
155
- }
241
+ {session.running ? "[P]ause" : "[R]esume"}
242
+ </button>
243
+ {/* end() hangs up and drops the sessionId, so the
244
+ session-scoped game state starts over and the title
245
+ screen returns. (session.reset() keeps the same
246
+ sessionId and would resume the old game.) */}
247
+ <button
248
+ type="button"
249
+ className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
250
+ style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
251
+ onClick={() => session.end()}
252
+ >
253
+ [N]ew Game
254
+ </button>
255
+ </div>
256
+ </div>
257
+ );
258
+ }
156
259
 
260
+ /** The CRT itself: the flicker, the scanlines and the vignette every screen
261
+ * sits inside. */
262
+ function Crt({ children }: { children: ReactNode }) {
157
263
  return (
158
264
  <>
159
265
  <style>{CSS}</style>
@@ -168,98 +274,7 @@ function InfocomAdventure() {
168
274
  animation: "ic-flicker 4s infinite",
169
275
  }}
170
276
  >
171
- <div className="flex flex-col h-full">
172
- {/* Status bar */}
173
- <div
174
- className="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
175
- style={{ background: GREEN, color: CRT_BG }}
176
- >
177
- <div className="flex gap-6">
178
- <span>CAVERN ADVENTURE</span>
179
- <span>Turns: {msgCount}</span>
180
- </div>
181
- <span>Voice Adventure</span>
182
- </div>
183
-
184
- {session.error && (
185
- <div className="px-5 py-2 text-xs" style={{ background: "#3a0000", color: "#ff4141" }}>
186
- ERROR: {session.error.message}
187
- </div>
188
- )}
189
-
190
- {/* Messages */}
191
- <AutoScroll
192
- scrollClassName="ic-messages overflow-y-auto"
193
- contentClassName="p-5"
194
- style={{ scrollbarWidth: "thin", scrollbarColor: `${GREEN} #001a00` }}
195
- >
196
- {session.messages.map((msg: ChatMessage, i: number) => (
197
- <div
198
- key={i}
199
- className={`mb-4 ${msg.role === "user" ? "ic-user-msg" : ""}`}
200
- style={{
201
- textShadow:
202
- msg.role === "user"
203
- ? "0 0 5px rgba(0,204,255,0.3)"
204
- : "0 0 5px rgba(0,255,65,0.3)",
205
- color: msg.role === "user" ? CYAN : GREEN,
206
- }}
207
- >
208
- {msg.content}
209
- </div>
210
- ))}
211
- {speaking && (
212
- <div
213
- className="ic-transcript italic"
214
- style={{ color: "#007a1e", textShadow: "0 0 5px rgba(0,255,65,0.15)" }}
215
- >
216
- {userTranscript}
217
- </div>
218
- )}
219
- </AutoScroll>
220
-
221
- {/* Footer controls */}
222
- <div
223
- className="flex items-center justify-between px-5 py-2 shrink-0 gap-3"
224
- style={{ borderTop: `1px solid ${GREEN_DARK}`, background: "#001100" }}
225
- >
226
- <div
227
- className="flex items-center gap-2.5 text-xs uppercase tracking-wider"
228
- style={{ color: GREEN_DIM }}
229
- >
230
- <div
231
- className="w-2 h-2 rounded-full"
232
- style={{
233
- background: dotColor,
234
- boxShadow: dotColor !== GREEN_DARK ? `0 0 6px ${dotColor}` : "none",
235
- }}
236
- />
237
- <span>{stateLabel}</span>
238
- </div>
239
- <div className="flex gap-2">
240
- <button
241
- type="button"
242
- className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
243
- style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
244
- onClick={session.toggle}
245
- >
246
- {session.running ? "[P]ause" : "[R]esume"}
247
- </button>
248
- {/* end() hangs up and drops the sessionId, so the
249
- session-scoped game state starts over and the title
250
- screen returns. (session.reset() keeps the same
251
- sessionId and would resume the old game.) */}
252
- <button
253
- type="button"
254
- className="px-4 py-1 bg-transparent cursor-pointer uppercase tracking-wider font-mono text-[11px]"
255
- style={{ color: GREEN_DIM, border: `1px solid ${GREEN_DARK}` }}
256
- onClick={() => session.end()}
257
- >
258
- [N]ew Game
259
- </button>
260
- </div>
261
- </div>
262
- </div>
277
+ {children}
263
278
  <div
264
279
  className="fixed inset-0 pointer-events-none z-12"
265
280
  style={{
@@ -271,6 +286,73 @@ function InfocomAdventure() {
271
286
  );
272
287
  }
273
288
 
289
+ function TitleScreen() {
290
+ const session = useSession();
291
+ return (
292
+ <Crt>
293
+ <div
294
+ className="flex flex-col items-center justify-center h-full text-center p-10"
295
+ style={{ animation: "ic-boot 1.5s ease-out" }}
296
+ >
297
+ <div
298
+ className="text-[11px] whitespace-pre mb-8"
299
+ style={{ textShadow: "0 0 10px rgba(0,255,65,0.5)" }}
300
+ >
301
+ {ASCII_LOGO}
302
+ </div>
303
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
304
+ AN INTERACTIVE FICTION
305
+ </div>
306
+ <div className="text-[13px] mb-2" style={{ color: GREEN_DIM }}>
307
+ In the style of the classic text adventures.
308
+ </div>
309
+ <div className="text-[13px] mt-4" style={{ color: GREEN }}>
310
+ VOICE-ENABLED EDITION
311
+ </div>
312
+ <button
313
+ type="button"
314
+ className="mt-10 px-12 py-3.5 bg-transparent cursor-pointer uppercase tracking-[3px] font-mono text-base"
315
+ style={{
316
+ color: GREEN,
317
+ border: `1px solid ${GREEN}`,
318
+ animation: "ic-pulse 2s ease-in-out infinite",
319
+ }}
320
+ onClick={session.start}
321
+ >
322
+ Begin Adventure
323
+ </button>
324
+ </div>
325
+ </Crt>
326
+ );
327
+ }
328
+
329
+ function InfocomAdventure() {
330
+ const started = useSessionSelector((snapshot) => snapshot.started);
331
+ if (!started) return <TitleScreen />;
332
+
333
+ return (
334
+ <Crt>
335
+ <div className="flex flex-col h-full">
336
+ {/* Status bar */}
337
+ <div
338
+ className="flex items-center justify-between px-5 py-2 text-[13px] font-bold tracking-wider shrink-0"
339
+ style={{ background: GREEN, color: CRT_BG }}
340
+ >
341
+ <div className="flex gap-6">
342
+ <span>CAVERN ADVENTURE</span>
343
+ <TurnCount />
344
+ </div>
345
+ <span>Voice Adventure</span>
346
+ </div>
347
+
348
+ <ErrorBanner />
349
+ <Transcript />
350
+ <Footer />
351
+ </div>
352
+ </Crt>
353
+ );
354
+ }
355
+
274
356
  client({
275
357
  component: InfocomAdventure,
276
358
  theme: {
@@ -18,9 +18,12 @@
18
18
  * `FatalError` guards are all testable.
19
19
  */
20
20
 
21
- import { stubStepFetch } from "@alexkroman1/aai/testing";
22
- import { installStubGateway as stubGateway } from "@alexkroman1/aai/testing/vitest";
23
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
21
+ import { schemaInputIssues } from "@alexkroman1/aai/testing";
22
+ import {
23
+ installStubStepFetch,
24
+ installStubGateway as stubGateway,
25
+ } from "@alexkroman1/aai/testing/vitest";
26
+ import { beforeEach, describe, expect, test, vi } from "vitest";
24
27
  import agentDef, { digest } from "./agent.ts";
25
28
  import { extractText, extractTitle, fetchArticle, summarize } from "./workflows/digest.ts";
26
29
 
@@ -49,13 +52,16 @@ describe("the agent declares itself a workflow app", () => {
49
52
 
50
53
  describe("the input schema", () => {
51
54
  test("accepts a URL", async () => {
52
- const result = await digest.input?.["~standard"].validate({ url: "https://example.com/a" });
53
- expect(result?.issues).toBeUndefined();
55
+ // `schemaInputIssues` rather than `digest.input?.["~standard"].validate`:
56
+ // the vendor interface is a wire contract, and `.validate` may answer
57
+ // synchronously or with a promise depending on it — the half a hand-rolled
58
+ // reach gets wrong, since a missing `await` leaves `.issues` undefined and
59
+ // the refusing test below then passes for the wrong reason.
60
+ expect(await schemaInputIssues(digest.input, { url: "https://example.com/a" })).toBeUndefined();
54
61
  });
55
62
 
56
63
  test("rejects a non-URL at the CALL SITE rather than three steps into a run", async () => {
57
- const result = await digest.input?.["~standard"].validate({ url: "not a url" });
58
- expect(result?.issues).toBeDefined();
64
+ expect(await schemaInputIssues(digest.input, { url: "not a url" })).toBeDefined();
59
65
  });
60
66
 
61
67
  test("carries a description, which is what a rendered form labels the field with", () => {
@@ -111,28 +117,23 @@ describe("extractTitle", () => {
111
117
  });
112
118
 
113
119
  describe("fetchArticle", () => {
114
- /** Unpublished between specs — a fetch left behind reaches the next file. */
115
- let restore: (() => void) | undefined;
116
- afterEach(() => {
117
- restore?.();
118
- restore = undefined;
119
- });
120
-
121
120
  /**
122
121
  * A page server answering `html` with `status`.
123
122
  *
124
123
  * Published into `stepFetch`'s own slot rather than over `globalThis.fetch`:
125
124
  * the step calls `stepFetch`, and stubbing the global would pass while
126
125
  * exercising the fallback path production never takes.
126
+ *
127
+ * `installStubStepFetch` unpublishes it on `onTestFinished`, which is why
128
+ * there is no `afterEach` here — a fetch left behind reaches the next file,
129
+ * and a hand-kept restore registry is the thing that forgets.
127
130
  */
128
131
  function stubPage(html: string, status = 200) {
129
- const stub = stubStepFetch(() => ({
132
+ return installStubStepFetch(() => ({
130
133
  status,
131
134
  body: html,
132
135
  headers: { "Content-Type": "text/html" },
133
136
  }));
134
- restore = stub.restore;
135
- return stub;
136
137
  }
137
138
 
138
139
  test("returns the page's title and its readable text", async () => {
@@ -157,10 +158,14 @@ describe("fetchArticle", () => {
157
158
  });
158
159
 
159
160
  test("fails FATALLY on a 404 and plainly on a 503", async () => {
161
+ // `stepFetchOk` builds the message, so it is the SDK's one spelling for a
162
+ // bad response — the REQUEST, the status, and a preview of any body.
160
163
  stubPage("", 404);
161
- await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 404/);
164
+ await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(
165
+ /GET https:\/\/example\.com\/gone 404/,
166
+ );
162
167
  stubPage("", 503);
163
- await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/HTTP 503/);
168
+ await expect(fetchArticle("https://example.com/gone")).rejects.toThrow(/503/);
164
169
  });
165
170
  });
166
171