@duetso/agent 0.1.139 → 0.1.141

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/package.json +1 -1
  2. package/dist/src/cli/help.d.ts.map +1 -1
  3. package/dist/src/cli/help.js +20 -7
  4. package/dist/src/cli/help.js.map +1 -1
  5. package/dist/src/cli/memory-reflect.d.ts.map +1 -1
  6. package/dist/src/cli/memory-reflect.js +3 -2
  7. package/dist/src/cli/memory-reflect.js.map +1 -1
  8. package/dist/src/memory/observation-groups.d.ts +11 -1
  9. package/dist/src/memory/observation-groups.d.ts.map +1 -1
  10. package/dist/src/memory/observation-groups.js +15 -5
  11. package/dist/src/memory/observation-groups.js.map +1 -1
  12. package/dist/src/memory/observational-prompts.d.ts +21 -2
  13. package/dist/src/memory/observational-prompts.d.ts.map +1 -1
  14. package/dist/src/memory/observational-prompts.js +89 -12
  15. package/dist/src/memory/observational-prompts.js.map +1 -1
  16. package/dist/src/memory/observational.d.ts +36 -6
  17. package/dist/src/memory/observational.d.ts.map +1 -1
  18. package/dist/src/memory/observational.js +118 -53
  19. package/dist/src/memory/observational.js.map +1 -1
  20. package/dist/src/session/session.d.ts +6 -0
  21. package/dist/src/session/session.d.ts.map +1 -1
  22. package/dist/src/session/session.js +7 -0
  23. package/dist/src/session/session.js.map +1 -1
  24. package/dist/src/tui/app.d.ts.map +1 -1
  25. package/dist/src/tui/app.js +19 -0
  26. package/dist/src/tui/app.js.map +1 -1
  27. package/dist/src/tui/autocomplete-controller.d.ts +7 -0
  28. package/dist/src/tui/autocomplete-controller.d.ts.map +1 -1
  29. package/dist/src/tui/autocomplete-controller.js +6 -0
  30. package/dist/src/tui/autocomplete-controller.js.map +1 -1
  31. package/dist/src/tui/boot-screen.d.ts.map +1 -1
  32. package/dist/src/tui/boot-screen.js +2 -10
  33. package/dist/src/tui/boot-screen.js.map +1 -1
  34. package/dist/src/tui/controllers.d.ts.map +1 -1
  35. package/dist/src/tui/controllers.js +24 -0
  36. package/dist/src/tui/controllers.js.map +1 -1
  37. package/dist/src/tui/dino/index.d.ts +31 -0
  38. package/dist/src/tui/dino/index.d.ts.map +1 -0
  39. package/dist/src/tui/dino/index.js +302 -0
  40. package/dist/src/tui/dino/index.js.map +1 -0
  41. package/dist/src/tui/dino/input.d.ts +12 -0
  42. package/dist/src/tui/dino/input.d.ts.map +1 -0
  43. package/dist/src/tui/dino/input.js +44 -0
  44. package/dist/src/tui/dino/input.js.map +1 -0
  45. package/dist/src/tui/dino/persistence.d.ts +3 -0
  46. package/dist/src/tui/dino/persistence.d.ts.map +1 -0
  47. package/dist/src/tui/dino/persistence.js +36 -0
  48. package/dist/src/tui/dino/persistence.js.map +1 -0
  49. package/dist/src/tui/dino/render.d.ts +18 -0
  50. package/dist/src/tui/dino/render.d.ts.map +1 -0
  51. package/dist/src/tui/dino/render.js +161 -0
  52. package/dist/src/tui/dino/render.js.map +1 -0
  53. package/dist/src/tui/dino/state.d.ts +141 -0
  54. package/dist/src/tui/dino/state.d.ts.map +1 -0
  55. package/dist/src/tui/dino/state.js +156 -0
  56. package/dist/src/tui/dino/state.js.map +1 -0
  57. package/dist/src/tui/dino/tick.d.ts +10 -0
  58. package/dist/src/tui/dino/tick.d.ts.map +1 -0
  59. package/dist/src/tui/dino/tick.js +117 -0
  60. package/dist/src/tui/dino/tick.js.map +1 -0
  61. package/dist/src/tui/dino/visibility.d.ts +2 -0
  62. package/dist/src/tui/dino/visibility.d.ts.map +1 -0
  63. package/dist/src/tui/dino/visibility.js +19 -0
  64. package/dist/src/tui/dino/visibility.js.map +1 -0
  65. package/dist/src/tui/key-handlers.d.ts +7 -0
  66. package/dist/src/tui/key-handlers.d.ts.map +1 -1
  67. package/dist/src/tui/key-handlers.js +25 -1
  68. package/dist/src/tui/key-handlers.js.map +1 -1
  69. package/dist/src/tui/layout.d.ts +7 -0
  70. package/dist/src/tui/layout.d.ts.map +1 -1
  71. package/dist/src/tui/layout.js +14 -1
  72. package/dist/src/tui/layout.js.map +1 -1
  73. package/dist/src/tui/slash-commands.d.ts +8 -0
  74. package/dist/src/tui/slash-commands.d.ts.map +1 -1
  75. package/dist/src/tui/slash-commands.js +17 -0
  76. package/dist/src/tui/slash-commands.js.map +1 -1
  77. package/dist/src/tui/status-controller.d.ts +9 -0
  78. package/dist/src/tui/status-controller.d.ts.map +1 -1
  79. package/dist/src/tui/status-controller.js +28 -0
  80. package/dist/src/tui/status-controller.js.map +1 -1
  81. package/dist/src/turn-runner/shell-state-handle.d.ts +8 -1
  82. package/dist/src/turn-runner/shell-state-handle.d.ts.map +1 -1
  83. package/dist/src/turn-runner/shell-state-handle.js +4 -1
  84. package/dist/src/turn-runner/shell-state-handle.js.map +1 -1
  85. package/dist/src/turn-runner/skill-context.d.ts +6 -0
  86. package/dist/src/turn-runner/skill-context.d.ts.map +1 -1
  87. package/dist/src/turn-runner/skill-context.js +12 -0
  88. package/dist/src/turn-runner/skill-context.js.map +1 -1
  89. package/dist/src/turn-runner/state-machine-controller.d.ts +8 -2
  90. package/dist/src/turn-runner/state-machine-controller.d.ts.map +1 -1
  91. package/dist/src/turn-runner/state-machine-controller.js +51 -20
  92. package/dist/src/turn-runner/state-machine-controller.js.map +1 -1
  93. package/dist/src/turn-runner/tools.js +17 -21
  94. package/dist/src/turn-runner/tools.js.map +1 -1
  95. package/dist/src/turn-runner/turn-runner.d.ts +15 -3
  96. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  97. package/dist/src/turn-runner/turn-runner.js +33 -19
  98. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  99. package/package.json +1 -1
@@ -0,0 +1,302 @@
1
+ // OpenTUI panel that hosts the dino game. Owns the tick timer, the
2
+ // `BoxRenderable` mounted into the layout, and the freeze/resume lifecycle
3
+ // that the StatusController drives via its `onRunningChange` hook.
4
+ //
5
+ // Lifecycle contract (matches the spec we agreed in design):
6
+ // - toggle() : user pressed Ctrl-G; expand/collapse the panel.
7
+ // - freeze() : agent flipped to needs-user; snap dino to ground
8
+ // and stop the world. No-op when collapsed.
9
+ // - resume() : agent flipped to busy; start the 3-2-1 countdown
10
+ // followed by a grace gap. No-op when collapsed or
11
+ // when no run is in progress.
12
+ // - handleKey(key) : input router forwards keystrokes here while the
13
+ // panel is expanded and the agent is busy. Returns
14
+ // true when the key was consumed.
15
+ // - destroy() : tear down the tick timer; persist high score.
16
+ import { BoxRenderable, TextRenderable } from "@opentui/core";
17
+ import { SIDEBAR_WIDTH } from "../sidebar.js";
18
+ import { COLORS } from "../theme.js";
19
+ import { actionForKey, applyJump, applyStart } from "./input.js";
20
+ import { loadHighScore, saveHighScore } from "./persistence.js";
21
+ import { EXPANDED_ROWS, renderExpanded } from "./render.js";
22
+ import { panelVisibleRowCount } from "./visibility.js";
23
+ import { beginCountdown, expandResumeKind, freezeRun, initialState, setFieldWidth, } from "./state.js";
24
+ import { tick } from "./tick.js";
25
+ /** Horizontal chrome around the dino panel: the panel sits inside the main
26
+ * layout column, which itself sits to the left of the sidebar. We deduct
27
+ * the sidebar plus a few cells of frame padding to land on a field width
28
+ * that fills the available column without overflowing it. */
29
+ const PANEL_CHROME_COLUMNS = 4;
30
+ /** Cadence of the physics loop. Chosen high enough that a jump arc feels
31
+ * smooth in a terminal but low enough that the redraw cost is negligible
32
+ * (15 fps ≈ 67ms per frame). The countdown layer uses this same value to
33
+ * size the 3-2-1 phase. */
34
+ const TICKS_PER_SECOND = 15;
35
+ const TICK_INTERVAL_MS = Math.round(1000 / TICKS_PER_SECOND);
36
+ export function createDinoPanel(opts) {
37
+ const { renderer } = opts;
38
+ // The panel is collapsed by default and re-collapses every time the
39
+ // agent goes idle. "Hint only" is the starting point for every busy
40
+ // cycle; if the user wants the game they toggle it open each time.
41
+ // We do not persist expanded state across sessions or across busy
42
+ // cycles — the simpler contract is more predictable.
43
+ let expanded = false;
44
+ // When expanded, tracks whether game keys (space, ArrowUp) belong to
45
+ // the dino or to the composer. Defaults to true on every expand so
46
+ // Ctrl-G straight into a playable game; flipping to false via a
47
+ // second Ctrl-G keeps the panel visible but releases the keys back
48
+ // to the textarea so the user can type a follow-up without
49
+ // collapsing the game.
50
+ let gameFocused = true;
51
+ let state = initialState(loadHighScore(), computeFieldWidth(renderer));
52
+ // Tracks whether the most recent freeze was caused by the agent needing
53
+ // input (so resume runs the 3-2-1 + grace gap) vs. a manual collapse
54
+ // (so re-expand resumes instantly per the spec).
55
+ let frozenByAgent = false;
56
+ // Mirrors the StatusController running signal. Ctrl-G is only
57
+ // accepted while this is true; at idle the panel is invisible and
58
+ // reserves no rows.
59
+ let agentBusy = false;
60
+ let ticker;
61
+ // Last persisted high score, so we only write on improvement. Hydrated
62
+ // from disk at construction time.
63
+ let persistedHighScore = state.highScore;
64
+ const view = new BoxRenderable(renderer, {
65
+ flexDirection: "column",
66
+ flexShrink: 0,
67
+ border: false,
68
+ });
69
+ // One TextRenderable per displayed row. The expanded layout reserves
70
+ // EXPANDED_ROWS rows; collapsed reuses the first row and hides the
71
+ // rest. Building a fixed row pool avoids reflowing the box on every
72
+ // frame.
73
+ const rows = Array.from({ length: EXPANDED_ROWS }, () => {
74
+ const row = new TextRenderable(renderer, {
75
+ content: "",
76
+ fg: COLORS.hint,
77
+ height: 1,
78
+ flexShrink: 0,
79
+ selectable: false,
80
+ });
81
+ view.add(row);
82
+ return row;
83
+ });
84
+ syncVisibility();
85
+ paint();
86
+ // Resize handler. The renderer fires "resize" on terminal size changes;
87
+ // we recompute the playfield width and let the next paint stretch the
88
+ // ground line / obstacles to the new column. We keep a reference so the
89
+ // panel's `destroy()` can detach it on teardown.
90
+ const handleResize = () => {
91
+ const next = computeFieldWidth(renderer);
92
+ if (next === state.fieldWidth)
93
+ return;
94
+ state = setFieldWidth(state, next);
95
+ paint();
96
+ };
97
+ renderer.on("resize", handleResize);
98
+ function isExpanded() {
99
+ return expanded;
100
+ }
101
+ function isGameFocused() {
102
+ return expanded && gameFocused;
103
+ }
104
+ function toggle() {
105
+ // The dino panel is a "while-you-wait" affordance: it only exists
106
+ // while the agent is actually busy. At rest there is no hint and no
107
+ // toggle — Ctrl-G is a no-op so a fresh `duet` session is clean.
108
+ if (!agentBusy)
109
+ return;
110
+ // Three-step cycle so the user can toggle keystroke ownership
111
+ // without losing what they typed:
112
+ // collapsed → expanded + game-focused
113
+ // expanded + game → expanded + composer-focused (typing)
114
+ // expanded + composer → collapsed
115
+ if (!expanded) {
116
+ expanded = true;
117
+ gameFocused = true;
118
+ }
119
+ else if (gameFocused) {
120
+ // Stay expanded; just release keys to the composer. The game
121
+ // keeps animating in the background so the user sees obstacles
122
+ // approach while they type.
123
+ gameFocused = false;
124
+ syncVisibility();
125
+ paint();
126
+ return;
127
+ }
128
+ else {
129
+ expanded = false;
130
+ gameFocused = true;
131
+ }
132
+ if (expanded) {
133
+ // Re-expanding into a frozen run: if the agent caused the freeze
134
+ // (needs-user → answered → busy again, but `resume()` no-op'd
135
+ // because the panel was collapsed), run the 3-2-1 + grace gap the
136
+ // live resume path uses. If the user collapsed manually, resume
137
+ // instantly — they explicitly chose to peek away.
138
+ const kind = expandResumeKind(state.phase, frozenByAgent);
139
+ if (kind === "countdown") {
140
+ state = beginCountdown(state, TICKS_PER_SECOND);
141
+ frozenByAgent = false;
142
+ }
143
+ else if (kind === "run") {
144
+ state = { ...state, phase: { kind: "running" } };
145
+ }
146
+ }
147
+ else {
148
+ // Collapsing while running snaps the world quiet without ending the
149
+ // run; the user can re-expand and pick up where they left off.
150
+ if (state.phase.kind === "running" || state.phase.kind === "grace") {
151
+ state = freezeRun(state);
152
+ frozenByAgent = false;
153
+ }
154
+ stopTicker();
155
+ }
156
+ syncVisibility();
157
+ paint();
158
+ if (expanded && stateNeedsTicking())
159
+ startTicker();
160
+ }
161
+ function resume() {
162
+ if (!expanded)
163
+ return;
164
+ // No run in progress → nothing to resume; the user starts a fresh run
165
+ // themselves by pressing space.
166
+ if (state.phase.kind !== "frozen")
167
+ return;
168
+ if (frozenByAgent) {
169
+ state = beginCountdown(state, TICKS_PER_SECOND);
170
+ }
171
+ else {
172
+ state = { ...state, phase: { kind: "running" } };
173
+ }
174
+ frozenByAgent = false;
175
+ startTicker();
176
+ paint();
177
+ }
178
+ function freeze() {
179
+ frozenByAgent = true;
180
+ state = freezeRun(state);
181
+ stopTicker();
182
+ paint();
183
+ }
184
+ function setAgentBusy(busy) {
185
+ if (agentBusy === busy)
186
+ return;
187
+ agentBusy = busy;
188
+ // The game is strictly opt-in: the panel never auto-expands. The
189
+ // Ctrl-G tease lives in the input placeholder, and the user has to
190
+ // press it themselves to bring up the playfield.
191
+ // When the agent goes idle, snap the panel back to collapsed so
192
+ // the next busy cycle starts invisible rather than re-appearing
193
+ // already expanded. The user opts back in by pressing Ctrl-G.
194
+ if (!busy) {
195
+ expanded = false;
196
+ gameFocused = true;
197
+ }
198
+ syncVisibility();
199
+ paint();
200
+ }
201
+ function handleKey(keyName) {
202
+ if (!expanded)
203
+ return false;
204
+ const action = actionForKey(state, keyName);
205
+ if (action === "ignore")
206
+ return false;
207
+ if (action === "jump") {
208
+ state = applyJump(state);
209
+ }
210
+ else if (action === "start") {
211
+ state = applyStart(state);
212
+ // Starting a run after a death needs the timer running again.
213
+ startTicker();
214
+ }
215
+ paint();
216
+ return true;
217
+ }
218
+ function destroy() {
219
+ stopTicker();
220
+ renderer.off("resize", handleResize);
221
+ if (state.highScore > persistedHighScore) {
222
+ saveHighScore(state.highScore);
223
+ persistedHighScore = state.highScore;
224
+ }
225
+ }
226
+ function stateNeedsTicking() {
227
+ return (state.phase.kind === "running" ||
228
+ state.phase.kind === "grace" ||
229
+ state.phase.kind === "countdown");
230
+ }
231
+ function startTicker() {
232
+ if (ticker !== undefined)
233
+ return;
234
+ if (!stateNeedsTicking())
235
+ return;
236
+ ticker = setInterval(() => {
237
+ state = tick(state, { random: Math.random, ticksPerSecond: TICKS_PER_SECOND });
238
+ if (state.highScore > persistedHighScore) {
239
+ saveHighScore(state.highScore);
240
+ persistedHighScore = state.highScore;
241
+ }
242
+ if (!stateNeedsTicking())
243
+ stopTicker();
244
+ paint();
245
+ }, TICK_INTERVAL_MS);
246
+ }
247
+ function stopTicker() {
248
+ if (ticker !== undefined) {
249
+ clearInterval(ticker);
250
+ ticker = undefined;
251
+ }
252
+ }
253
+ function syncVisibility() {
254
+ const visibleRows = panelVisibleRowCount(expanded, agentBusy);
255
+ for (let i = 0; i < rows.length; i++) {
256
+ rows[i].visible = i < visibleRows;
257
+ }
258
+ }
259
+ function paint() {
260
+ // At rest the panel is invisible; skip painting entirely so the row
261
+ // contents stay empty and no chrome leaks into the layout.
262
+ if (!agentBusy) {
263
+ for (const row of rows)
264
+ row.content = "";
265
+ return;
266
+ }
267
+ if (expanded) {
268
+ const frame = renderExpanded(state, gameFocused);
269
+ for (let i = 0; i < rows.length; i++) {
270
+ rows[i].content = frame[i] ?? "";
271
+ }
272
+ }
273
+ else {
274
+ // Collapsed reserves zero rows; clear any stale content so the
275
+ // pool stays tidy when the next expand reuses it.
276
+ for (const row of rows)
277
+ row.content = "";
278
+ }
279
+ }
280
+ return {
281
+ view,
282
+ isExpanded,
283
+ isGameFocused,
284
+ toggle,
285
+ resume,
286
+ freeze,
287
+ setAgentBusy,
288
+ handleKey,
289
+ destroy,
290
+ };
291
+ }
292
+ /** Compute the current playfield width from the renderer's terminal size.
293
+ * The panel sits in the main layout column to the left of the sidebar, so
294
+ * we deduct the sidebar plus a small chrome margin and let `setFieldWidth`
295
+ * clamp into the supported [MIN_FIELD_WIDTH, MAX_FIELD_WIDTH] range. */
296
+ function computeFieldWidth(renderer) {
297
+ const terminal = renderer.terminalWidth;
298
+ if (!Number.isFinite(terminal) || terminal <= 0)
299
+ return 0; // clamped by setFieldWidth
300
+ return terminal - SIDEBAR_WIDTH - PANEL_CHROME_COLUMNS;
301
+ }
302
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/tui/dino/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { GameState } from "./state.js";
2
+ export type DinoAction = "jump" | "start" | "ignore";
3
+ /** Returns the action implied by a key press for the current game state.
4
+ * Returns `"ignore"` for keys the panel should let bubble (so e.g. typing
5
+ * letters while the panel is collapsed never gets eaten). */
6
+ export declare function actionForKey(state: GameState, keyName: string | undefined): DinoAction;
7
+ /** Apply a `jump` action: only takes effect when the dino is grounded so
8
+ * the player cannot double-jump. */
9
+ export declare function applyJump(state: GameState): GameState;
10
+ /** Apply a `start` action: begin a fresh run from `idle`/`gameover`. */
11
+ export declare function applyStart(state: GameState): GameState;
12
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/input.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAQrD;;8DAE8D;AAC9D,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAetF;AAED;qCACqC;AACrC,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAGrD;AAED,wEAAwE;AACxE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAGtD"}
@@ -0,0 +1,44 @@
1
+ // Keystroke → game action. Pure so the panel can route keys through this
2
+ // without dragging OpenTUI key types into the reducer layer.
3
+ import { JUMP_VELOCITY, startRun } from "./state.js";
4
+ /** Names of keys that, when game is in a state that accepts input, trigger
5
+ * a jump. Arrow-only on purpose: the spacebar belongs to the composer so
6
+ * the user can type a follow-up while the game keeps running in the
7
+ * background. OpenTUI normalizes ArrowUp to `name: "up"`. */
8
+ const JUMP_KEYS = new Set(["up"]);
9
+ /** Returns the action implied by a key press for the current game state.
10
+ * Returns `"ignore"` for keys the panel should let bubble (so e.g. typing
11
+ * letters while the panel is collapsed never gets eaten). */
12
+ export function actionForKey(state, keyName) {
13
+ if (!keyName)
14
+ return "ignore";
15
+ if (state.phase.kind === "idle" || state.phase.kind === "gameover") {
16
+ // Any of the jump keys also acts as "start a run" so the user does not
17
+ // need a separate start key after a death.
18
+ if (JUMP_KEYS.has(keyName) || keyName === "return" || keyName === "enter")
19
+ return "start";
20
+ return "ignore";
21
+ }
22
+ if (state.phase.kind === "running" || state.phase.kind === "grace") {
23
+ if (JUMP_KEYS.has(keyName))
24
+ return "jump";
25
+ return "ignore";
26
+ }
27
+ // countdown / frozen swallow input so the user cannot pre-jump and then
28
+ // land on a cactus the instant the world unfreezes.
29
+ return "ignore";
30
+ }
31
+ /** Apply a `jump` action: only takes effect when the dino is grounded so
32
+ * the player cannot double-jump. */
33
+ export function applyJump(state) {
34
+ if (state.dinoY > 0 || state.dinoVy !== 0)
35
+ return state;
36
+ return { ...state, dinoVy: JUMP_VELOCITY };
37
+ }
38
+ /** Apply a `start` action: begin a fresh run from `idle`/`gameover`. */
39
+ export function applyStart(state) {
40
+ if (state.phase.kind !== "idle" && state.phase.kind !== "gameover")
41
+ return state;
42
+ return startRun(state);
43
+ }
44
+ //# sourceMappingURL=input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.js","sourceRoot":"","sources":["../../../../src/tui/dino/input.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,6DAA6D;AAG7D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIrD;;;8DAG8D;AAC9D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAElC;;8DAE8D;AAC9D,MAAM,UAAU,YAAY,CAAC,KAAgB,EAAE,OAA2B;IACxE,IAAI,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IAC9B,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACnE,uEAAuE;QACvE,2CAA2C;QAC3C,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,OAAO;YAAE,OAAO,OAAO,CAAC;QAC1F,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACnE,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,MAAM,CAAC;QAC1C,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,wEAAwE;IACxE,oDAAoD;IACpD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;qCACqC;AACrC,MAAM,UAAU,SAAS,CAAC,KAAgB;IACxC,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAC7C,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACjF,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function loadHighScore(): number;
2
+ export declare function saveHighScore(score: number): void;
3
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/persistence.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAOjD"}
@@ -0,0 +1,36 @@
1
+ // Reads/writes the dino panel's tiny persisted footprint under
2
+ // `~/.duet/dino/`. Only the high score is persisted: panel open/closed
3
+ // state is intentionally session-local because the spec is that every
4
+ // new busy cycle starts at "hint only" and the user toggles back in.
5
+ // Writes are best-effort: a failed write degrades the next run to the
6
+ // in-memory default but never propagates to the caller, because losing a
7
+ // high score is strictly better than crashing the TUI on a read-only
8
+ // home directory.
9
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ const DINO_DIR = join(homedir(), ".duet", "dino");
13
+ const HIGHSCORE_FILE = join(DINO_DIR, "highscore");
14
+ export function loadHighScore() {
15
+ try {
16
+ const raw = readFileSync(HIGHSCORE_FILE, "utf8").trim();
17
+ const n = Number.parseInt(raw, 10);
18
+ return Number.isFinite(n) && n >= 0 ? n : 0;
19
+ }
20
+ catch {
21
+ return 0;
22
+ }
23
+ }
24
+ export function saveHighScore(score) {
25
+ try {
26
+ ensureDir(HIGHSCORE_FILE);
27
+ writeFileSync(HIGHSCORE_FILE, `${Math.max(0, Math.floor(score))}\n`, "utf8");
28
+ }
29
+ catch {
30
+ // Best-effort.
31
+ }
32
+ }
33
+ function ensureDir(filePath) {
34
+ mkdirSync(dirname(filePath), { recursive: true });
35
+ }
36
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../../../src/tui/dino/persistence.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,uEAAuE;AACvE,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,yEAAyE;AACzE,qEAAqE;AACrE,kBAAkB;AAElB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAClD,MAAM,cAAc,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAEnD,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACxD,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,IAAI,CAAC;QACH,SAAS,CAAC,cAAc,CAAC,CAAC;QAC1B,aAAa,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,eAAe;IACjB,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,QAAgB;IACjC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type GameState } from "./state.js";
2
+ /** Total rendered rows of the expanded panel: title + 10 playfield rows +
3
+ * status row = 12 rows when expanded. Collapsed = 0 rows — the Ctrl-G
4
+ * tease lives in the input placeholder instead, so the dino takes no
5
+ * vertical space until the user opts in. */
6
+ export declare const EXPANDED_ROWS = 12;
7
+ export declare const COLLAPSED_ROWS = 0;
8
+ export declare const PLAYFIELD_ROWS = 10;
9
+ /** Width of the dino sprite in cells. Anchored at `DINO_X` (left edge). */
10
+ export declare const DINO_WIDTH = 4;
11
+ /** Height of the dino sprite in rows; feet sit on the ground row, so the
12
+ * body extends up `DINO_HEIGHT - 1` rows above ground when grounded. */
13
+ export declare const DINO_HEIGHT = 3;
14
+ /** Full 12-row expanded panel. The phase determines which overlay (idle
15
+ * splash, countdown numerals, dim "agent needs you" hint, gameover
16
+ * banner) is composited on top of the playfield. */
17
+ export declare function renderExpanded(state: GameState, gameFocused?: boolean): string[];
18
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../../src/tui/dino/render.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEhE;;;6CAG6C;AAC7C,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,2EAA2E;AAC3E,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B;yEACyE;AACzE,eAAO,MAAM,WAAW,IAAI,CAAC;AAc7B;;qDAEqD;AACrD,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,WAAW,GAAE,OAAc,GAAG,MAAM,EAAE,CAYtF"}
@@ -0,0 +1,161 @@
1
+ // state → string[]. Pure ASCII; no OpenTUI imports so tests can snapshot
2
+ // the rendered frame as plain strings.
3
+ import { DINO_X, GROUND_ROW } from "./state.js";
4
+ /** Total rendered rows of the expanded panel: title + 10 playfield rows +
5
+ * status row = 12 rows when expanded. Collapsed = 0 rows — the Ctrl-G
6
+ * tease lives in the input placeholder instead, so the dino takes no
7
+ * vertical space until the user opts in. */
8
+ export const EXPANDED_ROWS = 12;
9
+ export const COLLAPSED_ROWS = 0;
10
+ export const PLAYFIELD_ROWS = 10;
11
+ /** Width of the dino sprite in cells. Anchored at `DINO_X` (left edge). */
12
+ export const DINO_WIDTH = 4;
13
+ /** Height of the dino sprite in rows; feet sit on the ground row, so the
14
+ * body extends up `DINO_HEIGHT - 1` rows above ground when grounded. */
15
+ export const DINO_HEIGHT = 3;
16
+ // Tiny T-rex sprites in pure ASCII so they line up cleanly in any
17
+ // terminal. The right edge points in the direction of travel: head and
18
+ // snout to the right, tail to the left. Two run poses alternate while
19
+ // grounded so the legs visibly cycle; a third pose is used mid-air.
20
+ //
21
+ // Each sprite is exactly DINO_HEIGHT rows by DINO_WIDTH cols. Empty
22
+ // cells use a literal space so the sprite never accidentally erases
23
+ // neighboring playfield characters.
24
+ const DINO_RUN_A = [" __", " /_)", " /\\ "];
25
+ const DINO_RUN_B = [" __", " /_)", " \\/ "];
26
+ const DINO_JUMP = [" __", " /_)", " || "];
27
+ /** Full 12-row expanded panel. The phase determines which overlay (idle
28
+ * splash, countdown numerals, dim "agent needs you" hint, gameover
29
+ * banner) is composited on top of the playfield. */
30
+ export function renderExpanded(state, gameFocused = true) {
31
+ const rows = buildPlayfield(state);
32
+ const title = renderTitle(state);
33
+ const statusRow = renderStatusRow(state, gameFocused);
34
+ const overlay = renderOverlay(state);
35
+ if (overlay) {
36
+ // Overlay replaces a centered slice of the playfield rows; the dino
37
+ // and obstacles still show around it so the user sees what they will
38
+ // wake up to.
39
+ overlayRows(rows, overlay);
40
+ }
41
+ return [title, ...rows, statusRow];
42
+ }
43
+ function renderTitle(state) {
44
+ const score = pad4(Math.floor(state.score));
45
+ const hi = pad4(state.highScore);
46
+ return ` duet dino · HI ${hi} · ${score}`;
47
+ }
48
+ function renderStatusRow(state, gameFocused) {
49
+ // When the user has Ctrl-G'd into typing mode the panel stays open
50
+ // and the game keeps animating, but the spacebar belongs to the
51
+ // composer. The status row advertises how to take the keys back so
52
+ // the mode is discoverable.
53
+ if (!gameFocused) {
54
+ return " typing mode · Ctrl-G to play · Ctrl-G again to close";
55
+ }
56
+ switch (state.phase.kind) {
57
+ case "idle":
58
+ return " press ↑ to start · Ctrl-G for typing mode";
59
+ case "running":
60
+ return " ↑ to jump · Ctrl-G for typing mode";
61
+ case "grace":
62
+ return " …";
63
+ case "countdown":
64
+ return " resuming…";
65
+ case "frozen":
66
+ return " ▲ agent needs you — answer above";
67
+ case "gameover":
68
+ return " game over · ↑ to retry · Ctrl-G for typing mode";
69
+ }
70
+ }
71
+ function buildPlayfield(state) {
72
+ const width = state.fieldWidth;
73
+ const rows = Array.from({ length: PLAYFIELD_ROWS }, () => Array.from({ length: width }, () => " "));
74
+ // Ground line.
75
+ const groundIndex = Math.min(GROUND_ROW, PLAYFIELD_ROWS - 1);
76
+ for (let x = 0; x < width; x++)
77
+ rows[groundIndex][x] = "_";
78
+ // Obstacles. Cactus is drawn as "#" stacked vertically, anchored at the
79
+ // ground line. A 1-cell-wide column keeps collision and rendering in
80
+ // exact agreement.
81
+ for (const o of state.obstacles) {
82
+ const col = Math.round(o.x);
83
+ if (col < 0 || col >= width)
84
+ continue;
85
+ for (let h = 0; h < o.height; h++) {
86
+ const row = groundIndex - 1 - h;
87
+ if (row >= 0)
88
+ rows[row][col] = "#";
89
+ }
90
+ }
91
+ // Dino. Multi-row T-rex sprite anchored at `DINO_X`, with feet on the
92
+ // ground row when grounded. Floor-rounded `y` so a sub-cell hop still
93
+ // visibly lifts off. The grounded pose alternates between two leg
94
+ // positions so the dino looks like it's running; airborne uses the
95
+ // tucked-legs jump sprite so the user can confirm a jump landed.
96
+ const grounded = state.dinoY === 0;
97
+ const sprite = grounded
98
+ ? // Alternate legs every ~2 score units so the cadence reads as
99
+ // running rather than flickering at every frame.
100
+ Math.floor(state.score * 2) % 2 === 0
101
+ ? DINO_RUN_A
102
+ : DINO_RUN_B
103
+ : DINO_JUMP;
104
+ const feetRow = groundIndex - Math.max(0, Math.floor(state.dinoY));
105
+ for (let dy = 0; dy < sprite.length; dy++) {
106
+ const targetRow = feetRow - (sprite.length - 1 - dy);
107
+ if (targetRow < 0 || targetRow >= PLAYFIELD_ROWS)
108
+ continue;
109
+ const line = sprite[dy];
110
+ for (let dx = 0; dx < line.length; dx++) {
111
+ const ch = line[dx];
112
+ if (ch === " ")
113
+ continue; // sprite holes leave the playfield intact
114
+ const col = DINO_X + dx;
115
+ if (col < 0 || col >= width)
116
+ continue;
117
+ rows[targetRow][col] = ch;
118
+ }
119
+ }
120
+ return rows.map((r) => r.join(""));
121
+ }
122
+ function renderOverlay(state) {
123
+ if (state.phase.kind === "countdown") {
124
+ // Big numeral derived from ticksRemaining. The tick layer counts down
125
+ // in whole-second blocks of `ticksPerSecond` so dividing reproduces 3
126
+ // → 2 → 1 cleanly regardless of the actual cadence.
127
+ const seconds = Math.ceil(state.phase.ticksRemaining / 15);
128
+ return bigNumeral(seconds);
129
+ }
130
+ if (state.phase.kind === "frozen") {
131
+ return ["agent needs you", "answer the prompt above"];
132
+ }
133
+ if (state.phase.kind === "gameover") {
134
+ return ["game over", `score ${Math.floor(state.score)}`];
135
+ }
136
+ return undefined;
137
+ }
138
+ function overlayRows(rows, overlay) {
139
+ const width = rows[0]?.length ?? 0;
140
+ const startRow = Math.max(0, Math.floor((rows.length - overlay.length) / 2));
141
+ for (let i = 0; i < overlay.length; i++) {
142
+ const target = startRow + i;
143
+ if (target >= rows.length)
144
+ break;
145
+ const line = overlay[i];
146
+ const startCol = Math.max(0, Math.floor((width - line.length) / 2));
147
+ const before = rows[target].slice(0, startCol);
148
+ const after = rows[target].slice(startCol + line.length);
149
+ rows[target] = (before + line + after).slice(0, width);
150
+ }
151
+ }
152
+ function bigNumeral(n) {
153
+ // Stylized but ASCII-only so it carries the 8-bit feel without leaning
154
+ // on Unicode block characters that render at inconsistent widths.
155
+ const label = n > 0 ? String(n) : "GO!";
156
+ return [`>>> ${label} <<<`];
157
+ }
158
+ function pad4(n) {
159
+ return n.toString().padStart(4, "0");
160
+ }
161
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../../../src/tui/dino/render.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,uCAAuC;AAEvC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAkB,MAAM,YAAY,CAAC;AAEhE;;;6CAG6C;AAC7C,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAChC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAChC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC;AAC5B;yEACyE;AACzE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B,kEAAkE;AAClE,uEAAuE;AACvE,sEAAsE;AACtE,oEAAoE;AACpE,EAAE;AACF,oEAAoE;AACpE,oEAAoE;AACpE,oCAAoC;AACpC,MAAM,UAAU,GAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAChE,MAAM,UAAU,GAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAChE,MAAM,SAAS,GAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE9D;;qDAEqD;AACrD,MAAM,UAAU,cAAc,CAAC,KAAgB,EAAE,cAAuB,IAAI;IAC1E,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACjC,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,OAAO,EAAE,CAAC;QACZ,oEAAoE;QACpE,qEAAqE;QACrE,cAAc;QACd,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,CAAC,KAAK,EAAE,GAAG,IAAI,EAAE,SAAS,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,WAAW,CAAC,KAAgB;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,sBAAsB,EAAE,QAAQ,KAAK,EAAE,CAAC;AACjD,CAAC;AAED,SAAS,eAAe,CAAC,KAAgB,EAAE,WAAoB;IAC7D,mEAAmE;IACnE,gEAAgE;IAChE,mEAAmE;IACnE,4BAA4B;IAC5B,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,4DAA4D,CAAC;IACtE,CAAC;IACD,QAAQ,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACzB,KAAK,MAAM;YACT,OAAO,+CAA+C,CAAC;QACzD,KAAK,SAAS;YACZ,OAAO,wCAAwC,CAAC;QAClD,KAAK,OAAO;YACV,OAAO,KAAK,CAAC;QACf,KAAK,WAAW;YACd,OAAO,aAAa,CAAC;QACvB,KAAK,QAAQ;YACX,OAAO,oCAAoC,CAAC;QAC9C,KAAK,UAAU;YACb,OAAO,uDAAuD,CAAC;IACnE,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,KAAgB;IACtC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC;IAC/B,MAAM,IAAI,GAAe,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,CACnE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CACzC,CAAC;IACF,eAAe;IACf,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,cAAc,GAAG,CAAC,CAAC,CAAC;IAC7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;IAE3D,wEAAwE;IACxE,qEAAqE;IACrE,mBAAmB;IACnB,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,IAAI,KAAK;YAAE,SAAS;QACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAClC,MAAM,GAAG,GAAG,WAAW,GAAG,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,GAAG,IAAI,CAAC;gBAAE,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACrC,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,sEAAsE;IACtE,kEAAkE;IAClE,mEAAmE;IACnE,iEAAiE;IACjE,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,KAAK,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,QAAQ;QACrB,CAAC,CAAC,8DAA8D;YAC9D,iDAAiD;YACjD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;gBACrC,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,UAAU;QACd,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,OAAO,GAAG,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACnE,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,OAAO,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;QACrD,IAAI,SAAS,GAAG,CAAC,IAAI,SAAS,IAAI,cAAc;YAAE,SAAS;QAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;QACxB,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;YACxC,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;YACpB,IAAI,EAAE,KAAK,GAAG;gBAAE,SAAS,CAAC,0CAA0C;YACpE,MAAM,GAAG,GAAG,MAAM,GAAG,EAAE,CAAC;YACxB,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,IAAI,KAAK;gBAAE,SAAS;YACtC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,aAAa,CAAC,KAAgB;IACrC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACrC,sEAAsE;QACtE,sEAAsE;QACtE,oDAAoD;QACpD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,cAAc,GAAG,EAAE,CAAC,CAAC;QAC3D,OAAO,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,CAAC,iBAAiB,EAAE,yBAAyB,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,CAAC,WAAW,EAAE,SAAS,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,WAAW,CAAC,IAAc,EAAE,OAAiB;IACpD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,QAAQ,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,IAAI,IAAI,CAAC,MAAM;YAAE,MAAM;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;QACzD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACzD,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,uEAAuE;IACvE,kEAAkE;IAClE,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACxC,OAAO,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,IAAI,CAAC,CAAS;IACrB,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACvC,CAAC"}