npcsh 1.1.19__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 (173) hide show
  1. npcsh/_state.py +16 -78
  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 +18 -7
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
  10. npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  18. npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
  19. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  20. npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
  21. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  22. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  23. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
  25. npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
  26. npcsh/npc_team/jinxs/modes/team.jinx +504 -0
  27. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
  29. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. npcsh/npcsh.py +112 -47
  31. npcsh/routes.py +12 -3
  32. npcsh/salmon_simulation.py +0 -0
  33. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  34. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
  35. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  36. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  37. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  38. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
  39. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
  40. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
  41. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  42. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
  43. npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  44. npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
  45. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
  46. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
  48. npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
  49. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  50. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  51. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
  52. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
  53. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
  54. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
  55. npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
  56. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  57. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  58. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
  59. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  60. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  61. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
  62. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  63. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
  64. npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
  65. npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  66. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  67. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  68. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  69. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  70. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  71. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  72. npcsh/npc_team/plonkjr.npc +0 -23
  73. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  74. npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
  75. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
  76. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  77. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  78. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  79. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
  80. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  81. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  82. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
  83. npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  84. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  85. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  86. npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  87. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
  88. npcsh-1.1.19.dist-info/entry_points.txt +0 -22
  89. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  90. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  91. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  92. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  93. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  142. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  143. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  144. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  145. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  146. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  147. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  148. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  149. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  150. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  151. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  152. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  153. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  154. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  155. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  156. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  157. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  158. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  159. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  160. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  161. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  162. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  163. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  165. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  166. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  167. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  168. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  169. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  170. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  171. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  172. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  173. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,343 @@
1
+ jinx_name: models
2
+ description: Interactive model browser - detect available models and set active defaults
3
+ interactive: true
4
+ inputs: []
5
+ steps:
6
+ - name: model_browser
7
+ engine: python
8
+ code: |
9
+ import os
10
+ import sys
11
+ import tty
12
+ import termios
13
+ import select
14
+
15
+ if not sys.stdin.isatty():
16
+ context['output'] = "Models TUI requires an interactive terminal."
17
+
18
+ else:
19
+ from npcpy.npc_sysenv import get_locally_available_models
20
+ from npcsh.config import set_npcsh_config_value
21
+
22
+ LOCAL_PROVIDERS = {'ollama', 'llamacpp', 'lmstudio', 'mlx', 'lora'}
23
+ CLOUD_PROVIDERS = {'openai', 'anthropic', 'gemini', 'deepseek'}
24
+
25
+ class TUIState:
26
+ def __init__(self):
27
+ self.tab = 0
28
+ self.tabs = ['All', 'Local', 'Cloud', 'Active']
29
+ self.sel = 0
30
+ self.scroll = 0
31
+ self.status = ""
32
+ self.models = [] # list of (provider, model_id)
33
+ self.filtered = [] # filtered view
34
+ self.chat_model = ''
35
+ self.chat_provider = ''
36
+ self.vision_model = ''
37
+ self.vision_provider = ''
38
+ self.embed_model = ''
39
+ self.embed_provider = ''
40
+ self.reasoning_model = ''
41
+ self.reasoning_provider = ''
42
+
43
+ ui = TUIState()
44
+
45
+ def term_size():
46
+ try:
47
+ s = os.get_terminal_size()
48
+ return s.columns, s.lines
49
+ except:
50
+ return 80, 24
51
+
52
+ def load_active():
53
+ ui.chat_model = os.environ.get('NPCSH_CHAT_MODEL', '')
54
+ ui.chat_provider = os.environ.get('NPCSH_CHAT_PROVIDER', '')
55
+ ui.vision_model = os.environ.get('NPCSH_VISION_MODEL', '')
56
+ ui.vision_provider = os.environ.get('NPCSH_VISION_PROVIDER', '')
57
+ ui.embed_model = os.environ.get('NPCSH_EMBEDDING_MODEL', '')
58
+ ui.embed_provider = os.environ.get('NPCSH_EMBEDDING_PROVIDER', '')
59
+ ui.reasoning_model = os.environ.get('NPCSH_REASONING_MODEL', '')
60
+ ui.reasoning_provider = os.environ.get('NPCSH_REASONING_PROVIDER', '')
61
+
62
+ def detect_models():
63
+ ui.status = "Detecting models..."
64
+ try:
65
+ models_dict = get_locally_available_models('.', airplane_mode=False)
66
+ except Exception as e:
67
+ models_dict = {}
68
+ ui.status = f"Detection error: {e}"
69
+ ui.models = []
70
+ if models_dict:
71
+ for model_id, provider in sorted(models_dict.items(), key=lambda x: (x[1], x[0])):
72
+ ui.models.append((provider, model_id))
73
+ if ui.models and not ui.status.startswith("Detection error"):
74
+ ui.status = f"Found {len(ui.models)} models"
75
+ apply_filter()
76
+
77
+ def apply_filter():
78
+ if ui.tab == 0:
79
+ ui.filtered = list(ui.models)
80
+ elif ui.tab == 1:
81
+ ui.filtered = [(p, m) for p, m in ui.models if p in LOCAL_PROVIDERS]
82
+ elif ui.tab == 2:
83
+ ui.filtered = [(p, m) for p, m in ui.models if p in CLOUD_PROVIDERS]
84
+ elif ui.tab == 3:
85
+ active = set()
86
+ if ui.chat_model and ui.chat_provider:
87
+ active.add((ui.chat_provider, ui.chat_model))
88
+ if ui.vision_model and ui.vision_provider:
89
+ active.add((ui.vision_provider, ui.vision_model))
90
+ if ui.embed_model and ui.embed_provider:
91
+ active.add((ui.embed_provider, ui.embed_model))
92
+ if ui.reasoning_model and ui.reasoning_provider:
93
+ active.add((ui.reasoning_provider, ui.reasoning_model))
94
+ ui.filtered = [(p, m) for p, m in ui.models if (p, m) in active]
95
+ ui.sel = min(ui.sel, max(0, len(ui.filtered) - 1))
96
+ ui.scroll = min(ui.scroll, max(0, ui.sel))
97
+
98
+ def model_roles(provider, model_id):
99
+ roles = []
100
+ if model_id == ui.chat_model and provider == ui.chat_provider:
101
+ roles.append('chat')
102
+ if model_id == ui.vision_model and provider == ui.vision_provider:
103
+ roles.append('vision')
104
+ if model_id == ui.embed_model and provider == ui.embed_provider:
105
+ roles.append('embed')
106
+ if model_id == ui.reasoning_model and provider == ui.reasoning_provider:
107
+ roles.append('reasoning')
108
+ return roles
109
+
110
+ # -- rendering -----------------------------------------------
111
+ def wline(row, text):
112
+ return f"\033[{row};1H\033[K{text}"
113
+
114
+ def render():
115
+ W, H = term_size()
116
+ out = []
117
+
118
+ out.append("\033[H")
119
+
120
+ # -- header --
121
+ hdr = " Models "
122
+ pad = '=' * W
123
+ out.append(wline(1, f"\033[7;1m{pad}\033[0m"))
124
+ out.append(f"\033[1;{max(1,(W - len(hdr)) // 2)}H\033[7;1m{hdr}\033[0m")
125
+
126
+ # -- tabs --
127
+ tb = ""
128
+ for i, t in enumerate(ui.tabs):
129
+ if i == ui.tab:
130
+ tb += f"\033[7;1m [{t}] \033[0m"
131
+ else:
132
+ tb += f" {t} "
133
+ out.append(wline(2, f" {tb}"))
134
+ out.append(wline(3, f"\033[90m{'─' * W}\033[0m"))
135
+
136
+ # -- column header --
137
+ prov_w = 14
138
+ model_w = max(24, W - prov_w - 24)
139
+ col_hdr = f" {'Provider':<{prov_w}}{'Model':<{model_w}}Status"
140
+ out.append(wline(4, f"\033[1m{col_hdr[:W]}\033[0m"))
141
+
142
+ # -- body --
143
+ body_start = 5
144
+ body_end = H - 4
145
+ body_h = body_end - body_start + 1
146
+
147
+ vis = ui.filtered[ui.scroll:ui.scroll + body_h]
148
+ for r in range(body_h):
149
+ row = body_start + r
150
+ idx = r + ui.scroll
151
+ if r >= len(vis):
152
+ out.append(wline(row, ""))
153
+ continue
154
+ provider, model_id = vis[r]
155
+ roles = model_roles(provider, model_id)
156
+ if roles:
157
+ role_str = "\033[32m* " + ', '.join(roles) + "\033[0m"
158
+ role_plain = "* " + ', '.join(roles)
159
+ else:
160
+ role_str = ""
161
+ role_plain = ""
162
+
163
+ disp_model = model_id
164
+ if len(disp_model) > model_w - 1:
165
+ disp_model = disp_model[:model_w - 4] + '...'
166
+
167
+ line_plain = f" {provider:<{prov_w}}{disp_model:<{model_w}}{role_plain}"
168
+
169
+ if idx == ui.sel:
170
+ # highlighted row
171
+ line_sel = f" > {provider:<{prov_w}}{disp_model:<{model_w}}{role_plain}"
172
+ out.append(wline(row, f"\033[7m{line_sel[:W].ljust(W)}\033[0m"))
173
+ else:
174
+ prov_color = "\033[36m" if provider in LOCAL_PROVIDERS else "\033[35m"
175
+ line_fmt = f" {prov_color}{provider:<{prov_w}}\033[0m{disp_model:<{model_w}}{role_str}"
176
+ out.append(wline(row, line_fmt))
177
+
178
+ if not ui.filtered:
179
+ out.append(wline(body_start, " \033[90mNo models found for this filter.\033[0m"))
180
+
181
+ # -- separator --
182
+ out.append(wline(H - 3, f"\033[90m{'─' * W}\033[0m"))
183
+
184
+ # -- active summary --
185
+ parts = []
186
+ if ui.chat_model:
187
+ parts.append(f"chat: {ui.chat_model}/{ui.chat_provider}")
188
+ if ui.vision_model:
189
+ parts.append(f"vision: {ui.vision_model}/{ui.vision_provider}")
190
+ if ui.embed_model:
191
+ parts.append(f"embed: {ui.embed_model}/{ui.embed_provider}")
192
+ if ui.reasoning_model:
193
+ parts.append(f"reasoning: {ui.reasoning_model}/{ui.reasoning_provider}")
194
+ summary = ' '.join(parts)
195
+ if summary:
196
+ out.append(wline(H - 2, f" \033[33m{summary[:W-2]}\033[0m"))
197
+ else:
198
+ out.append(wline(H - 2, " \033[90mNo active models configured.\033[0m"))
199
+
200
+ # -- status / footer --
201
+ if ui.status:
202
+ stat_line = f" \033[33m{ui.status[:W-2]}\033[0m"
203
+ else:
204
+ stat_line = ""
205
+ # combine status into footer area
206
+ foot = " [Tab] Filter [j/k] Nav [c] Set Chat [v] Set Vision [e] Set Embed [r] Set Reasoning [d] Refresh [q] Quit"
207
+ out.append(wline(H - 1, stat_line))
208
+ out.append(wline(H, f"\033[7m{foot[:W].ljust(W)}\033[0m"))
209
+
210
+ sys.stdout.write(''.join(out))
211
+ sys.stdout.flush()
212
+
213
+ # -- input handling ------------------------------------------
214
+ def handle(c):
215
+ if c == '\x1b':
216
+ return handle_esc()
217
+ if c == 'q':
218
+ return False
219
+ elif c == '\t':
220
+ ui.tab = (ui.tab + 1) % len(ui.tabs)
221
+ ui.sel = 0
222
+ ui.scroll = 0
223
+ apply_filter()
224
+ ui.status = ""
225
+ elif c == 'k':
226
+ nav_up()
227
+ elif c == 'j':
228
+ nav_down()
229
+ elif c == 'c':
230
+ assign_role('chat')
231
+ elif c == 'v':
232
+ assign_role('vision')
233
+ elif c == 'e':
234
+ assign_role('embed')
235
+ elif c == 'r':
236
+ assign_role('reasoning')
237
+ elif c == 'd':
238
+ detect_models()
239
+ load_active()
240
+ return True
241
+
242
+ def handle_esc():
243
+ if select.select([fd], [], [], 0.05)[0]:
244
+ c2 = os.read(fd, 1).decode('latin-1')
245
+ if c2 == '[':
246
+ c3 = os.read(fd, 1).decode('latin-1')
247
+ if c3 == 'A':
248
+ nav_up()
249
+ elif c3 == 'B':
250
+ nav_down()
251
+ elif c3 == 'Z':
252
+ # Shift+Tab: cycle tabs backward
253
+ ui.tab = (ui.tab - 1) % len(ui.tabs)
254
+ ui.sel = 0
255
+ ui.scroll = 0
256
+ apply_filter()
257
+ ui.status = ""
258
+ # consume any other escape sequence
259
+ else:
260
+ # bare Esc: quit
261
+ return False
262
+ return True
263
+
264
+ def nav_up():
265
+ ui.sel = max(0, ui.sel - 1)
266
+ if ui.sel < ui.scroll:
267
+ ui.scroll = ui.sel
268
+ ui.status = ""
269
+
270
+ def nav_down():
271
+ _, H = term_size()
272
+ body_h = H - 8
273
+ mx = max(0, len(ui.filtered) - 1)
274
+ ui.sel = min(mx, ui.sel + 1)
275
+ if ui.sel >= ui.scroll + body_h:
276
+ ui.scroll = ui.sel - body_h + 1
277
+ ui.status = ""
278
+
279
+ def assign_role(role):
280
+ if not ui.filtered:
281
+ ui.status = "No model selected."
282
+ return
283
+ if ui.sel >= len(ui.filtered):
284
+ ui.status = "No model selected."
285
+ return
286
+ provider, model_id = ui.filtered[ui.sel]
287
+
288
+ if role == 'chat':
289
+ set_npcsh_config_value('NPCSH_CHAT_MODEL', model_id)
290
+ set_npcsh_config_value('NPCSH_CHAT_PROVIDER', provider)
291
+ os.environ['NPCSH_CHAT_MODEL'] = model_id
292
+ os.environ['NPCSH_CHAT_PROVIDER'] = provider
293
+ ui.chat_model = model_id
294
+ ui.chat_provider = provider
295
+ ui.status = f"Chat model set to {model_id}/{provider}"
296
+ elif role == 'vision':
297
+ set_npcsh_config_value('NPCSH_VISION_MODEL', model_id)
298
+ set_npcsh_config_value('NPCSH_VISION_PROVIDER', provider)
299
+ os.environ['NPCSH_VISION_MODEL'] = model_id
300
+ os.environ['NPCSH_VISION_PROVIDER'] = provider
301
+ ui.vision_model = model_id
302
+ ui.vision_provider = provider
303
+ ui.status = f"Vision model set to {model_id}/{provider}"
304
+ elif role == 'embed':
305
+ set_npcsh_config_value('NPCSH_EMBEDDING_MODEL', model_id)
306
+ set_npcsh_config_value('NPCSH_EMBEDDING_PROVIDER', provider)
307
+ os.environ['NPCSH_EMBEDDING_MODEL'] = model_id
308
+ os.environ['NPCSH_EMBEDDING_PROVIDER'] = provider
309
+ ui.embed_model = model_id
310
+ ui.embed_provider = provider
311
+ ui.status = f"Embedding model set to {model_id}/{provider}"
312
+ elif role == 'reasoning':
313
+ set_npcsh_config_value('NPCSH_REASONING_MODEL', model_id)
314
+ set_npcsh_config_value('NPCSH_REASONING_PROVIDER', provider)
315
+ os.environ['NPCSH_REASONING_MODEL'] = model_id
316
+ os.environ['NPCSH_REASONING_PROVIDER'] = provider
317
+ ui.reasoning_model = model_id
318
+ ui.reasoning_provider = provider
319
+ ui.status = f"Reasoning model set to {model_id}/{provider}"
320
+
321
+ # -- main loop -----------------------------------------------
322
+ load_active()
323
+ detect_models()
324
+ fd = sys.stdin.fileno()
325
+ old_attrs = termios.tcgetattr(fd)
326
+
327
+ try:
328
+ tty.setcbreak(fd)
329
+ sys.stdout.write('\033[?25l') # hide cursor
330
+ sys.stdout.write('\033[2J\033[H') # initial full clear
331
+ sys.stdout.flush()
332
+ render()
333
+ while True:
334
+ c = os.read(fd, 1).decode('latin-1')
335
+ if not handle(c):
336
+ break
337
+ render()
338
+ finally:
339
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
340
+ sys.stdout.write('\033[?25h\033[2J\033[H')
341
+ sys.stdout.flush()
342
+
343
+ context['output'] = "Models TUI closed."
@@ -1,5 +1,6 @@
1
1
  jinx_name: setup
2
2
  description: Interactive setup wizard for npcsh - detect local models, configure defaults
3
+ interactive: true
3
4
  inputs:
4
5
  - skip_detection: ""
5
6
  steps:
@@ -102,8 +103,8 @@ steps:
102
103
  out = []
103
104
  out.append("\033[2J\033[H")
104
105
  header = " NPCSH Setup Wizard "
105
- out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
106
- out.append(f"\033[1;{(width - len(header)) // 2}H\033[44;37;1m{header}\033[0m")
106
+ out.append(f"\033[1;1H\033[7;1m{'=' * width}\033[0m")
107
+ out.append(f"\033[1;{(width - len(header)) // 2}H\033[7;1m{header}\033[0m")
107
108
 
108
109
  if state.phase == 'detect':
109
110
  out.append(f"\033[3;2H\033[1mDetected Providers:\033[0m")
@@ -136,7 +137,7 @@ steps:
136
137
  row = 5 + i
137
138
  idx = i + state.scroll_offset
138
139
  if idx == state.selected_idx:
139
- out.append(f"\033[{row};4H\033[47;30m> {model} ({provider})\033[0m")
140
+ out.append(f"\033[{row};4H\033[7m> {model} ({provider})\033[0m")
140
141
  else:
141
142
  out.append(f"\033[{row};4H {model} \033[90m({provider})\033[0m")
142
143
 
@@ -148,10 +149,10 @@ steps:
148
149
  if c == 'q':
149
150
  return False
150
151
  if c == '\x1b':
151
- if select.select([sys.stdin], [], [], 0.05)[0]:
152
- c2 = sys.stdin.read(1)
152
+ if select.select([fd], [], [], 0.05)[0]:
153
+ c2 = os.read(fd, 1).decode('latin-1')
153
154
  if c2 == '[':
154
- c3 = sys.stdin.read(1)
155
+ c3 = os.read(fd, 1).decode('latin-1')
155
156
  if c3 == 'A' and state.phase == 'select_chat':
156
157
  state.selected_idx = max(0, state.selected_idx - 1)
157
158
  if state.selected_idx < state.scroll_offset:
@@ -224,7 +225,7 @@ steps:
224
225
  run_detection()
225
226
  render_screen()
226
227
  while True:
227
- c = sys.stdin.read(1)
228
+ c = os.read(fd, 1).decode('latin-1')
228
229
  if not handle_input(c):
229
230
  break
230
231
  render_screen()