indent 0.1.7__tar.gz → 0.1.9__tar.gz

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.

Potentially problematic release.


This version of indent might be problematic. Click here for more details.

Files changed (58) hide show
  1. {indent-0.1.7 → indent-0.1.9}/.gitignore +5 -1
  2. {indent-0.1.7 → indent-0.1.9}/PKG-INFO +2 -2
  3. indent-0.1.9/exponent/__init__.py +21 -0
  4. {indent-0.1.7 → indent-0.1.9}/exponent/cli.py +8 -10
  5. {indent-0.1.7 → indent-0.1.9}/exponent/commands/config_commands.py +10 -10
  6. {indent-0.1.7 → indent-0.1.9}/exponent/commands/run_commands.py +1 -1
  7. {indent-0.1.7 → indent-0.1.9}/exponent/commands/upgrade.py +3 -3
  8. {indent-0.1.7 → indent-0.1.9}/exponent/commands/utils.py +0 -90
  9. {indent-0.1.7 → indent-0.1.9}/exponent/commands/workflow_commands.py +1 -1
  10. {indent-0.1.7 → indent-0.1.9}/exponent/core/config.py +2 -0
  11. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/queries.py +13 -0
  12. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/subscriptions.py +13 -0
  13. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/cli_rpc_types.py +25 -1
  14. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/client.py +79 -1
  15. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/files.py +0 -12
  16. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/http_fetch.py +23 -15
  17. indent-0.1.9/exponent/core/remote_execution/system_context.py +27 -0
  18. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/types.py +0 -50
  19. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/utils.py +3 -98
  20. {indent-0.1.7 → indent-0.1.9}/pyproject.toml +12 -4
  21. indent-0.1.7/exponent/__init__.py +0 -1
  22. indent-0.1.7/exponent/commands/github_app_commands.py +0 -211
  23. indent-0.1.7/exponent/commands/listen_commands.py +0 -96
  24. indent-0.1.7/exponent/commands/shell_commands.py +0 -2840
  25. indent-0.1.7/exponent/commands/theme.py +0 -246
  26. indent-0.1.7/exponent/core/remote_execution/system_context.py +0 -54
  27. {indent-0.1.7 → indent-0.1.9}/exponent/commands/cloud_commands.py +0 -0
  28. {indent-0.1.7 → indent-0.1.9}/exponent/commands/common.py +0 -0
  29. {indent-0.1.7 → indent-0.1.9}/exponent/commands/settings.py +0 -0
  30. {indent-0.1.7 → indent-0.1.9}/exponent/commands/types.py +0 -0
  31. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/__init__.py +0 -0
  32. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/client.py +0 -0
  33. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/cloud_config_queries.py +0 -0
  34. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/get_chats_query.py +0 -0
  35. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/github_config_queries.py +0 -0
  36. {indent-0.1.7 → indent-0.1.9}/exponent/core/graphql/mutations.py +0 -0
  37. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/checkpoints.py +0 -0
  38. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/code_execution.py +0 -0
  39. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/error_info.py +0 -0
  40. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/exceptions.py +0 -0
  41. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/file_write.py +0 -0
  42. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/git.py +0 -0
  43. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/languages/python_execution.py +0 -0
  44. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
  45. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/languages/types.py +0 -0
  46. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/session.py +0 -0
  47. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/tool_execution.py +0 -0
  48. {indent-0.1.7 → indent-0.1.9}/exponent/core/remote_execution/truncation.py +0 -0
  49. {indent-0.1.7 → indent-0.1.9}/exponent/core/types/__init__.py +0 -0
  50. {indent-0.1.7 → indent-0.1.9}/exponent/core/types/command_data.py +0 -0
  51. {indent-0.1.7 → indent-0.1.9}/exponent/core/types/event_types.py +0 -0
  52. {indent-0.1.7 → indent-0.1.9}/exponent/core/types/generated/__init__.py +0 -0
  53. {indent-0.1.7 → indent-0.1.9}/exponent/core/types/generated/strategy_info.py +0 -0
  54. {indent-0.1.7 → indent-0.1.9}/exponent/migration-docs/login.md +0 -0
  55. {indent-0.1.7 → indent-0.1.9}/exponent/py.typed +0 -0
  56. {indent-0.1.7 → indent-0.1.9}/exponent/utils/__init__.py +0 -0
  57. {indent-0.1.7 → indent-0.1.9}/exponent/utils/colors.py +0 -0
  58. {indent-0.1.7 → indent-0.1.9}/exponent/utils/version.py +0 -0
@@ -9,10 +9,11 @@ __pycache__/
9
9
  .env.production
10
10
  .env.*_db
11
11
  .gcp-sa-credentials.json
12
- .indent-gh-app.pem
12
+ *.pem
13
13
  workflow_input.json
14
14
  .anthropic.log.json
15
15
  CLAUDE.local.md
16
+ indent.local.md
16
17
 
17
18
  # C extensions
18
19
  *.so
@@ -174,3 +175,6 @@ private-key.pem
174
175
 
175
176
  # Claude Code
176
177
  .claude
178
+
179
+ # Generated version files
180
+ python_modules/exponent/exponent/__init__.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indent
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Indent is an AI Pair Programmer
5
5
  Author-email: Sashank Thupukari <sashank@exponent.run>
6
6
  Requires-Python: <3.13,>=3.10
@@ -15,7 +15,7 @@ Requires-Dist: eval-type-backport<0.3,>=0.2.0
15
15
  Requires-Dist: git-python>=1.0.3
16
16
  Requires-Dist: gitignore-parser<0.2,>=0.1.11
17
17
  Requires-Dist: gql[httpx,websockets]<4,>=3.5.0
18
- Requires-Dist: httpx<0.28,>=0.27.0
18
+ Requires-Dist: httpx>=0.28.1
19
19
  Requires-Dist: ipykernel<7,>=6.29.4
20
20
  Requires-Dist: jupyter-client<9,>=8.6.1
21
21
  Requires-Dist: msgspec>=0.19.0
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.1.9'
21
+ __version_tuple__ = version_tuple = (0, 1, 9)
@@ -4,10 +4,7 @@ from click import Context, HelpFormatter
4
4
  from exponent.commands.cloud_commands import cloud_cli
5
5
  from exponent.commands.common import set_log_level
6
6
  from exponent.commands.config_commands import config_cli
7
- from exponent.commands.github_app_commands import github_app_cli
8
- from exponent.commands.listen_commands import listen_cli
9
- from exponent.commands.run_commands import run_cli
10
- from exponent.commands.shell_commands import shell_cli
7
+ from exponent.commands.run_commands import run, run_cli
11
8
  from exponent.commands.types import ExponentGroup, exponent_cli_group
12
9
  from exponent.commands.upgrade import upgrade_cli
13
10
  from exponent.commands.workflow_commands import workflow_cli
@@ -16,26 +13,27 @@ from exponent.utils.version import (
16
13
  )
17
14
 
18
15
 
19
- @exponent_cli_group()
16
+ @exponent_cli_group(invoke_without_command=True)
20
17
  @click.version_option(get_installed_version(), prog_name="Indent CLI")
21
18
  @click.pass_context
22
19
  def cli(ctx: Context) -> None:
23
20
  """
24
21
  Indent: Your AI pair programmer.
25
22
 
26
- Run indent run to start
23
+ Run indent to start (or indent run)
27
24
  """
28
25
  set_log_level()
29
26
 
27
+ # If no command is provided, invoke the 'run' command
28
+ if ctx.invoked_subcommand is None:
29
+ ctx.invoke(run)
30
+
30
31
 
31
32
  sources: list[ExponentGroup] = [
32
33
  config_cli, # Configuration commands
33
34
  run_cli, # Run AI chat commands
34
- shell_cli, # Shell interaction commands
35
35
  upgrade_cli, # Upgrade-related commands
36
36
  cloud_cli, # Cloud commands
37
- listen_cli, # Listen to chat events
38
- github_app_cli, # Setup github app installation
39
37
  workflow_cli, # Workflow commands
40
38
  ]
41
39
 
@@ -99,7 +97,7 @@ def hidden(ctx: Context) -> None:
99
97
  with formatter.section("Usage"):
100
98
  if ctx.parent and ctx.parent.command:
101
99
  formatter.write_usage(
102
- ctx.parent.command.name or "exponent", "COMMAND [ARGS]..."
100
+ ctx.parent.command.name or "indent", "COMMAND [ARGS]..."
103
101
  )
104
102
  formatter.write_paragraph()
105
103
  with formatter.indentation():
@@ -36,7 +36,7 @@ from exponent.utils.version import (
36
36
 
37
37
  @exponent_cli_group()
38
38
  def config_cli() -> None:
39
- """Manage Exponent configuration settings."""
39
+ """Manage Indent configuration settings."""
40
40
  pass
41
41
 
42
42
 
@@ -133,12 +133,12 @@ async def repos_for_github_config_task(
133
133
  @click.option(
134
134
  "--set-git-warning-disabled",
135
135
  is_flag=True,
136
- help="Disable the git warning for running Exponent from a non-git repository",
136
+ help="Disable the git warning for running Indent from a non-git repository",
137
137
  )
138
138
  @click.option(
139
139
  "--set-git-warning-enabled",
140
140
  is_flag=True,
141
- help="Enable the git warning for running Exponent from a non-git repository",
141
+ help="Enable the git warning for running Indent from a non-git repository",
142
142
  )
143
143
  @click.option(
144
144
  "--set-auto-upgrade-enabled",
@@ -198,7 +198,7 @@ def config(
198
198
  set_base_api_url_override: str | None = None,
199
199
  set_base_ws_url_override: str | None = None,
200
200
  ) -> None:
201
- """Display current Exponent configuration."""
201
+ """Display current Indent configuration."""
202
202
 
203
203
  num_options_set = sum(
204
204
  [
@@ -230,7 +230,7 @@ def config(
230
230
  settings.write_settings_to_config_file()
231
231
 
232
232
  click.secho(
233
- "Git warning enabled. Exponent will now check for a git repository.\n",
233
+ "Git warning enabled. Indent will now check for a git repository.\n",
234
234
  fg="yellow",
235
235
  )
236
236
 
@@ -239,7 +239,7 @@ def config(
239
239
  settings.write_settings_to_config_file()
240
240
 
241
241
  click.secho(
242
- "Git warning disabled. Exponent will no longer check for a git repository.\n",
242
+ "Git warning disabled. Indent will no longer check for a git repository.\n",
243
243
  fg="yellow",
244
244
  )
245
245
 
@@ -248,7 +248,7 @@ def config(
248
248
  settings.write_settings_to_config_file()
249
249
 
250
250
  click.secho(
251
- "Automatic upgrades enabled. Exponent will now check for updates.\n",
251
+ "Automatic upgrades enabled. Indent will now check for updates.\n",
252
252
  fg="yellow",
253
253
  )
254
254
 
@@ -257,7 +257,7 @@ def config(
257
257
  settings.write_settings_to_config_file()
258
258
 
259
259
  click.secho(
260
- "Automatic upgrades disabled. Exponent will no longer check for updates.\n",
260
+ "Automatic upgrades disabled. Indent will no longer check for updates.\n",
261
261
  fg="yellow",
262
262
  )
263
263
 
@@ -312,10 +312,10 @@ def config(
312
312
 
313
313
 
314
314
  @config_cli.command()
315
- @click.option("--key", help="Your Exponent API Key")
315
+ @click.option("--key", help="Your Indent API Key")
316
316
  @use_settings
317
317
  def login(settings: Settings, key: str) -> None:
318
- """Log in to Exponent."""
318
+ """Log in to Indent."""
319
319
 
320
320
  if not key:
321
321
  redirect_to_login(settings, "provided")
@@ -68,7 +68,7 @@ def run(
68
68
  prompt: str | None = None,
69
69
  workflow_id: str | None = None,
70
70
  ) -> None:
71
- """Start or reconnect to an Exponent session."""
71
+ """[default] Start or reconnect to an Indent session."""
72
72
  check_exponent_version_and_upgrade(settings)
73
73
 
74
74
  if not settings.api_key:
@@ -6,7 +6,7 @@ from exponent.utils.version import check_exponent_version, upgrade_exponent
6
6
 
7
7
  @exponent_cli_group()
8
8
  def upgrade_cli() -> None:
9
- """Manage Exponent version upgrades."""
9
+ """Manage Indent version upgrades."""
10
10
  pass
11
11
 
12
12
 
@@ -17,7 +17,7 @@ def upgrade_cli() -> None:
17
17
  help="Upgrade without prompting for confirmation, if a new version is available.",
18
18
  )
19
19
  def upgrade(force: bool = False) -> None:
20
- """Upgrade Exponent to the latest version."""
20
+ """Upgrade Indent to the latest version."""
21
21
  if result := check_exponent_version():
22
22
  installed_version, latest_version = result
23
23
  upgrade_exponent(
@@ -26,4 +26,4 @@ def upgrade(force: bool = False) -> None:
26
26
  force=force,
27
27
  )
28
28
  else:
29
- click.echo("Exponent is already up to date.")
29
+ click.echo("Indent is already up to date.")
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  import os
3
3
  import sys
4
- import threading
5
4
  import time
6
5
  import webbrowser
7
6
 
@@ -69,43 +68,6 @@ def launch_exponent_browser(
69
68
  webbrowser.open(url)
70
69
 
71
70
 
72
- def start_background_event_loop() -> asyncio.AbstractEventLoop:
73
- def run_event_loop(loop: asyncio.AbstractEventLoop) -> None:
74
- asyncio.set_event_loop(loop)
75
- loop.run_forever()
76
-
77
- loop = asyncio.new_event_loop()
78
- thread = threading.Thread(target=run_event_loop, args=(loop,), daemon=True)
79
- thread.start()
80
- return loop
81
-
82
-
83
- def read_input(prompt: str) -> str:
84
- sys.stdout.write(prompt)
85
- sys.stdout.flush()
86
- return sys.stdin.readline()
87
-
88
-
89
- def ask_for_quit_confirmation(program_name: str = "Exponent") -> bool:
90
- while True:
91
- try:
92
- answer = (
93
- input(f"Do you want to quit {program_name}? [y/\x1b[1mN\x1b[0m]")
94
- .strip()
95
- .lower()
96
- )
97
- if answer in {"y", "yes"}:
98
- return True
99
- elif answer in {"n", "no", ""}:
100
- return False
101
- except KeyboardInterrupt:
102
- print()
103
- return True
104
- except EOFError:
105
- print()
106
- return True
107
-
108
-
109
71
  class Spinner:
110
72
  def __init__(self, text: str) -> None:
111
73
  self.text = text
@@ -154,58 +116,6 @@ class Spinner:
154
116
  sys.stdout.flush()
155
117
 
156
118
 
157
- class ThinkingSpinner(Spinner):
158
- def __init__(
159
- self,
160
- fg_color: tuple[int, int, int] | None = None,
161
- text: str = "Exponent is thinking",
162
- ) -> None:
163
- super().__init__(text)
164
- self.fg_color = fg_color
165
- self.bold = True
166
- self.animation_chars = "⣾⣽⣻⢿⡿⣟⣯⣷" # More subtle animation
167
- self.animation_speed = 8 # Medium speed animation
168
- self.start_time = time.time() # Track when thinking started
169
-
170
- def show(self) -> None:
171
- if self.task is not None:
172
- return
173
-
174
- async def spinner(base_time: float) -> None:
175
- color_start = ""
176
- if self.fg_color:
177
- if isinstance(self.fg_color, tuple) and len(self.fg_color) == 3:
178
- r, g, b = self.fg_color
179
- color_start = f"\x1b[38;2;{r};{g};{b}m"
180
- elif isinstance(self.fg_color, int):
181
- color_start = f"\x1b[{30 + self.fg_color}m"
182
-
183
- bold_start = "\x1b[1m" if self.bold else ""
184
- style_start = f"{color_start}{bold_start}"
185
- style_end = "\x1b[0m"
186
-
187
- while True:
188
- t = time.time() - base_time
189
- i = round(t * self.animation_speed) % len(self.animation_chars)
190
-
191
- # Calculate elapsed seconds
192
- elapsed = time.time() - self.start_time
193
- if elapsed < 60:
194
- timer = f"{int(elapsed)}s"
195
- else:
196
- mins = int(elapsed // 60)
197
- secs = int(elapsed % 60)
198
- timer = f"{mins}m {secs}s"
199
-
200
- print(
201
- f"\r{style_start}{self.animation_chars[i]} {self.text} ({timer}){style_end}",
202
- end="",
203
- )
204
- await asyncio.sleep(0.1)
205
-
206
- self.task = asyncio.get_event_loop().create_task(spinner(self.base_time))
207
-
208
-
209
119
  class ConnectionTracker:
210
120
  def __init__(self) -> None:
211
121
  self.connected = True
@@ -22,7 +22,7 @@ def workflow_cli() -> None:
22
22
  pass
23
23
 
24
24
 
25
- @workflow_cli.group()
25
+ @workflow_cli.group(hidden=True)
26
26
  def workflow() -> None:
27
27
  """Workflow management commands."""
28
28
  pass
@@ -24,6 +24,8 @@ logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
26
  def is_editable_install() -> bool:
27
+ return True
28
+
27
29
  if os.getenv("ENVIRONMENT") == "test" or os.getenv("EXPONENT_TEST_AUTO_UPGRADE"):
28
30
  # We should explicitly set these variables
29
31
  # in test when needed
@@ -15,6 +15,19 @@ EVENTS_FOR_CHAT_QUERY: str = """query EventsForChat($chatUuid: UUID!) {
15
15
  ... on TextMessage {
16
16
  text
17
17
  }
18
+ ... on ToolCallMessage {
19
+ messageId
20
+ toolUseId
21
+ toolName
22
+ toolInput {
23
+ ... on BashToolInput {
24
+ command
25
+ }
26
+ ... on ReadToolInput {
27
+ filePath
28
+ }
29
+ }
30
+ }
18
31
  ... on ToolResultMessage {
19
32
  messageId
20
33
  toolUseId
@@ -76,6 +76,19 @@ INDENT_EVENTS_SUBSCRIPTION = """
76
76
  content
77
77
  }
78
78
  }
79
+ }
80
+ ... on ToolCallMessage {
81
+ messageId
82
+ toolUseId
83
+ toolName
84
+ toolInput {
85
+ ... on BashToolInput {
86
+ command
87
+ }
88
+ ... on ReadToolInput {
89
+ filePath
90
+ }
91
+ }
79
92
  }
80
93
  ... on PartialToolResultMessage {
81
94
  messageId
@@ -1,5 +1,6 @@
1
1
  import msgspec
2
2
  import yaml
3
+ from typing import Any
3
4
 
4
5
 
5
6
  class PartialToolResult(msgspec.Struct, tag_field="tool_name", omit_defaults=True):
@@ -9,6 +10,17 @@ class PartialToolResult(msgspec.Struct, tag_field="tool_name", omit_defaults=Tru
9
10
  class ToolInput(msgspec.Struct, tag_field="tool_name", omit_defaults=True):
10
11
  """Concrete subclasses describe the full input schema for a tool."""
11
12
 
13
+ def to_llm(self) -> dict[str, Any]:
14
+ """Convert ToolInput to LLM-friendly typed dict format.
15
+
16
+ Returns a dictionary with the tool parameters, excluding the tool_name
17
+ which is handled separately by the LLM integration layer.
18
+
19
+ Returns:
20
+ self by default, which msgspec will serialize appropriately.
21
+ """
22
+ return msgspec.to_builtins(self) # type: ignore[no-any-return]
23
+
12
24
 
13
25
  class ToolResult(msgspec.Struct, tag_field="tool_name", omit_defaults=True):
14
26
  """Concrete subclasses return data from a tool execution."""
@@ -107,6 +119,7 @@ class GrepToolResult(ToolResult, tag=GREP_TOOL_NAME):
107
119
  EDIT_TOOL_NAME = "edit"
108
120
 
109
121
 
122
+ # This is only used in the CLI. The server side type is edit_tool.py
110
123
  class EditToolInput(ToolInput, tag=EDIT_TOOL_NAME):
111
124
  file_path: str
112
125
  old_string: str
@@ -139,13 +152,13 @@ class BashToolResult(ToolResult, tag=BASH_TOOL_NAME):
139
152
  stopped_by_user: bool
140
153
 
141
154
 
142
-
143
155
  class HttpRequest(msgspec.Struct, tag="http_fetch_cli"):
144
156
  url: str
145
157
  method: str = "GET"
146
158
  headers: dict[str, str] | None = None
147
159
  timeout: int | None = None
148
160
 
161
+
149
162
  class HttpResponse(msgspec.Struct, tag="http_fetch_cli"):
150
163
  status_code: int | None = None
151
164
  content: str | None = None
@@ -153,6 +166,7 @@ class HttpResponse(msgspec.Struct, tag="http_fetch_cli"):
153
166
  duration_ms: int | None = None
154
167
  headers: dict[str, str] | None = None
155
168
 
169
+
156
170
  ToolInputType = (
157
171
  ReadToolInput
158
172
  | WriteToolInput
@@ -188,6 +202,10 @@ class TerminateRequest(msgspec.Struct, tag="terminate"):
188
202
  pass
189
203
 
190
204
 
205
+ class SwitchCLIChatRequest(msgspec.Struct, tag="switch_cli_chat"):
206
+ new_chat_uuid: str
207
+
208
+
191
209
  class BatchToolExecutionRequest(msgspec.Struct, tag="batch_tool_execution"):
192
210
  tool_inputs: list[ToolInputType]
193
211
 
@@ -204,6 +222,10 @@ class BatchToolExecutionResponse(msgspec.Struct, tag="batch_tool_execution"):
204
222
  tool_results: list[ToolResultType]
205
223
 
206
224
 
225
+ class SwitchCLIChatResponse(msgspec.Struct, tag="switch_cli_chat"):
226
+ pass
227
+
228
+
207
229
  class CliRpcRequest(msgspec.Struct):
208
230
  request_id: str
209
231
  request: (
@@ -212,6 +234,7 @@ class CliRpcRequest(msgspec.Struct):
212
234
  | TerminateRequest
213
235
  | HttpRequest
214
236
  | BatchToolExecutionRequest
237
+ | SwitchCLIChatRequest
215
238
  )
216
239
 
217
240
 
@@ -232,4 +255,5 @@ class CliRpcResponse(msgspec.Struct):
232
255
  | TerminateResponse
233
256
  | BatchToolExecutionResponse
234
257
  | HttpResponse
258
+ | SwitchCLIChatResponse
235
259
  )
@@ -36,6 +36,8 @@ from exponent.core.remote_execution.cli_rpc_types import (
36
36
  ToolExecutionRequest,
37
37
  ToolExecutionResponse,
38
38
  ToolResultType,
39
+ SwitchCLIChatRequest,
40
+ SwitchCLIChatResponse,
39
41
  )
40
42
  from exponent.core.remote_execution.code_execution import (
41
43
  execute_code_streaming,
@@ -150,7 +152,63 @@ class RemoteExecutionClient:
150
152
  return None
151
153
 
152
154
  data = json.dumps(msg_data["data"])
153
- request = msgspec.json.decode(data, type=CliRpcRequest)
155
+ try:
156
+ request = msgspec.json.decode(data, type=CliRpcRequest)
157
+ except msgspec.DecodeError as e:
158
+ # Try and decode to get request_id if possible
159
+ request = msgspec.json.decode(data)
160
+ if isinstance(request, dict) and "request_id" in request:
161
+ request_id = request["request_id"]
162
+ if (
163
+ request.get("request", {}).get("type", {}) == "tool_execution"
164
+ ) and (
165
+ "tool_input" in request["request"]
166
+ and "tool_name" in request["request"]["tool_input"]
167
+ ):
168
+ tool_name = request["request"]["tool_input"]["tool_name"]
169
+ logger.error(
170
+ f"Error tool {tool_name} received in a request."
171
+ "Please ensure you are running the latest version of Indent. If this issue persists, please contact support."
172
+ )
173
+ await websocket.send(
174
+ json.dumps(
175
+ {
176
+ "type": "result",
177
+ "data": msgspec.to_builtins(
178
+ CliRpcResponse(
179
+ request_id=request_id,
180
+ response=ErrorResponse(
181
+ error_message=f"Unknown tool: {tool_name}. If you are running an older version of Indent, please upgrade to the latest version to ensure compatibility."
182
+ ),
183
+ )
184
+ ),
185
+ }
186
+ )
187
+ )
188
+ else:
189
+ logger.error(
190
+ "Error decoding cli rpc request. Please ensure you are running the latest version of Indent."
191
+ )
192
+ await websocket.send(
193
+ json.dumps(
194
+ {
195
+ "type": "result",
196
+ "data": msgspec.to_builtins(
197
+ CliRpcResponse(
198
+ request_id=request_id,
199
+ response=ErrorResponse(
200
+ error_message=f"Unknown cli rpc request type: {request}",
201
+ ),
202
+ )
203
+ ),
204
+ }
205
+ )
206
+ )
207
+
208
+ return None
209
+ else:
210
+ # If we couldn't get a request_id, re-raise and fail noisily
211
+ raise e
154
212
 
155
213
  if isinstance(request.request, TerminateRequest):
156
214
  await self.halt_all_code_executions()
@@ -168,6 +226,21 @@ class RemoteExecutionClient:
168
226
  )
169
227
  )
170
228
  return None
229
+ elif isinstance(request.request, SwitchCLIChatRequest):
230
+ await websocket.send(
231
+ json.dumps(
232
+ {
233
+ "type": "result",
234
+ "data": msgspec.to_builtins(
235
+ CliRpcResponse(
236
+ request_id=request.request_id,
237
+ response=SwitchCLIChatResponse(),
238
+ )
239
+ ),
240
+ }
241
+ )
242
+ )
243
+ return SwitchCLIChat(new_chat_uuid=request.request.new_chat_uuid)
171
244
  else:
172
245
  if isinstance(request.request, ToolExecutionRequest) and isinstance(
173
246
  request.request.tool_input, BashToolInput
@@ -502,6 +575,11 @@ class RemoteExecutionClient:
502
575
  "TerminateRequest should not be handled by handle_request"
503
576
  )
504
577
 
578
+ elif isinstance(request.request, SwitchCLIChatRequest):
579
+ raise ValueError(
580
+ "SwitchCLIChatRequest should not be handled by handle_request"
581
+ )
582
+
505
583
  raise ValueError(f"Unhandled request type: {type(request)}")
506
584
 
507
585
  except Exception as e:
@@ -10,8 +10,6 @@ from exponent.core.remote_execution.cli_rpc_types import ErrorToolResult, GrepTo
10
10
  from exponent.core.remote_execution.types import (
11
11
  FileAttachment,
12
12
  FilePath,
13
- GetAllTrackedFilesRequest,
14
- GetAllTrackedFilesResponse,
15
13
  GetFileAttachmentRequest,
16
14
  GetFileAttachmentResponse,
17
15
  GetFileAttachmentsRequest,
@@ -209,16 +207,6 @@ async def get_matching_files(
209
207
  )
210
208
 
211
209
 
212
- async def get_all_tracked_files(
213
- request: GetAllTrackedFilesRequest,
214
- working_directory: str,
215
- ) -> GetAllTrackedFilesResponse:
216
- return GetAllTrackedFilesResponse(
217
- correlation_id=request.correlation_id,
218
- files=await get_all_non_ignored_files(working_directory),
219
- )
220
-
221
-
222
210
  async def search_files(
223
211
  path_str: str,
224
212
  file_pattern: str | None,