janito 1.10.0__py3-none-any.whl → 1.11.1__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 (57) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/conversation_api.py +178 -90
  3. janito/agent/conversation_ui.py +1 -1
  4. janito/agent/llm_conversation_history.py +12 -0
  5. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +19 -4
  6. janito/agent/tools/__init__.py +2 -0
  7. janito/agent/tools/create_directory.py +1 -1
  8. janito/agent/tools/create_file.py +1 -1
  9. janito/agent/tools/fetch_url.py +1 -1
  10. janito/agent/tools/find_files.py +26 -13
  11. janito/agent/tools/get_file_outline/core.py +1 -1
  12. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  13. janito/agent/tools/get_lines.py +92 -63
  14. janito/agent/tools/move_file.py +58 -32
  15. janito/agent/tools/open_url.py +31 -0
  16. janito/agent/tools/python_command_runner.py +85 -86
  17. janito/agent/tools/python_file_runner.py +85 -86
  18. janito/agent/tools/python_stdin_runner.py +87 -88
  19. janito/agent/tools/remove_directory.py +1 -1
  20. janito/agent/tools/remove_file.py +1 -1
  21. janito/agent/tools/replace_file.py +2 -2
  22. janito/agent/tools/replace_text_in_file.py +193 -149
  23. janito/agent/tools/run_bash_command.py +1 -1
  24. janito/agent/tools/run_powershell_command.py +4 -0
  25. janito/agent/tools/search_text/__init__.py +1 -0
  26. janito/agent/tools/search_text/core.py +176 -0
  27. janito/agent/tools/search_text/match_lines.py +58 -0
  28. janito/agent/tools/search_text/pattern_utils.py +65 -0
  29. janito/agent/tools/search_text/traverse_directory.py +132 -0
  30. janito/agent/tools/validate_file_syntax/core.py +41 -30
  31. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  32. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  33. janito/agent/tools_utils/gitignore_utils.py +25 -2
  34. janito/agent/tools_utils/utils.py +7 -1
  35. janito/cli/config_commands.py +112 -109
  36. janito/shell/main.py +51 -8
  37. janito/shell/session/config.py +83 -75
  38. janito/shell/ui/interactive.py +97 -73
  39. janito/termweb/static/editor.css +32 -29
  40. janito/termweb/static/editor.css.bak +140 -22
  41. janito/termweb/static/editor.html +12 -7
  42. janito/termweb/static/editor.html.bak +16 -11
  43. janito/termweb/static/editor.js +94 -40
  44. janito/termweb/static/editor.js.bak +97 -65
  45. janito/termweb/static/index.html +1 -2
  46. janito/termweb/static/index.html.bak +1 -1
  47. janito/termweb/static/termweb.css +1 -22
  48. janito/termweb/static/termweb.css.bak +6 -4
  49. janito/termweb/static/termweb.js +0 -6
  50. janito/termweb/static/termweb.js.bak +1 -2
  51. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/METADATA +1 -1
  52. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/RECORD +56 -51
  53. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/WHEEL +1 -1
  54. janito/agent/tools/search_text.py +0 -254
  55. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/entry_points.txt +0 -0
  56. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/licenses/LICENSE +0 -0
  57. {janito-1.10.0.dist-info → janito-1.11.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,65 @@
1
+ import re
2
+ from janito.i18n import tr
3
+ from janito.agent.tools_utils.utils import pluralize
4
+
5
+
6
+ def prepare_pattern(pattern, is_regex, report_error, report_warning):
7
+ if not pattern:
8
+ report_error(tr("Error: Empty search pattern provided. Operation aborted."))
9
+ return (
10
+ None,
11
+ False,
12
+ tr("Error: Empty search pattern provided. Operation aborted."),
13
+ )
14
+ regex = None
15
+ use_regex = False
16
+ if is_regex:
17
+ try:
18
+ regex = re.compile(pattern)
19
+ use_regex = True
20
+ except re.error as e:
21
+ report_warning(tr("\u26a0\ufe0f Invalid regex pattern."))
22
+ return (
23
+ None,
24
+ False,
25
+ tr(
26
+ "Error: Invalid regex pattern: {error}. Operation aborted.", error=e
27
+ ),
28
+ )
29
+ else:
30
+ # Do not compile as regex if is_regex is False; treat as plain text
31
+ regex = None
32
+ use_regex = False
33
+ return regex, use_regex, None
34
+
35
+
36
+ def format_result(
37
+ pattern, use_regex, output, limit_reached, count_only=False, per_file_counts=None
38
+ ):
39
+ # Ensure output is always a list for joining
40
+ if output is None or not isinstance(output, (list, tuple)):
41
+ output = []
42
+ if count_only:
43
+ lines = []
44
+ total = 0
45
+ if per_file_counts:
46
+ for file_path, count in per_file_counts:
47
+ lines.append(f"{file_path}: {count}")
48
+ total += count
49
+ lines.append(f"Total matches: {total}")
50
+ if limit_reached:
51
+ lines.append(tr("[Max results reached. Output truncated.]"))
52
+ return "\n".join(lines)
53
+ else:
54
+ if not output:
55
+ return tr("No matches found.")
56
+ result = "\n".join(output)
57
+ if limit_reached:
58
+ result += tr("\n[Max results reached. Output truncated.]")
59
+ return result
60
+
61
+
62
+ def summarize_total(all_per_file_counts):
63
+ total = sum(count for _, count in all_per_file_counts)
64
+ summary = f"\nGrand total matches: {total}"
65
+ return summary
@@ -0,0 +1,132 @@
1
+ import os
2
+ from janito.agent.tools_utils.gitignore_utils import GitignoreFilter
3
+ from .match_lines import match_line, should_limit, read_file_lines
4
+
5
+
6
+ def walk_directory(search_path, max_depth):
7
+ if max_depth == 1:
8
+ walk_result = next(os.walk(search_path), None)
9
+ if walk_result is None:
10
+ return [(search_path, [], [])]
11
+ else:
12
+ return [walk_result]
13
+ else:
14
+ return os.walk(search_path)
15
+
16
+
17
+ def filter_dirs(dirs, root, gitignore_filter):
18
+ # Always exclude directories named .git, regardless of gitignore
19
+ return [
20
+ d
21
+ for d in dirs
22
+ if d != ".git" and not gitignore_filter.is_ignored(os.path.join(root, d))
23
+ ]
24
+
25
+
26
+ def process_file_count_only(
27
+ file_path, per_file_counts, pattern, regex, use_regex, max_results, total_results
28
+ ):
29
+ match_count, file_limit_reached, _ = read_file_lines(
30
+ file_path,
31
+ pattern,
32
+ regex,
33
+ use_regex,
34
+ True,
35
+ max_results,
36
+ total_results + sum(count for _, count in per_file_counts),
37
+ )
38
+ if match_count > 0:
39
+ per_file_counts.append((file_path, match_count))
40
+ return file_limit_reached
41
+
42
+
43
+ def process_file_collect(
44
+ file_path,
45
+ dir_output,
46
+ per_file_counts,
47
+ pattern,
48
+ regex,
49
+ use_regex,
50
+ max_results,
51
+ total_results,
52
+ ):
53
+ actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
54
+ file_path,
55
+ pattern,
56
+ regex,
57
+ use_regex,
58
+ False,
59
+ max_results,
60
+ total_results + len(dir_output),
61
+ )
62
+ dir_output.extend(file_lines_output)
63
+ if actual_match_count > 0:
64
+ per_file_counts.append((file_path, actual_match_count))
65
+ return file_limit_reached
66
+
67
+
68
+ def should_limit_depth(root, search_path, max_depth, dirs):
69
+ if max_depth > 0:
70
+ rel_root = os.path.relpath(root, search_path)
71
+ if rel_root != ".":
72
+ depth = rel_root.count(os.sep) + 1
73
+ if depth >= max_depth:
74
+ del dirs[:]
75
+
76
+
77
+ def traverse_directory(
78
+ search_path,
79
+ pattern,
80
+ regex,
81
+ use_regex,
82
+ max_depth,
83
+ max_results,
84
+ total_results,
85
+ count_only,
86
+ ):
87
+ dir_output = []
88
+ dir_limit_reached = False
89
+ per_file_counts = []
90
+ walker = walk_directory(search_path, max_depth)
91
+ gitignore_filter = GitignoreFilter(search_path)
92
+
93
+ for root, dirs, files in walker:
94
+ dirs[:] = filter_dirs(dirs, root, gitignore_filter)
95
+ for file in files:
96
+ file_path = os.path.join(root, file)
97
+ if gitignore_filter.is_ignored(file_path):
98
+ continue
99
+ if count_only:
100
+ file_limit_reached = process_file_count_only(
101
+ file_path,
102
+ per_file_counts,
103
+ pattern,
104
+ regex,
105
+ use_regex,
106
+ max_results,
107
+ total_results,
108
+ )
109
+ if file_limit_reached:
110
+ dir_limit_reached = True
111
+ break
112
+ else:
113
+ file_limit_reached = process_file_collect(
114
+ file_path,
115
+ dir_output,
116
+ per_file_counts,
117
+ pattern,
118
+ regex,
119
+ use_regex,
120
+ max_results,
121
+ total_results,
122
+ )
123
+ if file_limit_reached:
124
+ dir_limit_reached = True
125
+ break
126
+ if dir_limit_reached:
127
+ break
128
+ should_limit_depth(root, search_path, max_depth, dirs)
129
+ if count_only:
130
+ return per_file_counts, dir_limit_reached, []
131
+ else:
132
+ return dir_output, dir_limit_reached, per_file_counts
@@ -16,39 +16,47 @@ from .js_validator import validate_js
16
16
  from .css_validator import validate_css
17
17
 
18
18
 
19
+ def _get_validator(ext):
20
+ """Return the appropriate validator function for the file extension."""
21
+ mapping = {
22
+ ".py": validate_python,
23
+ ".pyw": validate_python,
24
+ ".json": validate_json,
25
+ ".yml": validate_yaml,
26
+ ".yaml": validate_yaml,
27
+ ".ps1": validate_ps1,
28
+ ".xml": validate_xml,
29
+ ".html": validate_html,
30
+ ".htm": validate_html,
31
+ ".md": validate_markdown,
32
+ ".js": validate_js,
33
+ ".css": validate_css,
34
+ }
35
+ return mapping.get(ext)
36
+
37
+
38
+ def _handle_validation_error(e, report_warning):
39
+ msg = tr("\u26a0\ufe0f Warning: Syntax error: {error}", error=e)
40
+ if report_warning:
41
+ report_warning(msg)
42
+ return msg
43
+
44
+
19
45
  def validate_file_syntax(
20
46
  file_path: str, report_info=None, report_warning=None, report_success=None
21
47
  ) -> str:
22
48
  ext = os.path.splitext(file_path)[1].lower()
49
+ validator = _get_validator(ext)
23
50
  try:
24
- if ext in [".py", ".pyw"]:
25
- return validate_python(file_path)
26
- elif ext == ".json":
27
- return validate_json(file_path)
28
- elif ext in [".yml", ".yaml"]:
29
- return validate_yaml(file_path)
30
- elif ext == ".ps1":
31
- return validate_ps1(file_path)
32
- elif ext == ".xml":
33
- return validate_xml(file_path)
34
- elif ext in (".html", ".htm"):
35
- return validate_html(file_path)
36
- elif ext == ".md":
37
- return validate_markdown(file_path)
38
- elif ext == ".js":
39
- return validate_js(file_path)
40
- elif ext == ".css":
41
- return validate_css(file_path)
51
+ if validator:
52
+ return validator(file_path)
42
53
  else:
43
- msg = tr("⚠️ Warning: Unsupported file extension: {ext}", ext=ext)
54
+ msg = tr("\u26a0\ufe0f Warning: Unsupported file extension: {ext}", ext=ext)
44
55
  if report_warning:
45
56
  report_warning(msg)
46
57
  return msg
47
58
  except Exception as e:
48
- msg = tr("⚠️ Warning: Syntax error: {error}", error=e)
49
- if report_warning:
50
- report_warning(msg)
51
- return msg
59
+ return _handle_validation_error(e, report_warning)
52
60
 
53
61
 
54
62
  @register_tool(name="validate_file_syntax")
@@ -70,16 +78,19 @@ class ValidateFileSyntaxTool(ToolBase):
70
78
  file_path (str): Path to the file to validate.
71
79
  Returns:
72
80
  str: Validation status message. Example:
73
- - " Syntax OK"
74
- - "⚠️ Warning: Syntax error: <error message>"
75
- - "⚠️ Warning: Unsupported file extension: <ext>"
81
+ - "\u2705 Syntax OK"
82
+ - "\u26a0\ufe0f Warning: Syntax error: <error message>"
83
+ - "\u26a0\ufe0f Warning: Unsupported file extension: <ext>"
76
84
  """
77
85
 
78
86
  def run(self, file_path: str) -> str:
79
87
  disp_path = display_path(file_path)
80
88
  self.report_info(
81
89
  ActionType.READ,
82
- tr("🔎 Validating syntax for file '{disp_path}' ...", disp_path=disp_path),
90
+ tr(
91
+ "\U0001f50e Validate syntax for file '{disp_path}' ...",
92
+ disp_path=disp_path,
93
+ ),
83
94
  )
84
95
  result = validate_file_syntax(
85
96
  file_path,
@@ -87,8 +98,8 @@ class ValidateFileSyntaxTool(ToolBase):
87
98
  report_warning=self.report_warning,
88
99
  report_success=self.report_success,
89
100
  )
90
- if result.startswith(""):
101
+ if result.startswith("\u2705"):
91
102
  self.report_success(result)
92
- elif result.startswith("⚠️"):
93
- self.report_warning(tr("⚠️ ") + result.lstrip("⚠️ "))
103
+ elif result.startswith("\u26a0\ufe0f"):
104
+ self.report_warning(tr("\u26a0\ufe0f ") + result.lstrip("\u26a0\ufe0f "))
94
105
  return result
@@ -4,9 +4,19 @@ from lxml import etree
4
4
 
5
5
 
6
6
  def validate_html(file_path: str) -> str:
7
- warnings = []
7
+ html_content = _read_html_content(file_path)
8
+ warnings = _find_js_outside_script(html_content)
9
+ lxml_error = _parse_html_and_collect_errors(file_path)
10
+ msg = _build_result_message(warnings, lxml_error)
11
+ return msg
12
+
13
+
14
+ def _read_html_content(file_path):
8
15
  with open(file_path, "r", encoding="utf-8") as f:
9
- html_content = f.read()
16
+ return f.read()
17
+
18
+
19
+ def _find_js_outside_script(html_content):
10
20
  script_blocks = [
11
21
  m.span()
12
22
  for m in re.finditer(
@@ -21,6 +31,7 @@ def validate_html(file_path: str) -> str:
21
31
  r"^\s*window\.\w+\s*=",
22
32
  r"^\s*\$\s*\(",
23
33
  ]
34
+ warnings = []
24
35
  for pat in js_patterns:
25
36
  for m in re.finditer(pat, html_content):
26
37
  in_script = False
@@ -32,14 +43,16 @@ def validate_html(file_path: str) -> str:
32
43
  warnings.append(
33
44
  f"Line {html_content.count(chr(10), 0, m.start())+1}: JavaScript code ('{pat}') found outside <script> tag."
34
45
  )
46
+ return warnings
47
+
48
+
49
+ def _parse_html_and_collect_errors(file_path):
35
50
  lxml_error = None
36
51
  try:
37
- # Parse HTML and collect error log
38
52
  parser = etree.HTMLParser(recover=False)
39
53
  with open(file_path, "rb") as f:
40
54
  etree.parse(f, parser=parser)
41
55
  error_log = parser.error_log
42
- # Look for tag mismatch or unclosed tag errors
43
56
  syntax_errors = []
44
57
  for e in error_log:
45
58
  if (
@@ -52,7 +65,6 @@ def validate_html(file_path: str) -> str:
52
65
  if syntax_errors:
53
66
  lxml_error = tr("Syntax error: {error}", error="; ".join(syntax_errors))
54
67
  elif error_log:
55
- # Other warnings
56
68
  lxml_error = tr(
57
69
  "HTML syntax errors found:\n{errors}",
58
70
  errors="\n".join(str(e) for e in error_log),
@@ -61,6 +73,10 @@ def validate_html(file_path: str) -> str:
61
73
  lxml_error = tr("⚠️ lxml not installed. Cannot validate HTML.")
62
74
  except Exception as e:
63
75
  lxml_error = tr("Syntax error: {error}", error=str(e))
76
+ return lxml_error
77
+
78
+
79
+ def _build_result_message(warnings, lxml_error):
64
80
  msg = ""
65
81
  if warnings:
66
82
  msg += (
@@ -5,62 +5,105 @@ import re
5
5
  def validate_markdown(file_path: str) -> str:
6
6
  with open(file_path, "r", encoding="utf-8") as f:
7
7
  content = f.read()
8
- errors = []
9
8
  lines = content.splitlines()
10
- # Header space check
9
+ errors = []
10
+ errors.extend(_check_header_space(lines))
11
+ errors.extend(_check_unclosed_code_block(content))
12
+ errors.extend(_check_unclosed_links_images(lines))
13
+ errors.extend(_check_list_formatting(lines))
14
+ errors.extend(_check_unclosed_inline_code(content))
15
+ return _build_markdown_result(errors)
16
+
17
+
18
+ def _check_header_space(lines):
19
+ errors = []
11
20
  for i, line in enumerate(lines, 1):
12
21
  if re.match(r"^#+[^ #]", line):
13
22
  errors.append(f"Line {i}: Header missing space after # | {line.strip()}")
14
- # Unclosed code block
23
+ return errors
24
+
25
+
26
+ def _check_unclosed_code_block(content):
27
+ errors = []
15
28
  if content.count("```") % 2 != 0:
16
29
  errors.append("Unclosed code block (```) detected")
17
- # Unclosed link or image
30
+ return errors
31
+
32
+
33
+ def _check_unclosed_links_images(lines):
34
+ errors = []
18
35
  for i, line in enumerate(lines, 1):
19
36
  if re.search(r"\[[^\]]*\]\([^)]+$", line):
20
37
  errors.append(
21
38
  f"Line {i}: Unclosed link or image (missing closing parenthesis) | {line.strip()}"
22
39
  )
23
- # List item formatting and blank line before new list (bulleted and numbered)
40
+ return errors
41
+
42
+
43
+ def _is_table_line(line):
44
+ return line.lstrip().startswith("|")
45
+
46
+
47
+ def _list_item_missing_space(line):
48
+ return re.match(r"^[-*+][^ \n]", line)
49
+
50
+
51
+ def _should_skip_list_item(line):
52
+ stripped = line.strip()
53
+ return stripped.startswith("*") and stripped.endswith("*") and len(stripped) > 2
54
+
55
+
56
+ def _needs_blank_line_before_bullet(lines, i):
57
+ if i <= 1:
58
+ return False
59
+ prev_line = lines[i - 2]
60
+ prev_is_list = bool(re.match(r"^\s*[-*+] ", prev_line))
61
+ return not prev_is_list and prev_line.strip() != ""
62
+
63
+
64
+ def _needs_blank_line_before_numbered(lines, i):
65
+ if i <= 1:
66
+ return False
67
+ prev_line = lines[i - 2]
68
+ prev_is_numbered_list = bool(re.match(r"^\s*\d+\. ", prev_line))
69
+ return not prev_is_numbered_list and prev_line.strip() != ""
70
+
71
+
72
+ def _check_list_formatting(lines):
73
+ errors = []
24
74
  for i, line in enumerate(lines, 1):
25
- # Skip table lines
26
- if line.lstrip().startswith("|"):
75
+ if _is_table_line(line):
27
76
  continue
28
- # List item missing space after bullet
29
- if re.match(r"^[-*+][^ \n]", line):
30
- stripped = line.strip()
31
- if not (
32
- stripped.startswith("*")
33
- and stripped.endswith("*")
34
- and len(stripped) > 2
35
- ):
77
+ if _list_item_missing_space(line):
78
+ if not _should_skip_list_item(line):
36
79
  errors.append(
37
80
  f"Line {i}: List item missing space after bullet | {line.strip()}"
38
81
  )
39
- # Blank line before first item of a new bulleted list
40
82
  if re.match(r"^\s*[-*+] ", line):
41
- if i > 1:
42
- prev_line = lines[i - 2]
43
- prev_is_list = bool(re.match(r"^\s*[-*+] ", prev_line))
44
- if not prev_is_list and prev_line.strip() != "":
45
- errors.append(
46
- f"Line {i}: List should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
47
- )
48
- # Blank line before first item of a new numbered list
83
+ if _needs_blank_line_before_bullet(lines, i):
84
+ errors.append(
85
+ f"Line {i}: List should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
86
+ )
49
87
  if re.match(r"^\s*\d+\. ", line):
50
- if i > 1:
51
- prev_line = lines[i - 2]
52
- prev_is_numbered_list = bool(re.match(r"^\s*\d+\. ", prev_line))
53
- if not prev_is_numbered_list and prev_line.strip() != "":
54
- errors.append(
55
- f"Line {i}: Numbered list should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
56
- )
57
- # Unclosed inline code
88
+ if _needs_blank_line_before_numbered(lines, i):
89
+ errors.append(
90
+ f"Line {i}: Numbered list should be preceded by a blank line for compatibility with MkDocs and other Markdown parsers | {line.strip()}"
91
+ )
92
+ return errors
93
+
94
+
95
+ def _check_unclosed_inline_code(content):
96
+ errors = []
58
97
  if content.count("`") % 2 != 0:
59
98
  errors.append("Unclosed inline code (`) detected")
99
+ return errors
100
+
101
+
102
+ def _build_markdown_result(errors):
60
103
  if errors:
61
104
  msg = tr(
62
- "⚠️ Warning: Markdown syntax issues found:\n{errors}",
105
+ "\u26a0\ufe0f Warning: Markdown syntax issues found:\n{errors}",
63
106
  errors="\n".join(errors),
64
107
  )
65
108
  return msg
66
- return " Syntax valid"
109
+ return "\u2705 Syntax valid"
@@ -9,7 +9,7 @@ class GitignoreFilter:
9
9
  Methods
10
10
  -------
11
11
  __init__(self, gitignore_path: str = ".gitignore")
12
- Loads and parses .gitignore patterns from the specified path.
12
+ Loads and parses .gitignore patterns from the specified path or finds the nearest .gitignore if a directory is given.
13
13
 
14
14
  is_ignored(self, path: str) -> bool
15
15
  Returns True if the given path matches any of the loaded .gitignore patterns.
@@ -18,8 +18,31 @@ class GitignoreFilter:
18
18
  Filters out ignored directories and files from the provided lists, returning only those not ignored.
19
19
  """
20
20
 
21
+ @staticmethod
22
+ def find_nearest_gitignore(start_path):
23
+ """
24
+ Search upward from start_path for the nearest .gitignore file.
25
+ Returns the path to the found .gitignore, or the default .gitignore in start_path if none found.
26
+ """
27
+ current_dir = os.path.abspath(start_path)
28
+ if os.path.isfile(current_dir):
29
+ current_dir = os.path.dirname(current_dir)
30
+ while True:
31
+ candidate = os.path.join(current_dir, ".gitignore")
32
+ if os.path.isfile(candidate):
33
+ return candidate
34
+ parent = os.path.dirname(current_dir)
35
+ if parent == current_dir:
36
+ # Reached filesystem root, return default .gitignore path (may not exist)
37
+ return os.path.join(start_path, ".gitignore")
38
+ current_dir = parent
39
+
21
40
  def __init__(self, gitignore_path: str = ".gitignore"):
22
- self.gitignore_path = os.path.abspath(gitignore_path)
41
+ # If a directory is passed, find the nearest .gitignore up the tree
42
+ if os.path.isdir(gitignore_path):
43
+ self.gitignore_path = self.find_nearest_gitignore(gitignore_path)
44
+ else:
45
+ self.gitignore_path = os.path.abspath(gitignore_path)
23
46
  self.base_dir = os.path.dirname(self.gitignore_path)
24
47
  lines = []
25
48
  if not os.path.exists(self.gitignore_path):
@@ -12,7 +12,13 @@ def display_path(path):
12
12
  str: Display path, optionally as an ANSI hyperlink.
13
13
  """
14
14
  if os.path.isabs(path):
15
- disp = path
15
+ cwd = os.path.abspath(os.getcwd())
16
+ abs_path = os.path.abspath(path)
17
+ # Check if the absolute path is within the current working directory
18
+ if abs_path.startswith(cwd + os.sep):
19
+ disp = os.path.relpath(abs_path, cwd)
20
+ else:
21
+ disp = path
16
22
  else:
17
23
  disp = os.path.relpath(path)
18
24
  port = runtime_config.get("termweb_port")