python-delphi-lsp 2.0.2__py3-none-any.whl → 2.0.4__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.
delphi_lsp/__init__.py CHANGED
@@ -48,6 +48,7 @@ from .semantic_builder import SemanticBuilder, SemanticModel, SemanticProblem
48
48
  from .workspace import WorkspaceSemanticResult, build_workspace_semantics
49
49
  from .lsp_server import LspWorkspaceState, create_server
50
50
  from .project_discovery import DelphiProjectDiscovery, DiscoveryProblem, discover_delphi_project
51
+ from .progress import ProgressCallback, ProgressEvent
51
52
  from .project_indexer import (
52
53
  GetUnitSyntaxHook,
53
54
  IncludeFileInfo,
@@ -136,6 +137,8 @@ __all__ = [
136
137
  'DelphiProjectDiscovery',
137
138
  'DiscoveryProblem',
138
139
  'discover_delphi_project',
140
+ 'ProgressCallback',
141
+ 'ProgressEvent',
139
142
  'ProjectIndexer',
140
143
  'ProjectIndexResult',
141
144
  'ProjectProblemType',
delphi_lsp/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2.0.2"
1
+ __version__ = "2.0.4"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from bisect import bisect_left, bisect_right
4
4
  from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
+ from heapq import heappop, heappush
6
7
  import hashlib
7
8
  import json
8
9
  from pathlib import Path, PureWindowsPath
@@ -14,13 +15,12 @@ from .agent_protocol import (
14
15
  AgentResponse,
15
16
  ContextBudget,
16
17
  Focus,
17
- Page,
18
18
  make_target_id,
19
19
  paginate_items,
20
20
  )
21
21
  from .agent_metrics import build_workspace_metrics, project_metric_item, unit_metric_item
22
22
  from .agent_relations import ProjectRelationIndex, RelationTarget
23
- from .agent_workspace import AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
23
+ from .agent_workspace import AgentUnit, AgentWorkspace, unit_display_path, unit_source_path, unit_target_id
24
24
  from .consts import AttributeName, SyntaxNodeType
25
25
  from .lsp_server import build_outline_semantic_model, multiline_string_block_end
26
26
  from .nodes import CompoundSyntaxNode, SyntaxNode
@@ -121,6 +121,10 @@ class _SourceDocument:
121
121
  self.tokens = tuple(_lex_delphi(text))
122
122
  self.token_starts = tuple(token.start for token in self.tokens)
123
123
  self.directive_starts = tuple(token.start for token in self.tokens if token.directive)
124
+ self.declaration_section_indexes = [0]
125
+ self.declaration_section_checkpoints: dict[int, tuple[str, int, int, int]] = {
126
+ 0: ("", 0, 0, 0)
127
+ }
124
128
  words = [token for token in self.tokens if token.word and not token.escaped]
125
129
  self.unit_kind = next(
126
130
  (token.value for token in words if token.value in {"unit", "program", "library", "package"}),
@@ -129,6 +133,7 @@ class _SourceDocument:
129
133
  implementation = next((token for token in words if token.value == "implementation"), None)
130
134
  self.implementation_line = self.line_col(implementation.start)[0] if implementation else 0
131
135
  self.routine_spans: dict[int, tuple[int, int] | None] = {}
136
+ self.routine_token_spans: dict[int, tuple[int, int, int] | None] = {}
132
137
  self.parser_spans: dict[str, tuple[int, int] | None] = {}
133
138
  self._full_parse_attempted = False
134
139
  self._full_parse_result: object | None = None
@@ -230,6 +235,7 @@ class _Registry:
230
235
  entries: tuple[_SymbolEntry, ...]
231
236
  by_target: dict[str, _SymbolEntry]
232
237
  sources: dict[Path, _SourceDocument]
238
+ ranked_queries: dict[str, tuple[_SymbolEntry, ...]]
233
239
 
234
240
 
235
241
  class AgentContext:
@@ -280,7 +286,12 @@ class AgentContext:
280
286
  return self._handle_focus(parsed, revision)
281
287
  if parsed.action == "find":
282
288
  registry = self._require_registry(revision)
283
- items = [entry.card() for entry in _ranked_entries(registry.entries, parsed.query)]
289
+ ranked = registry.ranked_queries.get(parsed.query)
290
+ if ranked is None:
291
+ ranked = tuple(_ranked_entries(registry.entries, parsed.query))
292
+ registry.ranked_queries.clear()
293
+ registry.ranked_queries[parsed.query] = ranked
294
+ items = [entry.card() for entry in ranked]
284
295
  return self._response(parsed, revision, items)
285
296
  if parsed.action == "inspect":
286
297
  registry = self._require_registry(revision)
@@ -293,9 +304,10 @@ class AgentContext:
293
304
  previous_project_id = self._workspace.active_project_id
294
305
  selected_project_id = requested_project_id or previous_project_id
295
306
  if selected_project_id:
296
- self._workspace.select_project(selected_project_id)
307
+ revision = self._workspace._select_project_with_revision(selected_project_id)
308
+ else:
309
+ revision = self._workspace.workspace_revision
297
310
  current_project_id = self._workspace.active_project_id
298
- revision = self._workspace.workspace_revision
299
311
 
300
312
  if current_project_id != previous_project_id:
301
313
  self._registry = None
@@ -683,7 +695,14 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
683
695
  include_paths=workspace.include_paths,
684
696
  )
685
697
  sources[source_path] = document
686
- model = build_outline_semantic_model(text, str(source_path))
698
+ if workspace.defines:
699
+ model = build_outline_semantic_model(
700
+ text,
701
+ str(source_path),
702
+ defines=workspace.defines,
703
+ )
704
+ else:
705
+ model = build_outline_semantic_model(text, str(source_path))
687
706
  unit_symbols = _collect_raw_symbols(model.unit_scope, unit, source_path, document)
688
707
  raw_symbols.extend(_exclude_routine_locals(unit_symbols, document))
689
708
 
@@ -782,6 +801,7 @@ def _build_registry(workspace: AgentWorkspace, project_id: str, revision: str) -
782
801
  entries=entries_tuple,
783
802
  by_target={entry.target_id: entry for entry in entries_tuple},
784
803
  sources=sources,
804
+ ranked_queries={},
785
805
  )
786
806
 
787
807
 
@@ -925,32 +945,47 @@ def _correct_outline_symbol_kind(document: _SourceDocument, symbol: Symbol) -> N
925
945
  symbol.kind = SymbolKind.TYPE
926
946
 
927
947
 
948
+ def _advance_declaration_section(
949
+ state: tuple[str, int, int, int],
950
+ token: _Token,
951
+ ) -> tuple[str, int, int, int]:
952
+ section, parentheses, brackets, angles = state
953
+ if token.directive:
954
+ return state
955
+ if token.value == "(":
956
+ parentheses += 1
957
+ elif token.value == ")":
958
+ parentheses = max(0, parentheses - 1)
959
+ elif token.value == "[":
960
+ brackets += 1
961
+ elif token.value == "]":
962
+ brackets = max(0, brackets - 1)
963
+ elif token.value == "<":
964
+ angles += 1
965
+ elif token.value == ">":
966
+ angles = max(0, angles - 1)
967
+ elif not parentheses and not brackets and not angles and token.word:
968
+ if token.value in {"const", "resourcestring", "threadvar", "type", "var"}:
969
+ section = token.value
970
+ elif token.value in {"implementation", "initialization", "finalization"}:
971
+ section = ""
972
+ return section, parentheses, brackets, angles
973
+
974
+
928
975
  def _declaration_section(document: _SourceDocument, offset: int) -> str:
929
- section = ""
930
- parentheses = 0
931
- brackets = 0
932
- angles = 0
933
- for token in document.tokens[:document.first_token_index(offset)]:
934
- if token.directive:
935
- continue
936
- if token.value == "(":
937
- parentheses += 1
938
- elif token.value == ")":
939
- parentheses = max(0, parentheses - 1)
940
- elif token.value == "[":
941
- brackets += 1
942
- elif token.value == "]":
943
- brackets = max(0, brackets - 1)
944
- elif token.value == "<":
945
- angles += 1
946
- elif token.value == ">":
947
- angles = max(0, angles - 1)
948
- elif not any((parentheses, brackets, angles)) and token.word:
949
- if token.value in {"const", "resourcestring", "threadvar", "type", "var"}:
950
- section = token.value
951
- elif token.value in {"implementation", "initialization", "finalization"}:
952
- section = ""
953
- return section
976
+ target_index = document.first_token_index(offset)
977
+ cached = document.declaration_section_checkpoints.get(target_index)
978
+ if cached is not None:
979
+ return cached[0]
980
+ checkpoint_position = bisect_right(document.declaration_section_indexes, target_index) - 1
981
+ checkpoint_index = document.declaration_section_indexes[checkpoint_position]
982
+ state = document.declaration_section_checkpoints[checkpoint_index]
983
+ for token in document.tokens[checkpoint_index:target_index]:
984
+ state = _advance_declaration_section(state, token)
985
+ insert_at = bisect_left(document.declaration_section_indexes, target_index)
986
+ document.declaration_section_indexes.insert(insert_at, target_index)
987
+ document.declaration_section_checkpoints[target_index] = state
988
+ return state[0]
954
989
 
955
990
 
956
991
  def _declared_symbol_name(document: _SourceDocument, symbol: Symbol) -> str:
@@ -1208,20 +1243,47 @@ def _exclude_routine_locals(
1208
1243
  span = _raw_routine_span(raw, document)
1209
1244
  if span is not None:
1210
1245
  containers.append((span[0], span[1], raw))
1246
+ if not containers:
1247
+ return symbols
1211
1248
 
1212
- filtered: list[_RawSymbol] = []
1213
- for raw in symbols:
1214
- offset = document.offset(
1215
- raw.symbol.decl_range.start_line,
1216
- raw.symbol.decl_range.start_col,
1249
+ containers.sort(key=lambda item: (item[0], item[1]))
1250
+ positioned = sorted(
1251
+ (
1252
+ document.offset(
1253
+ raw.symbol.decl_range.start_line,
1254
+ raw.symbol.decl_range.start_col,
1255
+ ),
1256
+ order,
1257
+ raw,
1217
1258
  )
1218
- if any(
1219
- start < offset < end and raw is not container
1220
- for start, end, container in containers
1259
+ for order, raw in enumerate(symbols)
1260
+ )
1261
+ active_ends: list[tuple[int, int, int]] = []
1262
+ active_ids: set[int] = set()
1263
+ excluded_orders: set[int] = set()
1264
+ container_index = 0
1265
+ for offset, order, raw in positioned:
1266
+ while (
1267
+ container_index < len(containers)
1268
+ and containers[container_index][0] < offset
1221
1269
  ):
1222
- continue
1223
- filtered.append(raw)
1224
- return filtered
1270
+ _, end, container = containers[container_index]
1271
+ container_id = id(container)
1272
+ heappush(active_ends, (end, container_index, container_id))
1273
+ active_ids.add(container_id)
1274
+ container_index += 1
1275
+ while active_ends and active_ends[0][0] <= offset:
1276
+ _, _, container_id = heappop(active_ends)
1277
+ active_ids.discard(container_id)
1278
+ raw_id = id(raw)
1279
+ if active_ids and (raw_id not in active_ids or len(active_ids) > 1):
1280
+ excluded_orders.add(order)
1281
+
1282
+ return [
1283
+ raw
1284
+ for order, raw in enumerate(symbols)
1285
+ if order not in excluded_orders
1286
+ ]
1225
1287
 
1226
1288
 
1227
1289
  def _raw_routine_span(
@@ -1523,6 +1585,7 @@ def _routine_span(
1523
1585
  document.tokens,
1524
1586
  document.token_starts,
1525
1587
  token_index,
1588
+ cache=document.routine_token_spans,
1526
1589
  )
1527
1590
  span = (start, found[1]) if found is not None else None
1528
1591
  document.routine_spans[start] = span
@@ -1534,6 +1597,7 @@ def _find_routine_token_span(
1534
1597
  token_starts: tuple[int, ...],
1535
1598
  start_index: int,
1536
1599
  *,
1600
+ cache: dict[int, tuple[int, int, int] | None] | None = None,
1537
1601
  depth: int = 0,
1538
1602
  ) -> tuple[int, int, int] | None:
1539
1603
  if depth > 64:
@@ -1541,56 +1605,100 @@ def _find_routine_token_span(
1541
1605
  routine_index = _routine_keyword_index(tokens, start_index)
1542
1606
  if routine_index is None:
1543
1607
  return None
1608
+ spans = cache if cache is not None else {}
1609
+ missing = object()
1610
+ cached = spans.get(routine_index, missing)
1611
+ if cached is not missing:
1612
+ if cached is None:
1613
+ return None
1614
+ return tokens[start_index].start, cached[1], cached[2]
1615
+
1544
1616
  heading_end = _heading_semicolon_index(tokens, routine_index)
1545
1617
  if heading_end is None:
1618
+ spans[routine_index] = None
1546
1619
  return None
1547
1620
 
1548
- index = heading_end + 1
1549
- while index < len(tokens):
1621
+ frames: list[list[int]] = [[routine_index, heading_end + 1]]
1622
+
1623
+ def reject_active_frames() -> None:
1624
+ for active_routine_index, _ in frames:
1625
+ spans[active_routine_index] = None
1626
+ frames.clear()
1627
+
1628
+ while frames:
1629
+ frame = frames[-1]
1630
+ frame_routine_index, index = frame
1631
+ if index >= len(tokens):
1632
+ reject_active_frames()
1633
+ continue
1634
+
1550
1635
  token = tokens[index]
1551
1636
  if token.directive:
1552
- return None
1637
+ reject_active_frames()
1638
+ continue
1553
1639
  if token.word and not token.escaped:
1554
1640
  if token.value in _NO_BODY_DIRECTIVES:
1555
- return None
1641
+ spans[frame_routine_index] = None
1642
+ frames.pop()
1643
+ continue
1556
1644
  if token.value in {"implementation", "initialization", "finalization"}:
1557
- return None
1645
+ reject_active_frames()
1646
+ continue
1558
1647
  if (
1559
1648
  token.value in _STRUCTURED_TYPE_WORDS
1560
1649
  and _is_structured_type_opener(tokens, index)
1561
1650
  ):
1562
1651
  if index + 1 < len(tokens) and tokens[index + 1].value == ";":
1563
- index += 2
1652
+ frame[1] = index + 2
1564
1653
  continue
1565
1654
  structured_end = _match_end_terminated_block(tokens, index)
1566
1655
  if structured_end is None:
1567
- return None
1568
- index = bisect_left(token_starts, structured_end)
1656
+ reject_active_frames()
1657
+ continue
1658
+ frame[1] = bisect_left(token_starts, structured_end)
1569
1659
  continue
1570
1660
  if token.value == "end":
1571
- return None
1661
+ reject_active_frames()
1662
+ continue
1572
1663
  if token.value in {"begin", "asm"}:
1573
1664
  end = _match_end_terminated_block(tokens, index)
1574
1665
  if end is None:
1575
- return None
1666
+ reject_active_frames()
1667
+ continue
1576
1668
  end_index = bisect_left(token_starts, end)
1577
- return tokens[start_index].start, end, end_index
1578
- if token.value in _ROUTINE_WORDS and _is_nested_routine_declaration(tokens, index):
1579
- nested = _find_routine_token_span(
1580
- tokens,
1581
- token_starts,
1582
- index,
1583
- depth=depth + 1,
1669
+ spans[frame_routine_index] = (
1670
+ tokens[frame_routine_index].start,
1671
+ end,
1672
+ end_index,
1584
1673
  )
1674
+ frames.pop()
1675
+ continue
1676
+ if token.value in _ROUTINE_WORDS and _is_nested_routine_declaration(tokens, index):
1677
+ nested_routine_index = _routine_keyword_index(tokens, index)
1678
+ if nested_routine_index is None:
1679
+ frame[1] = index + 1
1680
+ continue
1681
+ nested = spans.get(nested_routine_index, missing)
1682
+ if nested is missing:
1683
+ nested_heading_end = _heading_semicolon_index(tokens, nested_routine_index)
1684
+ if nested_heading_end is None:
1685
+ spans[nested_routine_index] = None
1686
+ continue
1687
+ frames.append([nested_routine_index, nested_heading_end + 1])
1688
+ continue
1585
1689
  if nested is not None:
1586
- index = max(index + 1, nested[2])
1690
+ frame[1] = max(index + 1, nested[2])
1587
1691
  continue
1588
1692
  skipped = _routine_declaration_end_index(tokens, index)
1589
1693
  if skipped is not None:
1590
- index = skipped + 1
1694
+ frame[1] = skipped + 1
1591
1695
  continue
1592
- index += 1
1593
- return None
1696
+ frame[1] = index + 1
1697
+
1698
+ result = spans.get(routine_index)
1699
+ if result is None:
1700
+ return None
1701
+ return tokens[start_index].start, result[1], result[2]
1594
1702
 
1595
1703
 
1596
1704
  def _routine_keyword_index(
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, replace
4
4
  from pathlib import Path
5
5
  from typing import Any, Iterable
6
6
  import json
7
7
 
8
- from .lsp_server import build_outline_semantic_model, outline_source
8
+ from .lsp_server import build_outline_semantic_model
9
9
  from .metrics import analyze_project
10
10
  from .project_discovery import DelphiProjectDiscovery, discover_delphi_project
11
11
  from .project_indexer import ProjectIndexResult, ProjectIndexer
12
+ from .progress import ProgressCallback, ProgressEvent
12
13
  from .semantic import Scope, SourceRange, Symbol, SymbolIndex, SymbolKind
13
14
  from .semantic_builder import SemanticModel
14
15
  from .source_reader import read_source_text
@@ -28,10 +29,14 @@ def build_codebase_index(
28
29
  *,
29
30
  project_file: str | Path | None = None,
30
31
  index_projects: bool = False,
32
+ on_progress: ProgressCallback | None = None,
31
33
  ) -> CodebaseIndex:
32
- discovery = discover_delphi_project(root, project_file=project_file)
34
+ progress = _MonotonicProgress(on_progress)
35
+ discovery = discover_delphi_project(root, project_file=project_file, on_progress=progress)
33
36
  models: dict[str, SemanticModel] = {}
34
- for source in discovery.source_files:
37
+ lines_processed = 0
38
+ symbols_discovered = 0
39
+ for completed, source in enumerate(discovery.source_files, start=1):
35
40
  path = Path(source)
36
41
  if path.suffix.casefold() not in {".pas", ".dpr", ".dpk", ".inc"}:
37
42
  continue
@@ -39,7 +44,25 @@ def build_codebase_index(
39
44
  text = read_source_text(path)
40
45
  except (OSError, UnicodeError):
41
46
  continue
42
- models[source] = build_outline_semantic_model(outline_source(text), source)
47
+ model = build_outline_semantic_model(
48
+ text,
49
+ source,
50
+ defines=discovery.defines,
51
+ )
52
+ models[source] = model
53
+ lines_processed += text.count("\n") + (0 if not text or text.endswith("\n") else 1)
54
+ symbols_discovered += sum(len(items) for items in model.index.name_index.values())
55
+ _emit_progress(
56
+ progress,
57
+ "outline",
58
+ source,
59
+ len(discovery.source_files),
60
+ completed,
61
+ len(discovery.source_files),
62
+ "source outlined",
63
+ lines_processed=lines_processed,
64
+ symbols_discovered=symbols_discovered,
65
+ )
43
66
 
44
67
  symbol_index = SymbolIndex()
45
68
  for model in models.values():
@@ -47,6 +70,18 @@ def build_codebase_index(
47
70
  for model in models.values():
48
71
  model.index = symbol_index
49
72
 
73
+ _emit_progress(
74
+ progress,
75
+ "relations",
76
+ str(Path(root).expanduser().resolve()),
77
+ len(discovery.source_files),
78
+ len(discovery.source_files),
79
+ len(discovery.source_files),
80
+ "semantic relations indexed",
81
+ lines_processed=lines_processed,
82
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
83
+ )
84
+
50
85
  project_results: dict[str, ProjectIndexResult] = {}
51
86
  if index_projects:
52
87
  for project in discovery.project_files:
@@ -54,16 +89,90 @@ def build_codebase_index(
54
89
  search_paths=discovery.search_paths,
55
90
  include_paths=discovery.include_paths,
56
91
  defines=discovery.defines,
92
+ on_progress=progress,
57
93
  )
58
94
  project_results[project] = indexer.index(project)
59
95
 
60
- return CodebaseIndex(
96
+ index = CodebaseIndex(
61
97
  root=str(Path(root).expanduser().resolve()),
62
98
  discovery=discovery,
63
99
  models=models,
64
100
  symbol_index=symbol_index,
65
101
  project_results=project_results,
66
102
  )
103
+ _emit_progress(
104
+ progress,
105
+ "complete",
106
+ str(Path(root).expanduser().resolve()),
107
+ len(discovery.source_files),
108
+ len(discovery.source_files),
109
+ len(discovery.source_files),
110
+ "codebase index complete",
111
+ lines_processed=lines_processed,
112
+ symbols_discovered=sum(len(items) for items in symbol_index.name_index.values()),
113
+ )
114
+ return index
115
+
116
+
117
+ def _emit_progress(
118
+ callback: ProgressCallback | None,
119
+ phase: str,
120
+ path: str,
121
+ files_discovered: int,
122
+ files_completed: int,
123
+ files_total: int | None,
124
+ detail: str,
125
+ *,
126
+ lines_processed: int = 0,
127
+ symbols_discovered: int = 0,
128
+ ) -> None:
129
+ if callback is not None:
130
+ callback(
131
+ ProgressEvent(
132
+ phase,
133
+ "delphi",
134
+ path,
135
+ files_discovered,
136
+ files_completed,
137
+ files_total,
138
+ lines_processed,
139
+ symbols_discovered,
140
+ 0,
141
+ detail,
142
+ )
143
+ )
144
+
145
+
146
+ class _MonotonicProgress:
147
+ def __init__(self, callback: ProgressCallback | None) -> None:
148
+ self._callback = callback
149
+ self._files_discovered = 0
150
+ self._files_completed = 0
151
+ self._files_total: int | None = None
152
+ self._lines_processed = 0
153
+ self._symbols_discovered = 0
154
+ self._cached_files = 0
155
+
156
+ def __call__(self, event: ProgressEvent) -> None:
157
+ self._files_discovered = max(self._files_discovered, event.files_discovered)
158
+ self._files_completed = max(self._files_completed, event.files_completed)
159
+ if event.files_total is not None:
160
+ self._files_total = max(self._files_total or 0, event.files_total)
161
+ self._lines_processed = max(self._lines_processed, event.lines_processed)
162
+ self._symbols_discovered = max(self._symbols_discovered, event.symbols_discovered)
163
+ self._cached_files = max(self._cached_files, event.cached_files)
164
+ if self._callback is not None:
165
+ self._callback(
166
+ replace(
167
+ event,
168
+ files_discovered=self._files_discovered,
169
+ files_completed=self._files_completed,
170
+ files_total=self._files_total,
171
+ lines_processed=self._lines_processed,
172
+ symbols_discovered=self._symbols_discovered,
173
+ cached_files=self._cached_files,
174
+ )
175
+ )
67
176
 
68
177
 
69
178
  def render_layer(
@@ -239,27 +239,30 @@ Prefer `summary` and `declaration`, narrow `max_items` and `max_chars`, and requ
239
239
  def _agent_markdown() -> str:
240
240
  return """---
241
241
  description: Inspect Delphi and Object Pascal codebases through python-delphi-lsp.
242
- mode: subagent
242
+ mode: all
243
243
  temperature: 0
244
- tools:
245
- delphi_codebase: true
246
- skill: true
247
- lsp: false
248
- bash: false
249
- read: false
250
- glob: false
251
- grep: false
252
- edit: false
253
- write: false
254
- task: false
255
- webfetch: false
256
- todowrite: false
257
244
  permission:
258
- "*": deny
259
245
  delphi_codebase: allow
260
246
  skill:
261
247
  "*": deny
262
248
  python-delphi-lsp: allow
249
+ lsp: deny
250
+ bash: deny
251
+ read: deny
252
+ glob: deny
253
+ grep: deny
254
+ list: deny
255
+ edit: deny
256
+ write: deny
257
+ patch: deny
258
+ task: deny
259
+ webfetch: deny
260
+ websearch: deny
261
+ question: deny
262
+ todowrite: deny
263
+ todoread: deny
264
+ codebase_map: deny
265
+ code_guidelines: deny
263
266
  ---
264
267
 
265
268
  Load `python-delphi-lsp` first, then use only `delphi_codebase` for Delphi and
@@ -264,6 +264,9 @@ class AgentWorkspace:
264
264
  return f"workspace_v2_{fingerprint}"
265
265
 
266
266
  def select_project(self, project_id: str) -> None:
267
+ self._select_project_with_revision(project_id)
268
+
269
+ def _select_project_with_revision(self, project_id: str) -> str:
267
270
  if project_id not in self._project_paths:
268
271
  raise AgentProtocolError("project_not_found", f"Project not found: {project_id}.")
269
272
  project_path = self._project_paths[project_id]
@@ -280,7 +283,7 @@ class AgentWorkspace:
280
283
  fingerprint = _selection_fingerprint(discovery, cached.result, root=self._root)
281
284
  if fingerprint == cached.fingerprint:
282
285
  self._activate_project(project_id, discovery, cached.result)
283
- return
286
+ return f"workspace_v2_{fingerprint}"
284
287
 
285
288
  if project_path is None:
286
289
  result = _catalog_workspace_sources(discovery)
@@ -292,11 +295,13 @@ class AgentWorkspace:
292
295
  source_transform=_outline_agent_source,
293
296
  )
294
297
  result = indexer.index(str(project_path))
298
+ fingerprint = _selection_fingerprint(discovery, result, root=self._root)
295
299
  self._project_cache[project_id] = _ProjectCache(
296
300
  result=result,
297
- fingerprint=_selection_fingerprint(discovery, result, root=self._root),
301
+ fingerprint=fingerprint,
298
302
  )
299
303
  self._activate_project(project_id, discovery, result)
304
+ return f"workspace_v2_{fingerprint}"
300
305
 
301
306
  def _activate_project(
302
307
  self,