sigit-code 1.6.6__tar.gz → 1.6.7__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.
Files changed (117) hide show
  1. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/AGENTS.md +5 -1
  2. {sigit_code-1.6.6 → sigit_code-1.6.7}/AGENTS.md +5 -1
  3. {sigit_code-1.6.6 → sigit_code-1.6.7}/CHANGELOG.md +12 -1
  4. {sigit_code-1.6.6 → sigit_code-1.6.7}/CLAUDE.md +5 -1
  5. {sigit_code-1.6.6 → sigit_code-1.6.7}/Cargo.lock +1 -1
  6. {sigit_code-1.6.6 → sigit_code-1.6.7}/Cargo.toml +1 -1
  7. {sigit_code-1.6.6 → sigit_code-1.6.7}/PKG-INFO +1 -1
  8. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/main.rs +138 -15
  9. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_audio.rs +45 -0
  10. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
  11. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
  12. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/branding/SKILL.md +0 -0
  13. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/run-sigit/SKILL.md +0 -0
  14. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/run-sigit/driver.mjs +0 -0
  15. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
  16. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/sigit-code-release/SKILL.md +0 -0
  17. {sigit_code-1.6.6 → sigit_code-1.6.7}/.agents/skills/tool-calling/SKILL.md +0 -0
  18. {sigit_code-1.6.6 → sigit_code-1.6.7}/.cargo/config.toml +0 -0
  19. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
  20. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
  21. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/branding/SKILL.md +0 -0
  22. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/run-sigit/SKILL.md +0 -0
  23. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/run-sigit/driver.mjs +0 -0
  24. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
  25. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/sigit-code-release/SKILL.md +0 -0
  26. {sigit_code-1.6.6 → sigit_code-1.6.7}/.claude/skills/tool-calling/SKILL.md +0 -0
  27. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/ci.yml +0 -0
  28. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-aur.yml +0 -0
  29. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-crates.yml +0 -0
  30. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-github.yml +0 -0
  31. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-homebrew.yml +0 -0
  32. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-npm.yml +0 -0
  33. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-nuget.yml +0 -0
  34. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-pypi.yml +0 -0
  35. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-scoop.yml +0 -0
  36. {sigit_code-1.6.6 → sigit_code-1.6.7}/.github/workflows/release-winget.yml +0 -0
  37. {sigit_code-1.6.6 → sigit_code-1.6.7}/.gitignore +0 -0
  38. {sigit_code-1.6.6 → sigit_code-1.6.7}/.nvmrc +0 -0
  39. {sigit_code-1.6.6 → sigit_code-1.6.7}/.sigit/code-cloud-agent/config.toml +0 -0
  40. {sigit_code-1.6.6 → sigit_code-1.6.7}/.sigit/code-review/config.toml +0 -0
  41. {sigit_code-1.6.6 → sigit_code-1.6.7}/.sigit/factory/config.toml +0 -0
  42. {sigit_code-1.6.6 → sigit_code-1.6.7}/LICENSE +0 -0
  43. {sigit_code-1.6.6 → sigit_code-1.6.7}/README.md +0 -0
  44. {sigit_code-1.6.6 → sigit_code-1.6.7}/docs/headless.md +0 -0
  45. {sigit_code-1.6.6 → sigit_code-1.6.7}/docs/hooks.md +0 -0
  46. {sigit_code-1.6.6 → sigit_code-1.6.7}/docs/mcp.md +0 -0
  47. {sigit_code-1.6.6 → sigit_code-1.6.7}/examples/settings-with-hooks.toml +0 -0
  48. {sigit_code-1.6.6 → sigit_code-1.6.7}/examples/skills/README.md +0 -0
  49. {sigit_code-1.6.6 → sigit_code-1.6.7}/examples/skills/commit-message/SKILL.md +0 -0
  50. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/README.md.tmpl +0 -0
  51. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/package-compat.json.tmpl +0 -0
  52. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/package-main.json.tmpl +0 -0
  53. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/package.json.tmpl +0 -0
  54. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/scripts/render-main-package.cjs +0 -0
  55. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/scripts/render-platform-package.cjs +0 -0
  56. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/sigit/.gitignore +0 -0
  57. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/sigit/README.md +0 -0
  58. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/sigit/package.json +0 -0
  59. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/sigit/src/index.ts +0 -0
  60. {sigit_code-1.6.6 → sigit_code-1.6.7}/npm/sigit/tsconfig.json +0 -0
  61. {sigit_code-1.6.6 → sigit_code-1.6.7}/nuget/.gitignore +0 -0
  62. {sigit_code-1.6.6 → sigit_code-1.6.7}/nuget/sigit/Program.cs +0 -0
  63. {sigit_code-1.6.6 → sigit_code-1.6.7}/nuget/sigit/README.md +0 -0
  64. {sigit_code-1.6.6 → sigit_code-1.6.7}/nuget/sigit/SiGit.Code.csproj +0 -0
  65. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/aur/PKGBUILD.in +0 -0
  66. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/nfpm.yaml +0 -0
  67. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/windows/check-static-crt.ps1 +0 -0
  68. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
  69. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
  70. {sigit_code-1.6.6 → sigit_code-1.6.7}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
  71. {sigit_code-1.6.6 → sigit_code-1.6.7}/pypi/README.md +0 -0
  72. {sigit_code-1.6.6 → sigit_code-1.6.7}/pypi/pyproject.toml +0 -0
  73. {sigit_code-1.6.6 → sigit_code-1.6.7}/pyproject.toml +0 -0
  74. {sigit_code-1.6.6 → sigit_code-1.6.7}/rust-toolchain.toml +0 -0
  75. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/account.rs +0 -0
  76. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/backend.rs +0 -0
  77. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/browser_auth.rs +0 -0
  78. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/chat.rs +0 -0
  79. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/client_fs.rs +0 -0
  80. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/client_terminal.rs +0 -0
  81. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/commands.rs +0 -0
  82. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/credentials.rs +0 -0
  83. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/frontmatter.rs +0 -0
  84. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/harness_markup.rs +0 -0
  85. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/headless.rs +0 -0
  86. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/hooks.rs +0 -0
  87. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/inline_tool_calls.rs +0 -0
  88. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/instructions.rs +0 -0
  89. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/mcp.rs +0 -0
  90. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/models.rs +0 -0
  91. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/permissions.rs +0 -0
  92. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/provider.rs +0 -0
  93. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/repetition.rs +0 -0
  94. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/session_store.rs +0 -0
  95. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/settings.rs +0 -0
  96. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/setup.rs +0 -0
  97. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/skills.rs +0 -0
  98. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/subagents.rs +0 -0
  99. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/tools.rs +0 -0
  100. {sigit_code-1.6.6 → sigit_code-1.6.7}/src/workspace.rs +0 -0
  101. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_client_fs.rs +0 -0
  102. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_client_terminal.rs +0 -0
  103. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_concurrent_sessions.rs +0 -0
  104. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_embedded_context.rs +0 -0
  105. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_endpoint_errors.rs +0 -0
  106. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_harness_markup.rs +0 -0
  107. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_images.rs +0 -0
  108. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_logout.rs +0 -0
  109. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_multi_root.rs +0 -0
  110. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_permissions.rs +0 -0
  111. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_session_list.rs +0 -0
  112. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_session_load.rs +0 -0
  113. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_stop_reasons.rs +0 -0
  114. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_tool_call_name.rs +0 -0
  115. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/acp_tool_stdin.rs +0 -0
  116. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/headless_mode.rs +0 -0
  117. {sigit_code-1.6.6 → sigit_code-1.6.7}/tests/version_flag.rs +0 -0
@@ -310,7 +310,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
310
310
  conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
311
311
  through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
312
312
  answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
313
- on-device turn never releases it. Take the session lock first, never the other way round. Tool
313
+ on-device turn never releases it. Take the session lock first, never the other way round. A turn registers its `PromptCancellation` at
314
+ the top of `handle_prompt`, before it waits on either lock, and a `PromptRegistration`
315
+ guard clears it on every return, so a `session/cancel` that arrives while the turn
316
+ is still queued, switching threads, loading a model or running a slash command ends
317
+ it as `cancelled`. The checks sit after each of those waits. Tool
314
318
  execution, subagents included, still runs one session at a time.
315
319
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
316
320
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
@@ -310,7 +310,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
310
310
  conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
311
311
  through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
312
312
  answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
313
- on-device turn never releases it. Take the session lock first, never the other way round. Tool
313
+ on-device turn never releases it. Take the session lock first, never the other way round. A turn registers its `PromptCancellation` at
314
+ the top of `handle_prompt`, before it waits on either lock, and a `PromptRegistration`
315
+ guard clears it on every return, so a `session/cancel` that arrives while the turn
316
+ is still queued, switching threads, loading a model or running a slash command ends
317
+ it as `cancelled`. The checks sit after each of those waits. Tool
314
318
  execution, subagents included, still runs one session at a time.
315
319
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
316
320
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 1.6.7
4
+
5
+ ### Fixed
6
+
7
+ - **A cancel that arrives before the turn starts is no longer lost.** A prompt
8
+ now registers for `session/cancel` as soon as it begins, not once it reaches
9
+ inference. A cancel that lands while the turn is queued on the workspace,
10
+ switching threads, loading a model, or running a slash command ends it with
11
+ `cancelled` instead of `end_turn`.
12
+ - **A prompt that is only audio is answered.** A clip with no text or image used
13
+ to end the turn at once with nothing sent to the model, even though the agent
14
+ advertises audio.
4
15
 
5
16
  ## 1.6.6
6
17
 
@@ -310,7 +310,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
310
310
  conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
311
311
  through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
312
312
  answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
313
- on-device turn never releases it. Take the session lock first, never the other way round. Tool
313
+ on-device turn never releases it. Take the session lock first, never the other way round. A turn registers its `PromptCancellation` at
314
+ the top of `handle_prompt`, before it waits on either lock, and a `PromptRegistration`
315
+ guard clears it on every return, so a `session/cancel` that arrives while the turn
316
+ is still queued, switching threads, loading a model or running a slash command ends
317
+ it as `cancelled`. The checks sit after each of those waits. Tool
314
318
  execution, subagents included, still runs one session at a time.
315
319
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
316
320
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
@@ -5901,7 +5901,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
5901
5901
 
5902
5902
  [[package]]
5903
5903
  name = "sigit"
5904
- version = "1.6.6"
5904
+ version = "1.6.7"
5905
5905
  dependencies = [
5906
5906
  "agent-client-protocol",
5907
5907
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sigit"
3
- version = "1.6.6"
3
+ version = "1.6.7"
4
4
  edition = "2024"
5
5
  description = "siGit Code — ACP-compatible AI coding agent. Sí, git."
6
6
  documentation = "https://github.com/getsigit/sigit"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sigit-code
3
- Version: 1.6.6
3
+ Version: 1.6.7
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -465,6 +465,21 @@ impl PromptCancellation {
465
465
  }
466
466
  }
467
467
 
468
+ /// Removes a turn's cancellation from the agent's registry when the turn ends,
469
+ /// whichever way it returns.
470
+ struct PromptRegistration<'a> {
471
+ agent: &'a SiGitAgent,
472
+ session_id: SessionId,
473
+ cancellation: Arc<PromptCancellation>,
474
+ }
475
+
476
+ impl Drop for PromptRegistration<'_> {
477
+ fn drop(&mut self) {
478
+ self.agent
479
+ .finish_prompt(&self.session_id, &self.cancellation);
480
+ }
481
+ }
482
+
468
483
  enum DrainTurnError {
469
484
  Backend(backend::BackendError),
470
485
  Cancelled,
@@ -3063,10 +3078,31 @@ impl SiGitAgent {
3063
3078
  return Ok(PromptResponse::new(StopReason::Cancelled));
3064
3079
  }
3065
3080
 
3081
+ // Registered before anything that can wait, so a `session/cancel` that
3082
+ // lands while this turn is still queued on the workspace, switching
3083
+ // sessions or loading a model is seen. The session lock is held, so the
3084
+ // slot can only be this turn's. The guard clears it on every return.
3085
+ let cancellation = Arc::new(PromptCancellation::default());
3086
+ self.prompt_cancellations
3087
+ .lock()
3088
+ .unwrap()
3089
+ .insert(session_id.to_string(), Arc::clone(&cancellation));
3090
+ let _registration = PromptRegistration {
3091
+ agent: self,
3092
+ session_id: session_id.clone(),
3093
+ cancellation: Arc::clone(&cancellation),
3094
+ };
3095
+
3066
3096
  // Everything below (slash commands included) acts on the live session,
3067
3097
  // so it has to be this one. Resource links are read relative to its cwd.
3068
3098
  let mut workspace = WorkspaceHold::acquire(&self.workspace_lock).await;
3099
+ if cancellation.cancelled.load(Ordering::Acquire) {
3100
+ return Ok(PromptResponse::new(StopReason::Cancelled));
3101
+ }
3069
3102
  self.activate_session(&session_id).await?;
3103
+ if cancellation.cancelled.load(Ordering::Acquire) {
3104
+ return Ok(PromptResponse::new(StopReason::Cancelled));
3105
+ }
3070
3106
 
3071
3107
  // log every block so we can debug @ references and file context
3072
3108
  for (i, block) in args.prompt.iter().enumerate() {
@@ -3204,7 +3240,7 @@ impl SiGitAgent {
3204
3240
 
3205
3241
  let user_text = parts.join("\n");
3206
3242
 
3207
- if user_text.trim().is_empty() && images.is_empty() {
3243
+ if user_text.trim().is_empty() && images.is_empty() && audio.is_empty() {
3208
3244
  return Ok(PromptResponse::new(StopReason::EndTurn));
3209
3245
  }
3210
3246
 
@@ -3230,17 +3266,18 @@ impl SiGitAgent {
3230
3266
  match commands::resolve_command(&command) {
3231
3267
  Some(custom) => commands::render(&custom.body, argument.as_deref()),
3232
3268
  None => {
3233
- return exec_slash_acp(
3234
- self,
3235
- cx,
3236
- session_id,
3237
- SlashCommand::Unknown(command, argument),
3238
- )
3239
- .await;
3269
+ return self
3270
+ .run_slash(
3271
+ cx,
3272
+ session_id,
3273
+ SlashCommand::Unknown(command, argument),
3274
+ &cancellation,
3275
+ )
3276
+ .await;
3240
3277
  }
3241
3278
  }
3242
3279
  }
3243
- Some(command) => return exec_slash_acp(self, cx, session_id, command).await,
3280
+ Some(command) => return self.run_slash(cx, session_id, command, &cancellation).await,
3244
3281
  None => user_text,
3245
3282
  };
3246
3283
 
@@ -3267,7 +3304,12 @@ impl SiGitAgent {
3267
3304
  {
3268
3305
  if self.auto_load_local_model {
3269
3306
  self.start_startup_model_load_if_needed();
3270
- self.await_model_ready(cx, &session_id).await?;
3307
+ tokio::select! {
3308
+ ready = self.await_model_ready(cx, &session_id) => ready?,
3309
+ () = cancellation.cancelled() => {
3310
+ return Ok(PromptResponse::new(StopReason::Cancelled));
3311
+ }
3312
+ }
3271
3313
  } else {
3272
3314
  self.send_assistant_message(
3273
3315
  cx,
@@ -3280,11 +3322,9 @@ impl SiGitAgent {
3280
3322
  }
3281
3323
  }
3282
3324
 
3283
- let cancellation = Arc::new(PromptCancellation::default());
3284
- self.prompt_cancellations
3285
- .lock()
3286
- .unwrap()
3287
- .insert(session_id.to_string(), Arc::clone(&cancellation));
3325
+ if cancellation.cancelled.load(Ordering::Acquire) {
3326
+ return Ok(PromptResponse::new(StopReason::Cancelled));
3327
+ }
3288
3328
 
3289
3329
  // ── tool-calling loop ────────────────────────────────────────────
3290
3330
  // send message → execute any tool calls → feed results back
@@ -3987,6 +4027,24 @@ impl SiGitAgent {
3987
4027
  }
3988
4028
  }
3989
4029
 
4030
+ /// Run a built-in slash command as a turn. A `session/cancel` ends it with
4031
+ /// `Cancelled`; only `/compact` and `/load` take long enough to notice.
4032
+ async fn run_slash(
4033
+ &self,
4034
+ cx: &ConnectionTo<Client>,
4035
+ session_id: SessionId,
4036
+ command: SlashCommand,
4037
+ cancellation: &PromptCancellation,
4038
+ ) -> agent_client_protocol::Result<PromptResponse> {
4039
+ if cancellation.cancelled.load(Ordering::Acquire) {
4040
+ return Ok(PromptResponse::new(StopReason::Cancelled));
4041
+ }
4042
+ tokio::select! {
4043
+ response = exec_slash_acp(self, cx, session_id, command) => response,
4044
+ () = cancellation.cancelled() => Ok(PromptResponse::new(StopReason::Cancelled)),
4045
+ }
4046
+ }
4047
+
3990
4048
  fn finish_prompt(&self, session_id: &SessionId, cancellation: &Arc<PromptCancellation>) {
3991
4049
  let key = session_id.to_string();
3992
4050
  let mut active = self.prompt_cancellations.lock().unwrap();
@@ -6406,6 +6464,71 @@ mod tests {
6406
6464
  assert!(agent.active_session.lock().unwrap().is_none());
6407
6465
  }
6408
6466
 
6467
+ #[tokio::test]
6468
+ async fn a_turn_leaves_no_cancellation_behind_however_it_returns() {
6469
+ let agent = SiGitAgent::new(
6470
+ Arc::new(ChatEngine::new()),
6471
+ default_local_model_config(),
6472
+ Arc::new(AtomicBool::new(true)),
6473
+ Arc::new(AtomicBool::new(false)),
6474
+ Arc::new(std::sync::Mutex::new(None)),
6475
+ false,
6476
+ false,
6477
+ );
6478
+ let session_id = SessionId::new("early");
6479
+ let turn = Arc::new(PromptCancellation::default());
6480
+ agent
6481
+ .prompt_cancellations
6482
+ .lock()
6483
+ .unwrap()
6484
+ .insert("early".into(), Arc::clone(&turn));
6485
+ let registration = PromptRegistration {
6486
+ agent: &agent,
6487
+ session_id: session_id.clone(),
6488
+ cancellation: Arc::clone(&turn),
6489
+ };
6490
+
6491
+ // A cancel that arrives before the turn reaches inference is recorded
6492
+ // and stays visible to every check the turn makes afterwards.
6493
+ agent
6494
+ .handle_cancel(CancelNotification::new(session_id.clone()))
6495
+ .await
6496
+ .unwrap();
6497
+ assert!(turn.cancelled.load(Ordering::Acquire));
6498
+ tokio::time::timeout(std::time::Duration::from_millis(50), turn.cancelled())
6499
+ .await
6500
+ .expect("a pre-cancelled turn must resolve at once");
6501
+
6502
+ drop(registration);
6503
+ assert!(agent.prompt_cancellations.lock().unwrap().is_empty());
6504
+ }
6505
+
6506
+ #[tokio::test]
6507
+ async fn a_finished_turn_does_not_remove_its_successors_cancellation() {
6508
+ let agent = SiGitAgent::new(
6509
+ Arc::new(ChatEngine::new()),
6510
+ default_local_model_config(),
6511
+ Arc::new(AtomicBool::new(true)),
6512
+ Arc::new(AtomicBool::new(false)),
6513
+ Arc::new(std::sync::Mutex::new(None)),
6514
+ false,
6515
+ false,
6516
+ );
6517
+ let old = Arc::new(PromptCancellation::default());
6518
+ let new = Arc::new(PromptCancellation::default());
6519
+ agent
6520
+ .prompt_cancellations
6521
+ .lock()
6522
+ .unwrap()
6523
+ .insert("s".into(), Arc::clone(&new));
6524
+ drop(PromptRegistration {
6525
+ agent: &agent,
6526
+ session_id: SessionId::new("s"),
6527
+ cancellation: old,
6528
+ });
6529
+ assert!(agent.prompt_cancellations.lock().unwrap().contains_key("s"));
6530
+ }
6531
+
6409
6532
  #[tokio::test]
6410
6533
  async fn cancel_signals_only_the_matching_active_prompt() {
6411
6534
  let agent = SiGitAgent::new(
@@ -308,3 +308,48 @@ fn a_cloud_tier_gets_the_text_and_the_user_is_told() {
308
308
  "the user must be told the clip was left out: {updates}"
309
309
  );
310
310
  }
311
+
312
+ #[test]
313
+ fn an_audio_only_prompt_reaches_the_endpoint() {
314
+ let endpoint = start_fake_endpoint(vec![sse_text("heard it")]);
315
+ let scratch = std::env::temp_dir().join(format!("sigit_acp_audio_only_{}", std::process::id()));
316
+ let config_dir = scratch.join("config");
317
+ let project = scratch.join("project");
318
+ std::fs::create_dir_all(&config_dir).unwrap();
319
+ std::fs::create_dir_all(&project).unwrap();
320
+
321
+ let mut agent = spawn_agent(endpoint.port, &config_dir, "scripted-audio-model");
322
+ let id = agent.request(
323
+ "initialize",
324
+ json!({"protocolVersion": 1, "clientCapabilities": {}}),
325
+ );
326
+ agent.wait_for_response(id);
327
+ let id = agent.request("session/new", json!({"cwd": project, "mcpServers": []}));
328
+ let session_id = agent.wait_for_response(id)["result"]["sessionId"]
329
+ .as_str()
330
+ .expect("session id")
331
+ .to_string();
332
+
333
+ let id = agent.request(
334
+ "session/prompt",
335
+ json!({
336
+ "sessionId": session_id,
337
+ "prompt": [{"type": "audio", "mimeType": "audio/wav", "data": CLIP}],
338
+ }),
339
+ );
340
+ let response = agent.wait_for_response(id);
341
+ assert_eq!(response["result"]["stopReason"], "end_turn");
342
+
343
+ let requests = endpoint.requests.lock().unwrap();
344
+ let request = requests
345
+ .first()
346
+ .expect("a clip with no text is still a prompt, so the endpoint must see it");
347
+ assert!(
348
+ request.to_string().contains(CLIP),
349
+ "the clip must reach the endpoint: {request}"
350
+ );
351
+
352
+ drop(requests);
353
+ drop(agent);
354
+ let _ = std::fs::remove_dir_all(&scratch);
355
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes