npcsh 1.1.17__py3-none-any.whl → 1.1.19__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.
- npcsh/_state.py +122 -91
- npcsh/alicanto.py +2 -2
- npcsh/benchmark/__init__.py +8 -2
- npcsh/benchmark/npcsh_agent.py +87 -22
- npcsh/benchmark/runner.py +85 -43
- npcsh/benchmark/templates/install-npcsh.sh.j2 +35 -0
- npcsh/build.py +2 -4
- npcsh/completion.py +2 -6
- npcsh/config.py +2 -3
- npcsh/conversation_viewer.py +389 -0
- npcsh/corca.py +0 -1
- npcsh/diff_viewer.py +452 -0
- npcsh/execution.py +0 -1
- npcsh/guac.py +0 -1
- npcsh/mcp_helpers.py +2 -3
- npcsh/mcp_server.py +5 -10
- npcsh/npc.py +10 -11
- npcsh/npc_team/jinxs/bin/benchmark.jinx +1 -1
- npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
- npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
- npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
- npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
- npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
- npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +321 -17
- npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +312 -67
- npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +366 -44
- npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +73 -0
- npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +328 -20
- npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +242 -10
- npcsh/npc_team/jinxs/lib/core/sleep.jinx +22 -11
- npcsh/npc_team/jinxs/lib/core/sql.jinx +10 -6
- npcsh/npc_team/jinxs/lib/research/paper_search.jinx +387 -76
- npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +372 -55
- npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +299 -144
- npcsh/npc_team/jinxs/modes/alicanto.jinx +356 -0
- npcsh/npc_team/jinxs/modes/arxiv.jinx +720 -0
- npcsh/npc_team/jinxs/modes/corca.jinx +430 -0
- npcsh/npc_team/jinxs/modes/guac.jinx +542 -0
- npcsh/npc_team/jinxs/modes/plonk.jinx +379 -0
- npcsh/npc_team/jinxs/modes/pti.jinx +357 -0
- npcsh/npc_team/jinxs/modes/reattach.jinx +291 -0
- npcsh/npc_team/jinxs/modes/spool.jinx +350 -0
- npcsh/npc_team/jinxs/modes/wander.jinx +455 -0
- npcsh/npc_team/jinxs/{bin → modes}/yap.jinx +13 -7
- npcsh/npcsh.py +7 -4
- npcsh/plonk.py +0 -1
- npcsh/pti.py +0 -1
- npcsh/routes.py +1 -3
- npcsh/spool.py +0 -1
- npcsh/ui.py +0 -1
- npcsh/wander.py +0 -1
- npcsh/yap.py +0 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +356 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/arxiv.jinx +720 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/corca.jinx +430 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/db_search.jinx +348 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/file_search.jinx +339 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/guac.jinx +542 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +331 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/kg_search.jinx +418 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +73 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +388 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +412 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +379 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/pti.jinx +357 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/reattach.jinx +291 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +386 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +22 -11
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/spool.jinx +350 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/sql.jinx +20 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +455 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/web_search.jinx +283 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +13 -7
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/METADATA +110 -14
- npcsh-1.1.19.dist-info/RECORD +244 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/WHEEL +1 -1
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -3
- npcsh/npc_team/jinxs/bin/spool.jinx +0 -161
- npcsh/npc_team/jinxs/bin/wander.jinx +0 -242
- npcsh/npc_team/jinxs/lib/research/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/arxiv.jinx +0 -76
- npcsh-1.1.17.data/data/npcsh/npc_team/db_search.jinx +0 -44
- npcsh-1.1.17.data/data/npcsh/npc_team/file_search.jinx +0 -94
- npcsh-1.1.17.data/data/npcsh/npc_team/jinxs.jinx +0 -176
- npcsh-1.1.17.data/data/npcsh/npc_team/kg_search.jinx +0 -96
- npcsh-1.1.17.data/data/npcsh/npc_team/mem_search.jinx +0 -80
- npcsh-1.1.17.data/data/npcsh/npc_team/paper_search.jinx +0 -101
- npcsh-1.1.17.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -69
- npcsh-1.1.17.data/data/npcsh/npc_team/spool.jinx +0 -161
- npcsh-1.1.17.data/data/npcsh/npc_team/sql.jinx +0 -16
- npcsh-1.1.17.data/data/npcsh/npc_team/sync.jinx +0 -230
- npcsh-1.1.17.data/data/npcsh/npc_team/wander.jinx +0 -242
- npcsh-1.1.17.data/data/npcsh/npc_team/web_search.jinx +0 -51
- npcsh-1.1.17.dist-info/RECORD +0 -219
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.17.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.17.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive conversation viewer for /reattach command.
|
|
3
|
+
Provides a TUI for browsing and selecting previous conversations.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tty
|
|
8
|
+
import termios
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import List, Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from termcolor import colored
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_terminal_size() -> Tuple[int, int]:
|
|
16
|
+
"""Get terminal width and height."""
|
|
17
|
+
try:
|
|
18
|
+
size = os.get_terminal_size()
|
|
19
|
+
return size.columns, size.lines
|
|
20
|
+
except:
|
|
21
|
+
return 80, 24
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clear_screen():
|
|
25
|
+
"""Clear the terminal screen."""
|
|
26
|
+
sys.stdout.write('\033[2J\033[H')
|
|
27
|
+
sys.stdout.flush()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def move_cursor(row: int, col: int):
|
|
31
|
+
"""Move cursor to specific position."""
|
|
32
|
+
sys.stdout.write(f'\033[{row};{col}H')
|
|
33
|
+
sys.stdout.flush()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def hide_cursor():
|
|
37
|
+
"""Hide the cursor."""
|
|
38
|
+
sys.stdout.write('\033[?25l')
|
|
39
|
+
sys.stdout.flush()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def show_cursor():
|
|
43
|
+
"""Show the cursor."""
|
|
44
|
+
sys.stdout.write('\033[?25h')
|
|
45
|
+
sys.stdout.flush()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def getch() -> str:
|
|
49
|
+
"""Read a single character from stdin."""
|
|
50
|
+
fd = sys.stdin.fileno()
|
|
51
|
+
old_settings = termios.tcgetattr(fd)
|
|
52
|
+
try:
|
|
53
|
+
tty.setraw(fd)
|
|
54
|
+
ch = sys.stdin.read(1)
|
|
55
|
+
# Handle escape sequences
|
|
56
|
+
if ch == '\x1b':
|
|
57
|
+
ch2 = sys.stdin.read(1)
|
|
58
|
+
if ch2 == '[':
|
|
59
|
+
ch3 = sys.stdin.read(1)
|
|
60
|
+
return f'\x1b[{ch3}'
|
|
61
|
+
return ch
|
|
62
|
+
finally:
|
|
63
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def truncate(text: str, max_len: int) -> str:
|
|
67
|
+
"""Truncate text with ellipsis if needed."""
|
|
68
|
+
if len(text) <= max_len:
|
|
69
|
+
return text
|
|
70
|
+
return text[:max_len-3] + '...'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_timestamp(ts: str) -> str:
|
|
74
|
+
"""Format timestamp for display."""
|
|
75
|
+
if not ts:
|
|
76
|
+
return 'unknown'
|
|
77
|
+
try:
|
|
78
|
+
# Try parsing ISO format
|
|
79
|
+
if 'T' in ts:
|
|
80
|
+
dt = datetime.fromisoformat(ts.replace('Z', '+00:00'))
|
|
81
|
+
else:
|
|
82
|
+
dt = datetime.strptime(ts[:19], '%Y-%m-%d %H:%M:%S')
|
|
83
|
+
|
|
84
|
+
now = datetime.now()
|
|
85
|
+
diff = now - dt.replace(tzinfo=None)
|
|
86
|
+
|
|
87
|
+
if diff.days == 0:
|
|
88
|
+
return f"Today {dt.strftime('%H:%M')}"
|
|
89
|
+
elif diff.days == 1:
|
|
90
|
+
return f"Yesterday {dt.strftime('%H:%M')}"
|
|
91
|
+
elif diff.days < 7:
|
|
92
|
+
return dt.strftime('%a %H:%M')
|
|
93
|
+
else:
|
|
94
|
+
return dt.strftime('%b %d')
|
|
95
|
+
except:
|
|
96
|
+
return ts[:16] if len(ts) > 16 else ts
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ConversationViewer:
|
|
100
|
+
"""Interactive TUI for browsing conversations."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, conversations: List[Dict], current_path: str):
|
|
103
|
+
self.conversations = conversations
|
|
104
|
+
self.current_path = current_path
|
|
105
|
+
self.selected = 0
|
|
106
|
+
self.scroll_offset = 0
|
|
107
|
+
self.preview_conversation = None
|
|
108
|
+
self.mode = 'list' # 'list' or 'preview'
|
|
109
|
+
self.preview_scroll = 0
|
|
110
|
+
self.width, self.height = get_terminal_size()
|
|
111
|
+
|
|
112
|
+
def draw_header(self):
|
|
113
|
+
"""Draw the header bar."""
|
|
114
|
+
move_cursor(1, 1)
|
|
115
|
+
header = f" CONVERSATIONS: {truncate(self.current_path, self.width - 20)} "
|
|
116
|
+
header = header.ljust(self.width)
|
|
117
|
+
sys.stdout.write(colored(header, 'white', 'on_blue', attrs=['bold']))
|
|
118
|
+
|
|
119
|
+
def draw_help(self):
|
|
120
|
+
"""Draw the help bar at bottom."""
|
|
121
|
+
move_cursor(self.height, 1)
|
|
122
|
+
if self.mode == 'list':
|
|
123
|
+
help_text = " ↑/↓:Navigate Enter:Select p:Preview q:Quit "
|
|
124
|
+
else:
|
|
125
|
+
help_text = " ↑/↓:Scroll b:Back Enter:Select q:Quit "
|
|
126
|
+
help_text = help_text.ljust(self.width)
|
|
127
|
+
sys.stdout.write(colored(help_text, 'white', 'on_blue'))
|
|
128
|
+
|
|
129
|
+
def draw_conversation_list(self):
|
|
130
|
+
"""Draw the conversation list."""
|
|
131
|
+
list_height = self.height - 4 # Header, separator, status, help
|
|
132
|
+
|
|
133
|
+
# Calculate visible range
|
|
134
|
+
if self.selected < self.scroll_offset:
|
|
135
|
+
self.scroll_offset = self.selected
|
|
136
|
+
elif self.selected >= self.scroll_offset + list_height:
|
|
137
|
+
self.scroll_offset = self.selected - list_height + 1
|
|
138
|
+
|
|
139
|
+
for i in range(list_height):
|
|
140
|
+
row = 3 + i
|
|
141
|
+
move_cursor(row, 1)
|
|
142
|
+
|
|
143
|
+
idx = self.scroll_offset + i
|
|
144
|
+
if idx >= len(self.conversations):
|
|
145
|
+
sys.stdout.write(' ' * self.width)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
conv = self.conversations[idx]
|
|
149
|
+
is_selected = idx == self.selected
|
|
150
|
+
|
|
151
|
+
# Format conversation line
|
|
152
|
+
convo_id = conv.get('conversation_id', '')[:12]
|
|
153
|
+
msg_count = conv.get('msg_count', 0)
|
|
154
|
+
last_msg = format_timestamp(conv.get('last_msg', ''))
|
|
155
|
+
npcs = conv.get('npcs', 'default')
|
|
156
|
+
if npcs and len(npcs) > 15:
|
|
157
|
+
npcs = npcs[:12] + '...'
|
|
158
|
+
|
|
159
|
+
# Build line
|
|
160
|
+
prefix = '>' if is_selected else ' '
|
|
161
|
+
line = f"{prefix} {convo_id:<14} {msg_count:>4} msgs {last_msg:<15} {npcs}"
|
|
162
|
+
line = truncate(line, self.width - 1)
|
|
163
|
+
line = line.ljust(self.width - 1)
|
|
164
|
+
|
|
165
|
+
if is_selected:
|
|
166
|
+
sys.stdout.write(colored(line, 'black', 'on_white', attrs=['bold']))
|
|
167
|
+
else:
|
|
168
|
+
sys.stdout.write(line)
|
|
169
|
+
|
|
170
|
+
# Draw separator
|
|
171
|
+
move_cursor(self.height - 2, 1)
|
|
172
|
+
sys.stdout.write(colored('─' * self.width, 'grey'))
|
|
173
|
+
|
|
174
|
+
# Draw status
|
|
175
|
+
move_cursor(self.height - 1, 1)
|
|
176
|
+
if self.conversations:
|
|
177
|
+
conv = self.conversations[self.selected]
|
|
178
|
+
full_id = conv.get('conversation_id', '')
|
|
179
|
+
status = f" ID: {full_id}"
|
|
180
|
+
else:
|
|
181
|
+
status = " No conversations found"
|
|
182
|
+
sys.stdout.write(truncate(status, self.width).ljust(self.width))
|
|
183
|
+
|
|
184
|
+
def draw_preview(self):
|
|
185
|
+
"""Draw conversation preview."""
|
|
186
|
+
if not self.preview_conversation:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
preview_height = self.height - 4
|
|
190
|
+
messages = self.preview_conversation
|
|
191
|
+
|
|
192
|
+
# Draw messages
|
|
193
|
+
line_num = 0
|
|
194
|
+
for msg in messages:
|
|
195
|
+
if line_num >= self.preview_scroll + preview_height:
|
|
196
|
+
break
|
|
197
|
+
if line_num < self.preview_scroll:
|
|
198
|
+
line_num += 1
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
row = 3 + (line_num - self.preview_scroll)
|
|
202
|
+
move_cursor(row, 1)
|
|
203
|
+
|
|
204
|
+
role = msg.get('role', 'unknown')
|
|
205
|
+
content = msg.get('content', '')[:200].replace('\n', ' ')
|
|
206
|
+
|
|
207
|
+
if role == 'user':
|
|
208
|
+
prefix = colored('You: ', 'green', attrs=['bold'])
|
|
209
|
+
elif role == 'assistant':
|
|
210
|
+
prefix = colored('AI: ', 'blue', attrs=['bold'])
|
|
211
|
+
else:
|
|
212
|
+
prefix = colored(f'{role}: ', 'grey')
|
|
213
|
+
|
|
214
|
+
line = truncate(content, self.width - 8)
|
|
215
|
+
sys.stdout.write(prefix + line.ljust(self.width - 6))
|
|
216
|
+
line_num += 1
|
|
217
|
+
|
|
218
|
+
# Clear remaining lines
|
|
219
|
+
for i in range(line_num - self.preview_scroll, preview_height):
|
|
220
|
+
move_cursor(3 + i, 1)
|
|
221
|
+
sys.stdout.write(' ' * self.width)
|
|
222
|
+
|
|
223
|
+
# Draw separator and status
|
|
224
|
+
move_cursor(self.height - 2, 1)
|
|
225
|
+
sys.stdout.write(colored('─' * self.width, 'grey'))
|
|
226
|
+
move_cursor(self.height - 1, 1)
|
|
227
|
+
status = f" Preview: {len(messages)} messages (scroll: {self.preview_scroll})"
|
|
228
|
+
sys.stdout.write(truncate(status, self.width).ljust(self.width))
|
|
229
|
+
|
|
230
|
+
def draw(self):
|
|
231
|
+
"""Draw the full interface."""
|
|
232
|
+
self.draw_header()
|
|
233
|
+
if self.mode == 'list':
|
|
234
|
+
self.draw_conversation_list()
|
|
235
|
+
else:
|
|
236
|
+
self.draw_preview()
|
|
237
|
+
self.draw_help()
|
|
238
|
+
sys.stdout.flush()
|
|
239
|
+
|
|
240
|
+
def load_preview(self, fetch_messages_func):
|
|
241
|
+
"""Load messages for preview."""
|
|
242
|
+
if not self.conversations:
|
|
243
|
+
return
|
|
244
|
+
conv = self.conversations[self.selected]
|
|
245
|
+
convo_id = conv.get('conversation_id')
|
|
246
|
+
if convo_id and fetch_messages_func:
|
|
247
|
+
self.preview_conversation = fetch_messages_func(convo_id)
|
|
248
|
+
self.preview_scroll = 0
|
|
249
|
+
|
|
250
|
+
def run(self, fetch_messages_func=None) -> Optional[str]:
|
|
251
|
+
"""
|
|
252
|
+
Run the interactive viewer.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
fetch_messages_func: Function to fetch messages for a conversation_id
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Selected conversation_id or None if cancelled
|
|
259
|
+
"""
|
|
260
|
+
if not self.conversations:
|
|
261
|
+
print(colored("No conversations found for this path.", 'yellow'))
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
old_settings = None
|
|
265
|
+
try:
|
|
266
|
+
# Setup terminal
|
|
267
|
+
old_settings = termios.tcgetattr(sys.stdin.fileno())
|
|
268
|
+
hide_cursor()
|
|
269
|
+
clear_screen()
|
|
270
|
+
|
|
271
|
+
while True:
|
|
272
|
+
self.draw()
|
|
273
|
+
|
|
274
|
+
key = getch()
|
|
275
|
+
|
|
276
|
+
if key == 'q' or key == '\x03': # q or Ctrl+C
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
elif key == '\x1b[A': # Up
|
|
280
|
+
if self.mode == 'list':
|
|
281
|
+
if self.selected > 0:
|
|
282
|
+
self.selected -= 1
|
|
283
|
+
else:
|
|
284
|
+
if self.preview_scroll > 0:
|
|
285
|
+
self.preview_scroll -= 1
|
|
286
|
+
|
|
287
|
+
elif key == '\x1b[B': # Down
|
|
288
|
+
if self.mode == 'list':
|
|
289
|
+
if self.selected < len(self.conversations) - 1:
|
|
290
|
+
self.selected += 1
|
|
291
|
+
else:
|
|
292
|
+
self.preview_scroll += 1
|
|
293
|
+
|
|
294
|
+
elif key == '\r' or key == '\n': # Enter
|
|
295
|
+
if self.conversations:
|
|
296
|
+
return self.conversations[self.selected].get('conversation_id')
|
|
297
|
+
|
|
298
|
+
elif key == 'p' and self.mode == 'list':
|
|
299
|
+
self.load_preview(fetch_messages_func)
|
|
300
|
+
if self.preview_conversation:
|
|
301
|
+
self.mode = 'preview'
|
|
302
|
+
clear_screen()
|
|
303
|
+
|
|
304
|
+
elif key == 'b' and self.mode == 'preview':
|
|
305
|
+
self.mode = 'list'
|
|
306
|
+
clear_screen()
|
|
307
|
+
|
|
308
|
+
elif key == 'j': # vim-style down
|
|
309
|
+
if self.mode == 'list' and self.selected < len(self.conversations) - 1:
|
|
310
|
+
self.selected += 1
|
|
311
|
+
elif self.mode == 'preview':
|
|
312
|
+
self.preview_scroll += 1
|
|
313
|
+
|
|
314
|
+
elif key == 'k': # vim-style up
|
|
315
|
+
if self.mode == 'list' and self.selected > 0:
|
|
316
|
+
self.selected -= 1
|
|
317
|
+
elif self.mode == 'preview' and self.preview_scroll > 0:
|
|
318
|
+
self.preview_scroll -= 1
|
|
319
|
+
|
|
320
|
+
except Exception:
|
|
321
|
+
return None
|
|
322
|
+
finally:
|
|
323
|
+
# Restore terminal
|
|
324
|
+
show_cursor()
|
|
325
|
+
clear_screen()
|
|
326
|
+
if old_settings:
|
|
327
|
+
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old_settings)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def launch_conversation_viewer(
|
|
331
|
+
db_path: str,
|
|
332
|
+
target_path: str,
|
|
333
|
+
limit: int = 50
|
|
334
|
+
) -> Optional[str]:
|
|
335
|
+
"""
|
|
336
|
+
Launch the conversation viewer and return selected conversation_id.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
db_path: Path to the npcsh database
|
|
340
|
+
target_path: Directory path to filter conversations
|
|
341
|
+
limit: Maximum number of conversations to show
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Selected conversation_id or None
|
|
345
|
+
"""
|
|
346
|
+
from sqlalchemy import create_engine, text
|
|
347
|
+
|
|
348
|
+
engine = create_engine(f'sqlite:///{db_path}')
|
|
349
|
+
|
|
350
|
+
# Fetch conversations
|
|
351
|
+
with engine.connect() as conn:
|
|
352
|
+
result = conn.execute(text("""
|
|
353
|
+
SELECT conversation_id, directory_path,
|
|
354
|
+
MIN(timestamp) as started,
|
|
355
|
+
MAX(timestamp) as last_msg,
|
|
356
|
+
COUNT(*) as msg_count,
|
|
357
|
+
GROUP_CONCAT(DISTINCT npc) as npcs
|
|
358
|
+
FROM conversation_history
|
|
359
|
+
WHERE directory_path = :path OR directory_path LIKE :path_pattern
|
|
360
|
+
GROUP BY conversation_id
|
|
361
|
+
ORDER BY last_msg DESC
|
|
362
|
+
LIMIT :limit
|
|
363
|
+
"""), {"path": target_path, "path_pattern": target_path + "/%", "limit": limit})
|
|
364
|
+
|
|
365
|
+
conversations = []
|
|
366
|
+
for row in result.fetchall():
|
|
367
|
+
conversations.append({
|
|
368
|
+
'conversation_id': row[0],
|
|
369
|
+
'directory_path': row[1],
|
|
370
|
+
'started': row[2],
|
|
371
|
+
'last_msg': row[3],
|
|
372
|
+
'msg_count': row[4],
|
|
373
|
+
'npcs': row[5]
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
def fetch_messages(convo_id: str) -> List[Dict]:
|
|
377
|
+
"""Fetch messages for a conversation."""
|
|
378
|
+
with engine.connect() as conn:
|
|
379
|
+
result = conn.execute(text("""
|
|
380
|
+
SELECT role, content, timestamp, npc
|
|
381
|
+
FROM conversation_history
|
|
382
|
+
WHERE conversation_id = :convo_id
|
|
383
|
+
ORDER BY timestamp ASC
|
|
384
|
+
LIMIT 100
|
|
385
|
+
"""), {"convo_id": convo_id})
|
|
386
|
+
return [dict(row._mapping) for row in result.fetchall()]
|
|
387
|
+
|
|
388
|
+
viewer = ConversationViewer(conversations, target_path)
|
|
389
|
+
return viewer.run(fetch_messages_func=fetch_messages)
|