npcsh 1.1.20__py3-none-any.whl → 1.1.21__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 (166) hide show
  1. npcsh/_state.py +5 -71
  2. npcsh/diff_viewer.py +3 -3
  3. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  4. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +19 -8
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/{bin → lib/utils}/jinxs.jinx +12 -12
  10. npcsh/npc_team/jinxs/{bin → lib/utils}/models.jinx +7 -7
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +6 -6
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +3 -3
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. {npcsh-1.1.20.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/kg.jinx +13 -13
  18. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  19. npcsh/npc_team/jinxs/{bin → modes}/nql.jinx +10 -21
  20. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  21. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  22. npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  23. npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  24. npcsh/npc_team/jinxs/{bin → modes}/team.jinx +12 -12
  25. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  26. npcsh/npc_team/jinxs/modes/wander.jinx +454 -181
  27. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  28. npcsh/npcsh.py +112 -47
  29. npcsh/routes.py +4 -1
  30. npcsh/salmon_simulation.py +0 -0
  31. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  32. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +5 -5
  33. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  34. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  35. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  36. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +3 -3
  37. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +17 -6
  38. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +17 -6
  39. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  40. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/jinxs.jinx +12 -12
  41. {npcsh/npc_team/jinxs/bin → npcsh-1.1.21.data/data/npcsh/npc_team}/kg.jinx +13 -13
  42. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +19 -8
  43. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  44. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/models.jinx +7 -7
  45. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/nql.jinx +10 -21
  46. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  48. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +3 -3
  49. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +6 -6
  50. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +3 -3
  51. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/team.jinx +12 -12
  52. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  53. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  54. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +52 -14
  55. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  56. {npcsh-1.1.20.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  57. {npcsh-1.1.20.dist-info → npcsh-1.1.21.dist-info}/RECORD +145 -150
  58. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  59. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  60. npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  61. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  62. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  63. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  64. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  65. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  66. npcsh/npc_team/plonkjr.npc +0 -23
  67. npcsh-1.1.20.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  68. npcsh-1.1.20.data/data/npcsh/npc_team/compress.jinx +0 -140
  69. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +0 -300
  70. npcsh-1.1.20.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  71. npcsh-1.1.20.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  72. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +0 -317
  73. npcsh-1.1.20.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  74. npcsh-1.1.20.data/data/npcsh/npc_team/plonk.jinx +0 -379
  75. npcsh-1.1.20.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  76. npcsh-1.1.20.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  77. npcsh-1.1.20.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  78. npcsh-1.1.20.data/data/npcsh/npc_team/wander.jinx +0 -455
  79. npcsh-1.1.20.dist-info/entry_points.txt +0 -25
  80. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  81. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  82. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  83. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  84. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  85. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  86. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  87. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  88. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  89. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  90. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  91. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  92. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  93. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  94. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  95. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  96. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  97. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  98. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  99. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  100. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  101. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  102. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  103. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  104. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  105. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  106. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  107. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  108. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +0 -0
  109. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  110. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  111. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  112. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  113. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  114. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  115. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  116. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  117. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  118. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  119. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  120. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  121. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  122. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  123. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  124. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  125. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  126. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  127. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  128. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  129. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  130. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  131. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +0 -0
  132. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  133. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  134. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  135. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  136. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  137. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  138. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  139. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  140. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  141. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  142. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  143. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  144. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  145. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  146. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  147. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  148. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  149. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  150. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  151. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  152. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  153. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  154. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  155. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  156. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  157. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  158. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  159. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  160. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  161. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  162. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  163. {npcsh-1.1.20.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  164. {npcsh-1.1.20.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  165. {npcsh-1.1.20.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  166. {npcsh-1.1.20.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,18 @@
1
1
  jinx_name: wander
2
2
  description: Interactive wandering mode - creative exploration with live TUI dashboard
3
+ interactive: true
3
4
  inputs:
4
5
  - problem: null
5
6
  - environment: null
6
- - low_temp: 0.3
7
- - high_temp: 1.5
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
8
16
  - model: null
9
17
  - provider: null
10
18
 
@@ -31,10 +39,17 @@ steps:
31
39
 
32
40
  problem = context.get('problem')
33
41
  environment = context.get('environment')
34
- low_temp = float(context.get('low_temp', 0.3))
35
- high_temp = float(context.get('high_temp', 1.5))
36
-
37
- # Resolve npc if it's a string (npc name) rather than NPC object
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
38
53
  if isinstance(npc, str) and team:
39
54
  npc = team.get(npc) if hasattr(team, 'get') else None
40
55
  elif isinstance(npc, str):
@@ -43,39 +58,85 @@ steps:
43
58
  model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
44
59
  provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
45
60
 
61
+ # ========== Problem Entry ==========
46
62
  if not problem:
47
- context['output'] = """Usage: /wander <problem to explore>
48
-
49
- Interactive TUI Controls:
50
- SPACE - Generate new wandering stream
51
- t - Toggle temperature (focused/creative/wild)
52
- e - Trigger random event
53
- s - Star current insight (save to favorites)
54
- r - Request reflection/synthesis
55
- j/k - Scroll through output
56
- Tab - Switch panels (Environment/Streams/Starred)
57
- q - Quit and show final synthesis
58
-
59
- Example: /wander How might we reimagine urban transportation?"""
60
- context['messages'] = messages
61
- exit()
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()
62
77
 
63
78
  # ========== State ==========
64
79
  class WanderState:
65
80
  def __init__(self):
66
81
  self.environment = ""
67
- self.streams = [] # List of {temp, mode, insight, event, starred, timestamp}
68
- self.starred = [] # Starred insights
69
- self.current_temp = low_temp
70
- self.temp_mode = "focused" # focused, creative, wild
82
+ self.streams = [] # [{temp, insight, words, samples, event, starred, timestamp}]
83
+ self.starred = []
84
+ self.current_temp = high_temp
71
85
  self.scroll_offset = 0
72
- self.current_panel = 0 # 0=main, 1=streams, 2=starred
86
+ self.current_panel = 0 # 0=main, 1=streams, 2=starred, 3=review
73
87
  self.status = "Ready"
74
88
  self.generating = False
75
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 = ""
76
102
 
77
103
  state = WanderState()
78
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
+
79
140
  # ========== TUI Helpers ==========
80
141
  def get_size():
81
142
  try:
@@ -94,9 +155,7 @@ steps:
94
155
  return lines
95
156
 
96
157
  def draw_box(x, y, w, h, title="", color="\033[90m"):
97
- """Draw a box with optional title"""
98
158
  out = []
99
- # Top border
100
159
  if title:
101
160
  title_part = f" {title} "
102
161
  border = "─" * ((w - len(title_part) - 2) // 2)
@@ -104,119 +163,116 @@ steps:
104
163
  else:
105
164
  top = "┌" + "─" * (w - 2) + "┐"
106
165
  out.append(f"\033[{y};{x}H{color}{top}\033[0m")
107
- # Sides
108
166
  for i in range(1, h - 1):
109
167
  out.append(f"\033[{y+i};{x}H{color}│\033[0m")
110
168
  out.append(f"\033[{y+i};{x+w-1}H{color}│\033[0m")
111
- # Bottom
112
169
  out.append(f"\033[{y+h-1};{x}H{color}└{'─' * (w - 2)}┘\033[0m")
113
170
  return ''.join(out)
114
171
 
115
172
  def render_screen():
116
173
  width, height = get_size()
117
174
  out = []
118
-
119
- # Clear screen
120
175
  out.append("\033[2J\033[H")
121
176
 
122
177
  # ===== HEADER =====
123
- header = f" WANDER - {problem[:width-20]}... " if len(problem) > width-20 else f" WANDER - {problem} "
124
- temp_color = {"focused": "\033[36m", "creative": "\033[35m", "wild": "\033[31m"}[state.temp_mode]
125
- temp_info = f"{temp_color}[{state.temp_mode} T={state.current_temp:.1f}]\033[0m"
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"
126
181
  status_color = "\033[33m" if state.generating else "\033[32m"
127
182
  status_info = f"{status_color}[{state.status}]\033[0m"
128
183
 
129
- out.append(f"\033[1;1H\033[45;37;1m{header.ljust(width)}\033[0m")
184
+ out.append(f"\033[1;1H\033[7;1m{header.ljust(width)}\033[0m")
130
185
  out.append(f"\033[1;{width-35}H{temp_info} {status_info}")
131
186
 
132
- # ===== LAYOUT =====
133
- # Left panel: Environment + Controls (30% width)
134
- # Right panel: Main output / Streams / Starred (70% width)
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):
135
196
  left_w = max(25, width // 3)
136
197
  right_w = width - left_w - 1
137
198
  panel_h = height - 4
138
199
 
139
- # ===== LEFT PANEL: Environment =====
200
+ # Left: Environment
140
201
  out.append(draw_box(1, 3, left_w, panel_h // 2, "Environment", "\033[36m"))
141
- env_lines = wrap_text(state.environment or "Press 'e' to generate...", left_w - 4)
202
+ env_lines = wrap_text(state.environment or "Generating...", left_w - 4)
142
203
  for i, line in enumerate(env_lines[:panel_h // 2 - 3]):
143
204
  out.append(f"\033[{4+i};3H{line[:left_w-4]}")
144
205
 
145
- # ===== LEFT PANEL: Controls =====
206
+ # Left: Controls
146
207
  ctrl_y = 3 + panel_h // 2
147
208
  out.append(draw_box(1, ctrl_y, left_w, panel_h // 2, "Controls", "\033[33m"))
148
209
  controls = [
149
210
  "SPACE - New stream",
150
- f"t - Temp: {state.temp_mode}",
211
+ f"t - Temp: {state.current_temp:.1f}",
151
212
  "e - Trigger event",
152
213
  "s - Star insight",
153
- "r - Reflect/synthesize",
214
+ "R - Review & synthesize",
154
215
  "Tab - Switch panel",
155
216
  "j/k - Scroll",
156
217
  "q - Quit",
157
218
  "",
158
219
  f"Streams: {len(state.streams)}",
220
+ f"Samples: {len(state.all_samples)}",
159
221
  f"Starred: {len(state.starred)}",
160
222
  ]
161
223
  for i, ctrl in enumerate(controls[:panel_h // 2 - 3]):
162
224
  out.append(f"\033[{ctrl_y+1+i};3H\033[90m{ctrl[:left_w-4]}\033[0m")
163
225
 
164
- # ===== RIGHT PANEL =====
226
+ # Right panel
165
227
  panel_titles = ["Output", "Stream History", "Starred Insights"]
166
- panel_color = ["\033[37m", "\033[36m", "\033[33m"][state.current_panel]
167
- out.append(draw_box(left_w + 1, 3, right_w, panel_h, panel_titles[state.current_panel], panel_color))
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))
168
231
 
169
- # Panel content
170
232
  content_w = right_w - 4
171
233
  content_h = panel_h - 3
172
234
  content_lines = []
173
235
 
174
- if state.current_panel == 0: # Main output
236
+ if panel_idx == 0:
175
237
  if state.last_output:
176
238
  content_lines = wrap_text(state.last_output, content_w)
177
239
  else:
178
- content_lines = ["", " Press SPACE to begin wandering...", "",
179
- " The AI will explore your problem from",
180
- " different perspectives and temperatures.", "",
181
- " Star insights you find valuable with 's'.",
182
- " Request synthesis anytime with 'r'."]
240
+ content_lines = ["", " Waiting for streams..."]
183
241
 
184
- elif state.current_panel == 1: # Stream history
242
+ elif panel_idx == 1:
185
243
  for i, stream in enumerate(reversed(state.streams)):
186
- starred = "★ " if stream.get('starred') else " "
187
- mode_color = {"focused": "\033[36m", "creative": "\033[35m", "wild": "\033[31m"}.get(stream.get('mode', ''), '')
188
- header = f"{starred}{mode_color}Stream {len(state.streams)-i} ({stream.get('mode', '?')}, T={stream.get('temp', 0):.1f})\033[0m"
189
- content_lines.append(header)
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)
190
247
  preview = stream.get('insight', '')[:100].replace('\n', ' ')
191
248
  content_lines.append(f" {preview}...")
249
+ if stream.get('samples'):
250
+ content_lines.append(f" \033[90mSamples: {' '.join(stream['samples'][:8])}...\033[0m")
192
251
  content_lines.append("")
193
252
 
194
- elif state.current_panel == 2: # Starred
253
+ elif panel_idx == 2:
195
254
  if not state.starred:
196
- content_lines = ["", " No starred insights yet.", "", " Press 's' after generating a stream", " to star valuable insights."]
255
+ content_lines = ["", " No starred insights yet.", "", " Press 's' to star current stream."]
197
256
  else:
198
257
  for i, item in enumerate(state.starred):
199
- content_lines.append(f"★ {i+1}. [{item.get('mode', '?')}]")
258
+ content_lines.append(f"★ {i+1}. [T={item.get('temp', 0):.1f}]")
200
259
  for line in wrap_text(item.get('insight', ''), content_w - 4):
201
260
  content_lines.append(f" {line}")
202
261
  content_lines.append("")
203
262
 
204
- # Apply scroll and render content
205
263
  visible = content_lines[state.scroll_offset:state.scroll_offset + content_h]
206
264
  for i, line in enumerate(visible):
207
- # Strip ANSI for length calc but keep for display
208
265
  out.append(f"\033[{4+i};{left_w+3}H{line[:content_w]}")
209
266
 
210
- # Scroll indicator
211
267
  if len(content_lines) > content_h:
212
268
  scroll_pct = state.scroll_offset / max(1, len(content_lines) - content_h)
213
269
  indicator_pos = int(scroll_pct * (content_h - 1))
214
270
  out.append(f"\033[{4+indicator_pos};{width-1}H\033[33m▐\033[0m")
215
271
 
216
- # ===== FOOTER =====
272
+ # Footer
217
273
  panel_tabs = ""
218
274
  for i, name in enumerate(["Output", "Streams", "Starred"]):
219
- if i == state.current_panel:
275
+ if i == panel_idx:
220
276
  panel_tabs += f"\033[7m {name} \033[0m "
221
277
  else:
222
278
  panel_tabs += f"\033[90m {name} \033[0m "
@@ -224,14 +280,46 @@ steps:
224
280
  out.append(f"\033[{height-1};1H\033[90m{'─' * width}\033[0m")
225
281
  out.append(f"\033[{height};1H{panel_tabs}")
226
282
 
227
- sys.stdout.write(''.join(out))
228
- sys.stdout.flush()
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")
229
318
 
230
319
  # ========== Actions ==========
231
320
  def generate_environment():
232
321
  state.status = "Generating environment..."
233
322
  state.generating = True
234
- render_screen()
235
323
 
236
324
  env_prompt = f"""Create a vivid, metaphorical environment for wandering through while exploring:
237
325
  "{problem}"
@@ -249,82 +337,131 @@ steps:
249
337
  state.status = "Ready"
250
338
  state.generating = False
251
339
 
252
- def generate_stream():
253
- if state.generating:
254
- return
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
255
349
 
256
- state.status = f"Wandering ({state.temp_mode})..."
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)..."
257
353
  state.generating = True
258
- render_screen()
259
354
 
260
- # Build context from recent streams
261
- recent = state.streams[-3:] if state.streams else []
262
- recent_context = "\n".join([s.get('insight', '')[:200] for s in recent]) if recent else "Starting fresh"
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}
263
366
 
264
- wander_prompt = f"""You are wandering through: {state.environment or 'a conceptual landscape'}
367
+ Context: {state.environment or 'a conceptual landscape'}
368
+ Problem: "{problem}"
265
369
 
266
- Problem: "{problem}"
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"
267
375
 
268
- Recent thoughts: {recent_context}
376
+ sys_prompt = None
377
+ wander_prompt = f"""You are wandering through: {state.environment or 'a conceptual landscape'}
269
378
 
270
- In this {state.temp_mode} exploration (temperature {state.current_temp}):
271
- - Let associations flow freely
272
- - Notice unexpected connections
273
- - Follow interesting tangents
274
- - Share what emerges
379
+ Problem: "{problem}"
275
380
 
276
- Respond naturally, 2-4 paragraphs."""
381
+ Recent thoughts: {recent_context}
277
382
 
278
- resp = get_llm_response(wander_prompt, model=model, provider=provider,
279
- temperature=state.current_temp, npc=npc)
280
- insight = str(resp.get('response', ''))
383
+ In this exploration:
384
+ - Let associations flow freely
385
+ - Notice unexpected connections
386
+ - Follow interesting tangents
387
+ - Share what emerges
281
388
 
282
- stream = {
283
- 'temp': state.current_temp,
284
- 'mode': state.temp_mode,
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,
285
412
  'insight': insight,
413
+ 'words': words,
414
+ 'samples': samples,
286
415
  'event': None,
287
416
  'starred': False,
288
417
  'timestamp': datetime.now().isoformat()
289
418
  }
290
- state.streams.append(stream)
419
+ state.streams.append(stream_entry)
291
420
  state.last_output = insight
292
- state.scroll_offset = 0
293
- state.status = "Ready"
294
- state.generating = False
295
421
 
296
- def trigger_event():
297
- if state.generating:
298
- return
299
-
300
- state.status = "Event occurring..."
301
- state.generating = True
302
- render_screen()
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()
303
426
 
304
- event_types = ["encounter", "discovery", "obstacle", "revelation", "memory", "transformation"]
305
- event_type = random.choice(event_types)
427
+ state.scroll_offset = 0
428
+ state.generating = False
429
+ state.status = f"Ready. {len(state.streams)} streams, {len(state.all_samples)} samples"
306
430
 
431
+ def _trigger_event_sync():
432
+ event_type = weighted_event_type()
307
433
  event_prompt = f"""In the environment: {state.environment or 'a conceptual landscape'}
308
434
  While exploring "{problem}", a {event_type} occurs.
309
435
 
310
436
  Describe this {event_type} in 2-3 vivid sentences.
311
437
  Make it metaphorical and thought-provoking."""
312
438
 
313
- resp = get_llm_response(event_prompt, model=model, provider=provider, temperature=1.0, npc=npc)
314
- event = str(resp.get('response', ''))
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}"
315
449
 
316
- state.last_output = f"[{event_type.upper()}]\n\n{event}"
450
+ def trigger_event():
451
+ if state.generating:
452
+ return
453
+ state.status = "Event occurring..."
454
+ state.generating = True
455
+ _trigger_event_sync()
317
456
  state.scroll_offset = 0
318
457
  state.status = "Ready"
319
458
  state.generating = False
320
459
 
321
460
  def toggle_temp():
322
- modes = ["focused", "creative", "wild"]
323
- temps = [low_temp, (low_temp + high_temp) / 2, high_temp]
324
- idx = modes.index(state.temp_mode)
325
- idx = (idx + 1) % 3
326
- state.temp_mode = modes[idx]
327
- state.current_temp = temps[idx]
461
+ if state.current_temp == high_temp:
462
+ state.current_temp = low_temp
463
+ else:
464
+ state.current_temp = high_temp
328
465
 
329
466
  def star_current():
330
467
  if state.streams and not state.streams[-1].get('starred'):
@@ -332,118 +469,252 @@ steps:
332
469
  state.starred.append(state.streams[-1])
333
470
  state.status = "★ Starred!"
334
471
 
335
- def synthesize():
336
- if state.generating or not state.streams:
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"
337
508
  return
338
509
 
510
+ state.review_mode = False
339
511
  state.status = "Synthesizing..."
340
512
  state.generating = True
341
- render_screen()
342
513
 
343
- all_insights = "\n---\n".join([s.get('insight', '') for s in state.streams])
344
- starred_insights = "\n---\n".join([s.get('insight', '') for s in state.starred]) if state.starred else "None"
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', []))
345
524
 
346
- synth_prompt = f"""After wandering through thoughts about "{problem}":
525
+ sample_text = ' '.join(selected_samples[:50]) if selected_samples else 'N/A'
347
526
 
348
- All explorations:
349
- {all_insights}
527
+ synth_prompt = f"""You are synthesizing a wandering exploration of: "{problem}"
350
528
 
351
- Starred insights:
352
- {starred_insights}
529
+ Environment traversed: {state.environment or 'a conceptual landscape'}
353
530
 
354
- Synthesize:
355
- 1. Key themes and patterns
356
- 2. Most surprising connections
357
- 3. Questions worth pursuing
358
- 4. Potential next steps
531
+ Selected explorations ({len(selected)} of {len(state.streams)} streams):
532
+ {"---".join(selected_insights)}
359
533
 
360
- Be concise but insightful."""
534
+ Word samples extracted during wandering: {sample_text}
361
535
 
362
- resp = get_llm_response(synth_prompt, model=model, provider=provider, temperature=0.4, npc=npc)
363
- synthesis = str(resp.get('response', ''))
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)
364
551
 
365
552
  state.last_output = "=== SYNTHESIS ===\n\n" + synthesis
366
553
  state.scroll_offset = 0
554
+ state.current_panel = 0
367
555
  state.status = "Ready"
368
556
  state.generating = False
369
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
+
370
575
  # ========== Main Loop ==========
371
576
  fd = sys.stdin.fileno()
372
577
  old_settings = termios.tcgetattr(fd)
578
+ import select as _sel
373
579
 
374
580
  try:
375
581
  tty.setcbreak(fd)
376
- sys.stdout.write('\033[?25l') # Hide cursor
582
+ sys.stdout.write('\033[?25l')
377
583
  sys.stdout.flush()
378
584
 
379
- # Generate environment AFTER TUI is set up
380
- if environment:
381
- state.environment = environment
382
- else:
383
- generate_environment()
384
-
385
585
  render_screen()
386
586
 
387
- while True:
388
- c = sys.stdin.read(1)
587
+ auto_thread = threading.Thread(target=auto_run, daemon=True)
588
+ auto_thread.start()
389
589
 
390
- if c == 'q' or c == '\x03': # q or Ctrl+C
391
- break
392
- elif c == ' ': # Space - new stream
393
- generate_stream()
394
- elif c == 't': # Toggle temperature
395
- toggle_temp()
396
- elif c == 'e': # Trigger event
397
- trigger_event()
398
- elif c == 's': # Star
399
- star_current()
400
- elif c == 'r': # Reflect/synthesize
401
- synthesize()
402
- elif c == '\t': # Tab - switch panel
403
- state.current_panel = (state.current_panel + 1) % 3
404
- state.scroll_offset = 0
405
- elif c == 'j': # Scroll down
406
- state.scroll_offset += 1
407
- elif c == 'k': # Scroll up
408
- state.scroll_offset = max(0, state.scroll_offset - 1)
409
- elif c == '\x1b': # Escape sequence
410
- c2 = sys.stdin.read(1)
411
- if c2 == '[':
412
- c3 = sys.stdin.read(1)
413
- if c3 == 'A': # Up
414
- state.scroll_offset = max(0, state.scroll_offset - 1)
415
- elif c3 == 'B': # Down
416
- state.scroll_offset += 1
417
- elif c3 == 'C': # Right - next panel
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':
418
669
  state.current_panel = (state.current_panel + 1) % 3
419
670
  state.scroll_offset = 0
420
- elif c3 == 'D': # Left - prev panel
421
- state.current_panel = (state.current_panel - 1) % 3
422
- 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
423
693
 
424
694
  render_screen()
425
695
 
426
696
  finally:
427
697
  termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
428
- sys.stdout.write('\033[?25h') # Show cursor
429
- sys.stdout.write('\033[2J\033[H') # Clear screen
698
+ sys.stdout.write('\033[?25h')
699
+ sys.stdout.write('\033[2J\033[H')
430
700
  sys.stdout.flush()
431
701
 
432
- # Final output
433
702
  if state.streams:
434
703
  print(colored("=== WANDER SESSION COMPLETE ===\n", "green"))
435
704
  print(f"Problem: {problem}")
436
705
  print(f"Streams: {len(state.streams)}")
437
- print(f"Starred: {len(state.starred)}\n")
706
+ print(f"Samples: {len(state.all_samples)}")
707
+ print(f"Starred: {len(state.starred)}")
708
+ print(f"Events: {len(state.events)}\n")
438
709
 
439
710
  if state.starred:
440
711
  print(colored("Starred Insights:", "yellow"))
441
712
  for i, s in enumerate(state.starred):
442
- print(f"\n{i+1}. [{s.get('mode')}] {s.get('insight')[:300]}...")
713
+ print(f"\n{i+1}. [T={s.get('temp', 0):.1f}] {s.get('insight', '')[:300]}...")
443
714
 
444
- print(colored("\n--- Final Synthesis ---", "cyan"))
445
- synthesize()
446
- print(state.last_output)
715
+ if state.last_output and "SYNTHESIS" in state.last_output:
716
+ print(colored("\n--- Final Synthesis ---", "cyan"))
717
+ print(state.last_output)
447
718
 
448
719
  context['output'] = state.last_output
449
720
  context['messages'] = messages
@@ -452,4 +723,6 @@ steps:
452
723
  'environment': state.environment,
453
724
  'streams': state.streams,
454
725
  'starred': state.starred,
726
+ 'events': state.events,
727
+ 'samples': state.all_samples,
455
728
  }