npcsh 1.1.20__py3-none-any.whl → 1.1.22__py3-none-any.whl

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 (186) hide show
  1. npcsh/_state.py +15 -76
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/diff_viewer.py +3 -3
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +12 -6
  7. npcsh/npc_team/corca.npc +0 -1
  8. npcsh/npc_team/frederic.npc +2 -3
  9. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  11. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  12. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  13. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  14. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  15. npcsh/npc_team/jinxs/{bin → lib/utils}/jinxs.jinx +12 -12
  16. npcsh/npc_team/jinxs/{bin → lib/utils}/models.jinx +7 -7
  17. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +6 -6
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +1633 -295
  19. npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  20. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  21. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  22. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  23. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  24. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  25. {npcsh-1.1.20.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/kg.jinx +82 -15
  26. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  27. npcsh/npc_team/jinxs/{bin → modes}/nql.jinx +10 -21
  28. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  29. npcsh/npc_team/jinxs/modes/plonk.jinx +503 -308
  30. npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  31. npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  32. npcsh/npc_team/jinxs/{bin → modes}/team.jinx +12 -12
  33. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  34. npcsh/npc_team/jinxs/modes/wander.jinx +454 -181
  35. npcsh/npc_team/jinxs/modes/yap.jinx +630 -182
  36. npcsh/npc_team/kadiefa.npc +2 -1
  37. npcsh/npc_team/sibiji.npc +3 -3
  38. npcsh/npcsh.py +112 -47
  39. npcsh/routes.py +4 -1
  40. npcsh/salmon_simulation.py +0 -0
  41. npcsh-1.1.22.data/data/npcsh/npc_team/alicanto.jinx +1694 -0
  42. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  43. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +5 -5
  44. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  45. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  46. npcsh-1.1.22.data/data/npcsh/npc_team/compress.jinx +428 -0
  47. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  48. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  49. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  50. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +17 -6
  51. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  52. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +17 -6
  53. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  54. npcsh-1.1.22.data/data/npcsh/npc_team/git.jinx +795 -0
  55. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +12 -12
  56. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  57. {npcsh/npc_team/jinxs/bin → npcsh-1.1.22.data/data/npcsh/npc_team}/kg.jinx +82 -15
  58. npcsh-1.1.22.data/data/npcsh/npc_team/memories.jinx +414 -0
  59. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +7 -7
  60. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +10 -21
  61. npcsh-1.1.22.data/data/npcsh/npc_team/papers.jinx +578 -0
  62. npcsh-1.1.22.data/data/npcsh/npc_team/plonk.jinx +574 -0
  63. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +3 -3
  64. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +6 -6
  65. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  66. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +3 -3
  67. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +12 -12
  68. npcsh-1.1.22.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  69. npcsh-1.1.22.data/data/npcsh/npc_team/wander.jinx +728 -0
  70. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +52 -14
  71. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  72. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  73. npcsh-1.1.22.dist-info/RECORD +240 -0
  74. npcsh-1.1.22.dist-info/entry_points.txt +11 -0
  75. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  76. npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  77. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  78. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -418
  79. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  80. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  81. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  82. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  83. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  84. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  85. npcsh/npc_team/plonkjr.npc +0 -23
  86. npcsh-1.1.20.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  87. npcsh-1.1.20.data/data/npcsh/npc_team/build.jinx +0 -65
  88. npcsh-1.1.20.data/data/npcsh/npc_team/compress.jinx +0 -140
  89. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +0 -300
  90. npcsh-1.1.20.data/data/npcsh/npc_team/corca.jinx +0 -430
  91. npcsh-1.1.20.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  92. npcsh-1.1.20.data/data/npcsh/npc_team/kg_search.jinx +0 -418
  93. npcsh-1.1.20.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  94. npcsh-1.1.20.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  95. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +0 -317
  96. npcsh-1.1.20.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  97. npcsh-1.1.20.data/data/npcsh/npc_team/plonk.jinx +0 -379
  98. npcsh-1.1.20.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  99. npcsh-1.1.20.data/data/npcsh/npc_team/search.jinx +0 -54
  100. npcsh-1.1.20.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  101. npcsh-1.1.20.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  102. npcsh-1.1.20.data/data/npcsh/npc_team/wander.jinx +0 -455
  103. npcsh-1.1.20.data/data/npcsh/npc_team/yap.jinx +0 -268
  104. npcsh-1.1.20.dist-info/RECORD +0 -248
  105. npcsh-1.1.20.dist-info/entry_points.txt +0 -25
  106. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  107. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  108. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  109. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  110. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  111. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  112. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  113. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  114. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  115. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  116. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  117. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  118. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  119. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  120. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  121. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  122. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  123. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  124. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  125. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  126. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  127. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  128. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  129. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  130. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  131. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  132. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  133. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  134. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  135. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  136. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  137. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  138. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  139. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  140. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  141. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  142. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  143. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  144. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  145. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  146. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  147. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  148. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  149. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  150. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  151. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  152. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  153. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  154. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  156. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  157. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  158. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  159. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  160. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  161. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  162. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  163. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  164. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  165. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  166. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  167. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  168. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  169. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  170. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  171. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  172. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  173. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  174. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  175. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  181. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  182. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  183. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  184. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  185. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  186. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,728 @@
1
+ jinx_name: wander
2
+ description: Interactive wandering mode - creative exploration with live TUI dashboard
3
+ interactive: true
4
+ inputs:
5
+ - problem: null
6
+ - environment: null
7
+ - low_temp: 0.5
8
+ - high_temp: 1.9
9
+ - n_min: 30
10
+ - n_max: 150
11
+ - interruption_likelihood: 0.1
12
+ - sample_rate: 0.5
13
+ - n_high_temp_streams: 5
14
+ - include_events: true
15
+ - num_events: 3
16
+ - model: null
17
+ - provider: null
18
+
19
+ steps:
20
+ - name: wander_interactive
21
+ engine: python
22
+ code: |
23
+ import os
24
+ import sys
25
+ import tty
26
+ import termios
27
+ import random
28
+ import threading
29
+ import time
30
+ import textwrap
31
+ from datetime import datetime
32
+ from termcolor import colored
33
+
34
+ from npcpy.llm_funcs import get_llm_response
35
+
36
+ npc = context.get('npc')
37
+ team = context.get('team')
38
+ messages = context.get('messages', [])
39
+
40
+ problem = context.get('problem')
41
+ environment = context.get('environment')
42
+ low_temp = float(context.get('low_temp') or 0.5)
43
+ high_temp = float(context.get('high_temp') or 1.9)
44
+ n_min = int(context.get('n_min') or 30)
45
+ n_max = int(context.get('n_max') or 150)
46
+ interruption_likelihood = float(context.get('interruption_likelihood') or 0.1)
47
+ sample_rate = float(context.get('sample_rate') or 0.5)
48
+ n_high_temp_streams = int(context.get('n_high_temp_streams') or 5)
49
+ include_events = bool(context.get('include_events', True))
50
+ num_events = int(context.get('num_events') or 3)
51
+
52
+ # Resolve npc
53
+ if isinstance(npc, str) and team:
54
+ npc = team.get(npc) if hasattr(team, 'get') else None
55
+ elif isinstance(npc, str):
56
+ npc = None
57
+
58
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
59
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
60
+
61
+ # ========== Problem Entry ==========
62
+ if not problem:
63
+ if not sys.stdin.isatty():
64
+ context['output'] = "Wander requires an interactive terminal or a problem argument."
65
+ context['messages'] = messages
66
+ exit()
67
+ print("\033[1;35m WANDER - Creative Exploration \033[0m")
68
+ print("\033[90mEnter a problem or question to explore (or 'q' to quit):\033[0m")
69
+ try:
70
+ problem = input("\033[33m> \033[0m").strip()
71
+ except (EOFError, KeyboardInterrupt):
72
+ problem = ""
73
+ if not problem or problem.lower() == 'q':
74
+ context['output'] = "Wander cancelled."
75
+ context['messages'] = messages
76
+ exit()
77
+
78
+ # ========== State ==========
79
+ class WanderState:
80
+ def __init__(self):
81
+ self.environment = ""
82
+ self.streams = [] # [{temp, insight, words, samples, event, starred, timestamp}]
83
+ self.starred = []
84
+ self.current_temp = high_temp
85
+ self.scroll_offset = 0
86
+ self.current_panel = 0 # 0=main, 1=streams, 2=starred, 3=review
87
+ self.status = "Ready"
88
+ self.generating = False
89
+ self.last_output = ""
90
+ self.all_samples = [] # collected word samples across streams
91
+ self.events = [] # [{type, text}]
92
+ self.quit_requested = False
93
+ self.auto_done = False
94
+ # Review mode
95
+ self.review_mode = False
96
+ self.review_items = [] # [{idx, text_preview, selected}]
97
+ self.review_cursor = 0
98
+ self.review_scroll = 0
99
+ self.review_sample_count = 0
100
+ self.editing_count = False
101
+ self.count_buf = ""
102
+
103
+ state = WanderState()
104
+
105
+ # ========== Event Weights ==========
106
+ EVENT_WEIGHTS = {
107
+ 'encounter': 0.20,
108
+ 'discovery': 0.20,
109
+ 'obstacle': 0.15,
110
+ 'insight': 0.20,
111
+ 'shift': 0.10,
112
+ 'memory': 0.15,
113
+ }
114
+
115
+ def weighted_event_type():
116
+ types = list(EVENT_WEIGHTS.keys())
117
+ weights = list(EVENT_WEIGHTS.values())
118
+ return random.choices(types, weights=weights, k=1)[0]
119
+
120
+ # ========== Original Algorithm Helpers ==========
121
+ def truncate_to_word_count(text, n):
122
+ words = text.split()
123
+ if len(words) <= n:
124
+ return text
125
+ return ' '.join(words[:n])
126
+
127
+ def subsample_words(text, k=20):
128
+ words = text.split()
129
+ if len(words) <= k:
130
+ return words
131
+ return random.sample(words, k)
132
+
133
+ def extract_samples(text, rate):
134
+ words = text.split()
135
+ n_sample = max(1, int(len(words) * rate))
136
+ if len(words) <= n_sample:
137
+ return words
138
+ return random.sample(words, n_sample)
139
+
140
+ # ========== TUI Helpers ==========
141
+ def get_size():
142
+ try:
143
+ s = os.get_terminal_size()
144
+ return s.columns, s.lines
145
+ except:
146
+ return 80, 24
147
+
148
+ def wrap_text(text, width):
149
+ lines = []
150
+ for line in text.split('\n'):
151
+ if len(line) <= width:
152
+ lines.append(line)
153
+ else:
154
+ lines.extend(textwrap.wrap(line, width) or [''])
155
+ return lines
156
+
157
+ def draw_box(x, y, w, h, title="", color="\033[90m"):
158
+ out = []
159
+ if title:
160
+ title_part = f" {title} "
161
+ border = "─" * ((w - len(title_part) - 2) // 2)
162
+ top = f"┌{border}{title_part}{border}{'─' * ((w - len(title_part) - 2) % 2)}┐"
163
+ else:
164
+ top = "┌" + "─" * (w - 2) + "┐"
165
+ out.append(f"\033[{y};{x}H{color}{top}\033[0m")
166
+ for i in range(1, h - 1):
167
+ out.append(f"\033[{y+i};{x}H{color}│\033[0m")
168
+ out.append(f"\033[{y+i};{x+w-1}H{color}│\033[0m")
169
+ out.append(f"\033[{y+h-1};{x}H{color}└{'─' * (w - 2)}┘\033[0m")
170
+ return ''.join(out)
171
+
172
+ def render_screen():
173
+ width, height = get_size()
174
+ out = []
175
+ out.append("\033[2J\033[H")
176
+
177
+ # ===== HEADER =====
178
+ prob_display = problem[:width-20] + "..." if len(problem) > width-20 else problem
179
+ header = f" WANDER - {prob_display} "
180
+ temp_info = f"\033[35m[T={state.current_temp:.1f}]\033[0m"
181
+ status_color = "\033[33m" if state.generating else "\033[32m"
182
+ status_info = f"{status_color}[{state.status}]\033[0m"
183
+
184
+ out.append(f"\033[1;1H\033[7;1m{header.ljust(width)}\033[0m")
185
+ out.append(f"\033[1;{width-35}H{temp_info} {status_info}")
186
+
187
+ if state.review_mode:
188
+ render_review(out, width, height)
189
+ else:
190
+ render_explore(out, width, height)
191
+
192
+ sys.stdout.write(''.join(out))
193
+ sys.stdout.flush()
194
+
195
+ def render_explore(out, width, height):
196
+ left_w = max(25, width // 3)
197
+ right_w = width - left_w - 1
198
+ panel_h = height - 4
199
+
200
+ # Left: Environment
201
+ out.append(draw_box(1, 3, left_w, panel_h // 2, "Environment", "\033[36m"))
202
+ env_lines = wrap_text(state.environment or "Generating...", left_w - 4)
203
+ for i, line in enumerate(env_lines[:panel_h // 2 - 3]):
204
+ out.append(f"\033[{4+i};3H{line[:left_w-4]}")
205
+
206
+ # Left: Controls
207
+ ctrl_y = 3 + panel_h // 2
208
+ out.append(draw_box(1, ctrl_y, left_w, panel_h // 2, "Controls", "\033[33m"))
209
+ controls = [
210
+ "SPACE - New stream",
211
+ f"t - Temp: {state.current_temp:.1f}",
212
+ "e - Trigger event",
213
+ "s - Star insight",
214
+ "R - Review & synthesize",
215
+ "Tab - Switch panel",
216
+ "j/k - Scroll",
217
+ "q - Quit",
218
+ "",
219
+ f"Streams: {len(state.streams)}",
220
+ f"Samples: {len(state.all_samples)}",
221
+ f"Starred: {len(state.starred)}",
222
+ ]
223
+ for i, ctrl in enumerate(controls[:panel_h // 2 - 3]):
224
+ out.append(f"\033[{ctrl_y+1+i};3H\033[90m{ctrl[:left_w-4]}\033[0m")
225
+
226
+ # Right panel
227
+ panel_titles = ["Output", "Stream History", "Starred Insights"]
228
+ panel_idx = min(state.current_panel, 2)
229
+ panel_color = ["\033[37m", "\033[36m", "\033[33m"][panel_idx]
230
+ out.append(draw_box(left_w + 1, 3, right_w, panel_h, panel_titles[panel_idx], panel_color))
231
+
232
+ content_w = right_w - 4
233
+ content_h = panel_h - 3
234
+ content_lines = []
235
+
236
+ if panel_idx == 0:
237
+ if state.last_output:
238
+ content_lines = wrap_text(state.last_output, content_w)
239
+ else:
240
+ content_lines = ["", " Waiting for streams..."]
241
+
242
+ elif panel_idx == 1:
243
+ for i, stream in enumerate(reversed(state.streams)):
244
+ starred_mark = "★ " if stream.get('starred') else " "
245
+ hdr = f"{starred_mark}\033[35mStream {len(state.streams)-i} (T={stream.get('temp', 0):.1f}, {len(stream.get('words', []))}w)\033[0m"
246
+ content_lines.append(hdr)
247
+ preview = stream.get('insight', '')[:100].replace('\n', ' ')
248
+ content_lines.append(f" {preview}...")
249
+ if stream.get('samples'):
250
+ content_lines.append(f" \033[90mSamples: {' '.join(stream['samples'][:8])}...\033[0m")
251
+ content_lines.append("")
252
+
253
+ elif panel_idx == 2:
254
+ if not state.starred:
255
+ content_lines = ["", " No starred insights yet.", "", " Press 's' to star current stream."]
256
+ else:
257
+ for i, item in enumerate(state.starred):
258
+ content_lines.append(f"★ {i+1}. [T={item.get('temp', 0):.1f}]")
259
+ for line in wrap_text(item.get('insight', ''), content_w - 4):
260
+ content_lines.append(f" {line}")
261
+ content_lines.append("")
262
+
263
+ visible = content_lines[state.scroll_offset:state.scroll_offset + content_h]
264
+ for i, line in enumerate(visible):
265
+ out.append(f"\033[{4+i};{left_w+3}H{line[:content_w]}")
266
+
267
+ if len(content_lines) > content_h:
268
+ scroll_pct = state.scroll_offset / max(1, len(content_lines) - content_h)
269
+ indicator_pos = int(scroll_pct * (content_h - 1))
270
+ out.append(f"\033[{4+indicator_pos};{width-1}H\033[33m▐\033[0m")
271
+
272
+ # Footer
273
+ panel_tabs = ""
274
+ for i, name in enumerate(["Output", "Streams", "Starred"]):
275
+ if i == panel_idx:
276
+ panel_tabs += f"\033[7m {name} \033[0m "
277
+ else:
278
+ panel_tabs += f"\033[90m {name} \033[0m "
279
+
280
+ out.append(f"\033[{height-1};1H\033[90m{'─' * width}\033[0m")
281
+ out.append(f"\033[{height};1H{panel_tabs}")
282
+
283
+ def render_review(out, width, height):
284
+ panel_h = height - 4
285
+ out.append(draw_box(1, 3, width, panel_h, "Review Insights", "\033[33m"))
286
+
287
+ content_w = width - 6
288
+ content_h = panel_h - 5
289
+
290
+ # Sample count control at top
291
+ if state.editing_count:
292
+ count_line = f" Insights to sample: \033[7m {state.count_buf} \033[0m (Enter to confirm, Esc to cancel)"
293
+ else:
294
+ count_line = f" Insights to sample: \033[1m{state.review_sample_count}\033[0m / {len(state.review_items)} (n to change)"
295
+ out.append(f"\033[4;3H{count_line[:content_w]}")
296
+ out.append(f"\033[5;3H\033[90m{'─' * (content_w)}\033[0m")
297
+
298
+ # Items list
299
+ for i in range(content_h):
300
+ idx = state.review_scroll + i
301
+ row = 6 + i
302
+ out.append(f"\033[{row};3H\033[K")
303
+ if idx >= len(state.review_items):
304
+ continue
305
+ item = state.review_items[idx]
306
+ check = "\033[32m[x]\033[0m" if item['selected'] else "\033[90m[ ]\033[0m"
307
+ cursor = "\033[7m>" if idx == state.review_cursor else " "
308
+ text = item['text_preview'][:content_w - 10]
309
+ if idx == state.review_cursor:
310
+ out.append(f"{cursor} {check} {text}\033[0m")
311
+ else:
312
+ out.append(f" {check} {text}")
313
+
314
+ # Footer
315
+ out.append(f"\033[{height-1};1H\033[90m{'─' * width}\033[0m")
316
+ footer = " j/k:Nav SPACE:Toggle a:All x:None n:Count Enter:Synthesize Esc:Back "
317
+ out.append(f"\033[{height};1H\033[7m{footer.ljust(width)}\033[0m")
318
+
319
+ # ========== Actions ==========
320
+ def generate_environment():
321
+ state.status = "Generating environment..."
322
+ state.generating = True
323
+
324
+ env_prompt = f"""Create a vivid, metaphorical environment for wandering through while exploring:
325
+ "{problem}"
326
+
327
+ The environment should:
328
+ 1. Have distinct regions that map to aspects of the problem
329
+ 2. Include sensory details (sights, sounds, textures)
330
+ 3. Feel alive and explorable
331
+ 4. Be described in 4-6 evocative sentences
332
+
333
+ Respond with only the description."""
334
+
335
+ resp = get_llm_response(env_prompt, model=model, provider=provider, temperature=0.8, npc=npc)
336
+ state.environment = str(resp.get('response', 'A vast conceptual landscape stretches before you.'))
337
+ state.status = "Ready"
338
+ state.generating = False
339
+
340
+ def run_one_stream():
341
+ """Original algorithm: generate with random word-count cutoff, subsample, extract samples."""
342
+ # Alternate temperature
343
+ if len(state.streams) % 2 == 0:
344
+ state.current_temp = low_temp
345
+ is_high = False
346
+ else:
347
+ state.current_temp = high_temp
348
+ is_high = True
349
+
350
+ temp = state.current_temp
351
+ word_limit = random.randint(n_min, n_max)
352
+ state.status = f"Stream {len(state.streams)+1} (T={temp:.1f}, ~{word_limit}w)..."
353
+ state.generating = True
354
+
355
+ # Build prompt
356
+ if is_high:
357
+ # High-temp: use subsample seeds from previous stream
358
+ seeds = []
359
+ if state.streams:
360
+ prev = state.streams[-1]
361
+ seeds = subsample_words(prev.get('insight', ''), k=20)
362
+ seed_text = ' '.join(seeds) if seeds else problem
363
+
364
+ sys_prompt = "Just generate without thinking. Let words and ideas flow freely. Do not filter or organize."
365
+ wander_prompt = f"""Seeds: {seed_text}
366
+
367
+ Context: {state.environment or 'a conceptual landscape'}
368
+ Problem: "{problem}"
369
+
370
+ Generate freely from these seeds. Follow any tangent. No structure needed."""
371
+ else:
372
+ # Low-temp: focused exploration
373
+ recent = state.streams[-3:] if state.streams else []
374
+ recent_context = "\n".join([s.get('insight', '')[:200] for s in recent]) if recent else "Starting fresh"
375
+
376
+ sys_prompt = None
377
+ wander_prompt = f"""You are wandering through: {state.environment or 'a conceptual landscape'}
378
+
379
+ Problem: "{problem}"
380
+
381
+ Recent thoughts: {recent_context}
382
+
383
+ In this exploration:
384
+ - Let associations flow freely
385
+ - Notice unexpected connections
386
+ - Follow interesting tangents
387
+ - Share what emerges
388
+
389
+ Respond naturally, 2-4 paragraphs."""
390
+
391
+ try:
392
+ kwargs = dict(model=model, provider=provider, temperature=temp, npc=npc)
393
+ if sys_prompt:
394
+ kwargs['system_prompt'] = sys_prompt
395
+ resp = get_llm_response(wander_prompt, **kwargs)
396
+ insight = str(resp.get('response', ''))
397
+ except Exception as e:
398
+ insight = "Error: " + str(e)
399
+
400
+ # Probabilistic interruption: truncate at random word count
401
+ if random.random() < interruption_likelihood:
402
+ word_limit = random.randint(n_min // 2, n_min)
403
+ insight = truncate_to_word_count(insight, word_limit)
404
+
405
+ # Extract samples
406
+ words = insight.split()
407
+ samples = extract_samples(insight, sample_rate)
408
+ state.all_samples.extend(samples)
409
+
410
+ stream_entry = {
411
+ 'temp': temp,
412
+ 'insight': insight,
413
+ 'words': words,
414
+ 'samples': samples,
415
+ 'event': None,
416
+ 'starred': False,
417
+ 'timestamp': datetime.now().isoformat()
418
+ }
419
+ state.streams.append(stream_entry)
420
+ state.last_output = insight
421
+
422
+ # After high-temp streams, maybe inject an event
423
+ if is_high and include_events and len(state.events) < num_events:
424
+ if random.random() < 0.3:
425
+ _trigger_event_sync()
426
+
427
+ state.scroll_offset = 0
428
+ state.generating = False
429
+ state.status = f"Ready. {len(state.streams)} streams, {len(state.all_samples)} samples"
430
+
431
+ def _trigger_event_sync():
432
+ event_type = weighted_event_type()
433
+ event_prompt = f"""In the environment: {state.environment or 'a conceptual landscape'}
434
+ While exploring "{problem}", a {event_type} occurs.
435
+
436
+ Describe this {event_type} in 2-3 vivid sentences.
437
+ Make it metaphorical and thought-provoking."""
438
+
439
+ try:
440
+ resp = get_llm_response(event_prompt, model=model, provider=provider, temperature=1.0, npc=npc)
441
+ event_text = str(resp.get('response', ''))
442
+ except Exception as e:
443
+ event_text = f"A {event_type} occurred but faded before you could grasp it."
444
+
445
+ state.events.append({'type': event_type, 'text': event_text})
446
+ if state.streams:
447
+ state.streams[-1]['event'] = {'type': event_type, 'text': event_text}
448
+ state.last_output = f"[{event_type.upper()}]\n\n{event_text}"
449
+
450
+ def trigger_event():
451
+ if state.generating:
452
+ return
453
+ state.status = "Event occurring..."
454
+ state.generating = True
455
+ _trigger_event_sync()
456
+ state.scroll_offset = 0
457
+ state.status = "Ready"
458
+ state.generating = False
459
+
460
+ def toggle_temp():
461
+ if state.current_temp == high_temp:
462
+ state.current_temp = low_temp
463
+ else:
464
+ state.current_temp = high_temp
465
+
466
+ def star_current():
467
+ if state.streams and not state.streams[-1].get('starred'):
468
+ state.streams[-1]['starred'] = True
469
+ state.starred.append(state.streams[-1])
470
+ state.status = "★ Starred!"
471
+
472
+ def enter_review():
473
+ """Enter review mode where user selects insights for synthesis."""
474
+ if not state.streams:
475
+ state.status = "No streams to review"
476
+ return
477
+ state.review_mode = True
478
+ state.review_items = []
479
+ for i, s in enumerate(state.streams):
480
+ preview = s.get('insight', '')[:120].replace('\n', ' ')
481
+ state.review_items.append({
482
+ 'idx': i,
483
+ 'text_preview': f"[T={s.get('temp', 0):.1f}] {preview}",
484
+ 'selected': s.get('starred', False),
485
+ })
486
+ # Default: select starred + random sample
487
+ n_select = max(1, min(len(state.review_items), int(len(state.review_items) * sample_rate)))
488
+ state.review_sample_count = n_select
489
+ # Auto-select starred ones, then fill randomly
490
+ selected_idxs = set()
491
+ for item in state.review_items:
492
+ if state.streams[item['idx']].get('starred'):
493
+ item['selected'] = True
494
+ selected_idxs.add(item['idx'])
495
+ remaining = [item for item in state.review_items if item['idx'] not in selected_idxs]
496
+ n_random = max(0, n_select - len(selected_idxs))
497
+ if remaining and n_random > 0:
498
+ for item in random.sample(remaining, min(n_random, len(remaining))):
499
+ item['selected'] = True
500
+ state.review_cursor = 0
501
+ state.review_scroll = 0
502
+
503
+ def synthesize_from_review():
504
+ """Synthesize using the selected review items."""
505
+ selected = [state.review_items[i] for i in range(len(state.review_items)) if state.review_items[i]['selected']]
506
+ if not selected:
507
+ state.status = "No insights selected"
508
+ return
509
+
510
+ state.review_mode = False
511
+ state.status = "Synthesizing..."
512
+ state.generating = True
513
+
514
+ selected_insights = []
515
+ for item in selected:
516
+ idx = item['idx']
517
+ selected_insights.append(state.streams[idx].get('insight', ''))
518
+
519
+ # Collect samples from selected streams
520
+ selected_samples = []
521
+ for item in selected:
522
+ idx = item['idx']
523
+ selected_samples.extend(state.streams[idx].get('samples', []))
524
+
525
+ sample_text = ' '.join(selected_samples[:50]) if selected_samples else 'N/A'
526
+
527
+ synth_prompt = f"""You are synthesizing a wandering exploration of: "{problem}"
528
+
529
+ Environment traversed: {state.environment or 'a conceptual landscape'}
530
+
531
+ Selected explorations ({len(selected)} of {len(state.streams)} streams):
532
+ {"---".join(selected_insights)}
533
+
534
+ Word samples extracted during wandering: {sample_text}
535
+
536
+ Events encountered: {'; '.join([e['type'] + ': ' + e['text'][:100] for e in state.events]) if state.events else 'None'}
537
+
538
+ From these wanderings, synthesize creative hypotheses:
539
+ 1. What unexpected patterns emerge from the word samples?
540
+ 2. What creative hypotheses can you form by connecting disparate ideas?
541
+ 3. What questions have emerged that weren't visible at the start?
542
+ 4. What surprising connections exist between the different temperature explorations?
543
+
544
+ Be bold, creative, and insightful. These hypotheses should feel like discoveries, not summaries."""
545
+
546
+ try:
547
+ resp = get_llm_response(synth_prompt, model=model, provider=provider, temperature=0.4, npc=npc)
548
+ synthesis = str(resp.get('response', ''))
549
+ except Exception as e:
550
+ synthesis = "Error during synthesis: " + str(e)
551
+
552
+ state.last_output = "=== SYNTHESIS ===\n\n" + synthesis
553
+ state.scroll_offset = 0
554
+ state.current_panel = 0
555
+ state.status = "Ready"
556
+ state.generating = False
557
+
558
+ # ========== Auto-run ==========
559
+ def auto_run():
560
+ if not environment:
561
+ generate_environment()
562
+ else:
563
+ state.environment = environment
564
+
565
+ total_streams = n_high_temp_streams * 2 # low + high alternating
566
+ for i in range(total_streams):
567
+ if state.quit_requested:
568
+ break
569
+ run_one_stream()
570
+
571
+ if not state.quit_requested:
572
+ state.auto_done = True
573
+ state.status = f"Done! {len(state.streams)} streams. R=Review, SPACE=more, q=quit"
574
+
575
+ # ========== Main Loop ==========
576
+ fd = sys.stdin.fileno()
577
+ old_settings = termios.tcgetattr(fd)
578
+ import select as _sel
579
+
580
+ try:
581
+ tty.setcbreak(fd)
582
+ sys.stdout.write('\033[?25l')
583
+ sys.stdout.flush()
584
+
585
+ render_screen()
586
+
587
+ auto_thread = threading.Thread(target=auto_run, daemon=True)
588
+ auto_thread.start()
589
+
590
+ while True:
591
+ if _sel.select([fd], [], [], 0.3)[0]:
592
+ c = os.read(fd, 1).decode('latin-1')
593
+
594
+ if state.review_mode:
595
+ # Review mode input
596
+ if state.editing_count:
597
+ if c in ('\r', '\n'):
598
+ try:
599
+ val = int(state.count_buf)
600
+ state.review_sample_count = max(0, min(val, len(state.review_items)))
601
+ except:
602
+ pass
603
+ state.editing_count = False
604
+ elif c == '\x1b':
605
+ state.editing_count = False
606
+ elif c == '\x7f' or c == '\x08':
607
+ state.count_buf = state.count_buf[:-1]
608
+ elif c.isdigit():
609
+ state.count_buf += c
610
+ else:
611
+ if c == '\x1b':
612
+ if _sel.select([fd], [], [], 0.05)[0]:
613
+ c2 = os.read(fd, 1).decode('latin-1')
614
+ if c2 == '[':
615
+ c3 = os.read(fd, 1).decode('latin-1')
616
+ if c3 == 'A':
617
+ state.review_cursor = max(0, state.review_cursor - 1)
618
+ elif c3 == 'B':
619
+ state.review_cursor = min(len(state.review_items) - 1, state.review_cursor + 1)
620
+ else:
621
+ state.review_mode = False
622
+ elif c == 'j':
623
+ state.review_cursor = min(len(state.review_items) - 1, state.review_cursor + 1)
624
+ elif c == 'k':
625
+ state.review_cursor = max(0, state.review_cursor - 1)
626
+ elif c == ' ':
627
+ if state.review_items:
628
+ state.review_items[state.review_cursor]['selected'] = not state.review_items[state.review_cursor]['selected']
629
+ elif c == 'a':
630
+ for item in state.review_items:
631
+ item['selected'] = True
632
+ elif c == 'x':
633
+ for item in state.review_items:
634
+ item['selected'] = False
635
+ elif c == 'n':
636
+ state.editing_count = True
637
+ state.count_buf = str(state.review_sample_count)
638
+ elif c in ('\r', '\n'):
639
+ threading.Thread(target=synthesize_from_review, daemon=True).start()
640
+ elif c == 'q':
641
+ state.quit_requested = True
642
+ break
643
+ # Keep cursor in scroll view
644
+ _, h = get_size()
645
+ view_h = h - 9
646
+ if state.review_cursor < state.review_scroll:
647
+ state.review_scroll = state.review_cursor
648
+ elif state.review_cursor >= state.review_scroll + view_h:
649
+ state.review_scroll = state.review_cursor - view_h + 1
650
+ else:
651
+ # Explore mode input
652
+ if c == 'q' or c == '\x03':
653
+ state.quit_requested = True
654
+ break
655
+ elif c == ' ':
656
+ if not state.generating:
657
+ threading.Thread(target=run_one_stream, daemon=True).start()
658
+ elif c == 't':
659
+ toggle_temp()
660
+ elif c == 'e':
661
+ if not state.generating:
662
+ threading.Thread(target=trigger_event, daemon=True).start()
663
+ elif c == 's':
664
+ star_current()
665
+ elif c == 'R' or c == 'r':
666
+ if not state.generating:
667
+ enter_review()
668
+ elif c == '\t':
669
+ state.current_panel = (state.current_panel + 1) % 3
670
+ state.scroll_offset = 0
671
+ elif c == 'j':
672
+ state.scroll_offset += 1
673
+ elif c == 'k':
674
+ state.scroll_offset = max(0, state.scroll_offset - 1)
675
+ elif c == '\x1b':
676
+ if _sel.select([fd], [], [], 0.05)[0]:
677
+ c2 = os.read(fd, 1).decode('latin-1')
678
+ if c2 == '[':
679
+ c3 = os.read(fd, 1).decode('latin-1')
680
+ if c3 == 'A':
681
+ state.scroll_offset = max(0, state.scroll_offset - 1)
682
+ elif c3 == 'B':
683
+ state.scroll_offset += 1
684
+ elif c3 == 'C':
685
+ state.current_panel = (state.current_panel + 1) % 3
686
+ state.scroll_offset = 0
687
+ elif c3 == 'D':
688
+ state.current_panel = (state.current_panel - 1) % 3
689
+ state.scroll_offset = 0
690
+ else:
691
+ state.quit_requested = True
692
+ break
693
+
694
+ render_screen()
695
+
696
+ finally:
697
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
698
+ sys.stdout.write('\033[?25h')
699
+ sys.stdout.write('\033[2J\033[H')
700
+ sys.stdout.flush()
701
+
702
+ if state.streams:
703
+ print(colored("=== WANDER SESSION COMPLETE ===\n", "green"))
704
+ print(f"Problem: {problem}")
705
+ print(f"Streams: {len(state.streams)}")
706
+ print(f"Samples: {len(state.all_samples)}")
707
+ print(f"Starred: {len(state.starred)}")
708
+ print(f"Events: {len(state.events)}\n")
709
+
710
+ if state.starred:
711
+ print(colored("Starred Insights:", "yellow"))
712
+ for i, s in enumerate(state.starred):
713
+ print(f"\n{i+1}. [T={s.get('temp', 0):.1f}] {s.get('insight', '')[:300]}...")
714
+
715
+ if state.last_output and "SYNTHESIS" in state.last_output:
716
+ print(colored("\n--- Final Synthesis ---", "cyan"))
717
+ print(state.last_output)
718
+
719
+ context['output'] = state.last_output
720
+ context['messages'] = messages
721
+ context['wander_result'] = {
722
+ 'problem': problem,
723
+ 'environment': state.environment,
724
+ 'streams': state.streams,
725
+ 'starred': state.starred,
726
+ 'events': state.events,
727
+ 'samples': state.all_samples,
728
+ }