sigit-code 1.5.5__tar.gz → 1.5.6__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.
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/AGENTS.md +22 -5
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/sigit-code-release/SKILL.md +15 -4
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/sigit-code-release/SKILL.md +15 -4
- {sigit_code-1.5.5 → sigit_code-1.5.6}/AGENTS.md +22 -5
- {sigit_code-1.5.5 → sigit_code-1.5.6}/CHANGELOG.md +29 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/CLAUDE.md +22 -5
- {sigit_code-1.5.5 → sigit_code-1.5.6}/Cargo.lock +1 -1
- {sigit_code-1.5.5 → sigit_code-1.5.6}/Cargo.toml +1 -1
- {sigit_code-1.5.5 → sigit_code-1.5.6}/PKG-INFO +1 -1
- {sigit_code-1.5.5 → sigit_code-1.5.6}/docs/mcp.md +5 -3
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/backend.rs +202 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/headless.rs +17 -37
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/main.rs +194 -43
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/provider.rs +18 -5
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/tools.rs +40 -12
- {sigit_code-1.5.5 → sigit_code-1.5.6}/tests/acp_permissions.rs +231 -7
- {sigit_code-1.5.5 → sigit_code-1.5.6}/tests/headless_mode.rs +59 -0
- sigit_code-1.5.5/.github/workflows/release-mcp-registry.yml +0 -79
- sigit_code-1.5.5/server.json +0 -39
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.agents/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.claude/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/ci.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-aur.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-crates.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-github.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-homebrew.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-npm.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-nuget.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-pypi.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-scoop.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.github/workflows/release-winget.yml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.gitignore +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/.nvmrc +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/LICENSE +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/README.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/docs/hooks.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/settings-with-hooks.toml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/skills/README.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/examples/skills/commit-message/SKILL.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/README.md.tmpl +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/package-main.json.tmpl +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/package.json.tmpl +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/scripts/render-main-package.cjs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/scripts/render-platform-package.cjs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/.gitignore +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/README.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/package.json +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/src/index.ts +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/npm/sigit/tsconfig.json +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/.gitignore +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/Program.cs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/README.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/nuget/sigit/SiGit.Code.csproj +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/aur/PKGBUILD.in +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/nfpm.yaml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/pypi/README.md +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/pypi/pyproject.toml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/pyproject.toml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/rust-toolchain.toml +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/account.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/browser_auth.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/chat.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/commands.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/credentials.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/frontmatter.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/hooks.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/instructions.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/mcp.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/models.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/permissions.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/session_store.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/settings.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/setup.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/skills.rs +0 -0
- {sigit_code-1.5.5 → sigit_code-1.5.6}/src/subagents.rs +0 -0
|
@@ -26,7 +26,23 @@ from the name alone (e.g. `feature/tool-permission-system`, `fix/glob-mtime-sort
|
|
|
26
26
|
a task, ticket, or session id (not `feature/task-q003hm`).
|
|
27
27
|
|
|
28
28
|
**IMPORTANT — pull request target:** Always open pull requests against the `development` branch,
|
|
29
|
-
never `main`. `main` is release-only; `development` is where day-to-day work integrates
|
|
29
|
+
never `main`. `main` is release-only; `development` is where day-to-day work integrates, and the
|
|
30
|
+
release merge (see the `sigit-code-release` skill) is the one thing that ever puts commits on
|
|
31
|
+
`main`.
|
|
32
|
+
|
|
33
|
+
This rule needs help to hold, because the repository's default branch is `main`: both
|
|
34
|
+
`gh pr create` and the GitHub web form pre-select it, so a PR lands on the wrong base unless the
|
|
35
|
+
base is passed explicitly. Pass it every time, and check that it took:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
gh pr create --base development --head <branch>
|
|
39
|
+
gh pr view <number> --json baseRefName
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Retargeting a PR that was opened against `main` costs nothing before it merges:
|
|
43
|
+
`gh pr edit <number> --base development`. After it merges it costs real work — the change sits on
|
|
44
|
+
`main` while `development`, which the next release is cut from, does not have it, and someone has
|
|
45
|
+
to carry it back by hand. PR #65 went in that way.
|
|
30
46
|
|
|
31
47
|
**IMPORTANT — branch off `development`:** Start every working branch from the latest
|
|
32
48
|
`origin/development`. Exception: when new work *depends on* a feature branch that has not merged
|
|
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
|
|
|
182
198
|
`tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
|
|
183
199
|
(excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
|
|
184
200
|
server is *also* listed in the public MCP Registry as `si.sigit/sigit` — a **remote**
|
|
185
|
-
Streamable-HTTP listing
|
|
186
|
-
`release-mcp-registry.yml`)
|
|
187
|
-
forces a domain namespace (`si.sigit` ↔
|
|
188
|
-
GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
201
|
+
Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
|
|
202
|
+
(`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
|
|
203
|
+
it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
|
|
204
|
+
`sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
205
|
+
package listing.
|
|
189
206
|
- **`src/permissions.rs`** — tool permission policy. Every tool call passes through
|
|
190
207
|
`decision_for` before executing: read-only tools always run; mutating tools (and all
|
|
191
208
|
`mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
|
|
@@ -29,8 +29,13 @@ Use this skill when preparing a release for this repository.
|
|
|
29
29
|
- Add or update the top changelog entry in `CHANGELOG.md` for the release being cut.
|
|
30
30
|
- Do not treat `npm/sigit/package.json` `0.0.0-dev` as a bug by default. The npm release workflow rewrites it at publish time using `npm/scripts/render-main-package.cjs` and the release tag.
|
|
31
31
|
- Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `Cargo.toml`.
|
|
32
|
-
- Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, and `release-
|
|
32
|
+
- Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-nuget.yml`, and `release-mcp-registry.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
|
|
33
33
|
- The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
|
|
34
|
+
- `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
|
|
35
|
+
- Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
|
|
36
|
+
- npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
|
|
37
|
+
- The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
|
|
38
|
+
- The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
|
|
34
39
|
|
|
35
40
|
## Git release flow
|
|
36
41
|
|
|
@@ -42,6 +47,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
|
|
|
42
47
|
4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
|
|
43
48
|
5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
|
|
44
49
|
|
|
50
|
+
Step 4 is the only way commits are meant to reach `main`. A feature or fix PR targeting `main` breaks that: it puts work on the release branch that `development` does not have, so the next release is cut without it. Such PRs belong on `development` (see the pull request target rule in `AGENTS.md`).
|
|
51
|
+
|
|
45
52
|
Pushing the `v*.*.*` tag is what fires every release workflow, so create and push it only after the merge into `main` has landed. Do not commit, tag, or push until the user explicitly asks — confirm the version and that they want the release to go out first.
|
|
46
53
|
|
|
47
54
|
## Typical files to inspect
|
|
@@ -54,7 +61,10 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
|
|
|
54
61
|
- `npm/scripts/render-main-package.cjs`
|
|
55
62
|
- `npm/`
|
|
56
63
|
- `pypi/`
|
|
57
|
-
-
|
|
64
|
+
- `nuget/sigit/` (bundled-binary `.NET` tool packaging)
|
|
65
|
+
- `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
|
|
66
|
+
- `server.json` (MCP Registry listing)
|
|
67
|
+
- `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-scoop.yml`, `release-winget.yml`, `release-aur.yml`, `release-nuget.yml`, `release-mcp-registry.yml`)
|
|
58
68
|
|
|
59
69
|
## Release checklist
|
|
60
70
|
|
|
@@ -67,5 +77,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
|
|
|
67
77
|
- Git flow followed: bump committed on `release/v<version>`, merged back to `development`, then `development` merged into `main`, with `v<version>` tagged on the `main` merge commit.
|
|
68
78
|
- Release notes or changelog entries match the actual changes.
|
|
69
79
|
- CI-equivalent local checks pass for the relevant platform or target.
|
|
70
|
-
- Package names, install commands, and branding stay consistent.
|
|
71
|
-
-
|
|
80
|
+
- Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
|
|
81
|
+
- Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
|
|
82
|
+
- Any known release limitations are called out explicitly, including a dispatch failure in `release-scoop`/`release-winget`/`release-aur` (non-fatal — logged as a warning by `release-github.yml`) versus a hard failure in a directly tag-triggered workflow like `release-nuget` or `release-mcp-registry`.
|
|
@@ -29,8 +29,13 @@ Use this skill when preparing a release for this repository.
|
|
|
29
29
|
- Add or update the top changelog entry in `CHANGELOG.md` for the release being cut.
|
|
30
30
|
- Do not treat `npm/sigit/package.json` `0.0.0-dev` as a bug by default. The npm release workflow rewrites it at publish time using `npm/scripts/render-main-package.cjs` and the release tag.
|
|
31
31
|
- Do not add a hardcoded version to `pypi/pyproject.toml` for normal releases. PyPI uses `maturin` with `dynamic = ["version"]` and derives the published package version from `Cargo.toml`.
|
|
32
|
-
- Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, and `release-
|
|
32
|
+
- Release workflows are tag-driven. `release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-nuget.yml`, and `release-mcp-registry.yml` all derive `RELEASE_VERSION` from a `v*.*.*` tag or a manually supplied tag input.
|
|
33
33
|
- The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
|
|
34
|
+
- `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
|
|
35
|
+
- Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
|
|
36
|
+
- npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
|
|
37
|
+
- The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
|
|
38
|
+
- The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
|
|
34
39
|
|
|
35
40
|
## Git release flow
|
|
36
41
|
|
|
@@ -42,6 +47,8 @@ Releases are cut from `development` and shipped on `main`. The published tags (`
|
|
|
42
47
|
4. Merge `development` into `main` (commit message: `Merge development into main for v<version> release`).
|
|
43
48
|
5. Tag `v<version>` on that `main` merge commit, then push `main` and the tag.
|
|
44
49
|
|
|
50
|
+
Step 4 is the only way commits are meant to reach `main`. A feature or fix PR targeting `main` breaks that: it puts work on the release branch that `development` does not have, so the next release is cut without it. Such PRs belong on `development` (see the pull request target rule in `AGENTS.md`).
|
|
51
|
+
|
|
45
52
|
Pushing the `v*.*.*` tag is what fires every release workflow, so create and push it only after the merge into `main` has landed. Do not commit, tag, or push until the user explicitly asks — confirm the version and that they want the release to go out first.
|
|
46
53
|
|
|
47
54
|
## Typical files to inspect
|
|
@@ -54,7 +61,10 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
|
|
|
54
61
|
- `npm/scripts/render-main-package.cjs`
|
|
55
62
|
- `npm/`
|
|
56
63
|
- `pypi/`
|
|
57
|
-
-
|
|
64
|
+
- `nuget/sigit/` (bundled-binary `.NET` tool packaging)
|
|
65
|
+
- `packaging/` (`nfpm.yaml` for `.deb`/`.rpm`, plus `aur/` and `winget/` templates)
|
|
66
|
+
- `server.json` (MCP Registry listing)
|
|
67
|
+
- `.github/workflows/` (`release-github.yml`, `release-npm.yml`, `release-pypi.yml`, `release-crates.yml`, `release-homebrew.yml`, `release-scoop.yml`, `release-winget.yml`, `release-aur.yml`, `release-nuget.yml`, `release-mcp-registry.yml`)
|
|
58
68
|
|
|
59
69
|
## Release checklist
|
|
60
70
|
|
|
@@ -67,5 +77,6 @@ Pushing the `v*.*.*` tag is what fires every release workflow, so create and pus
|
|
|
67
77
|
- Git flow followed: bump committed on `release/v<version>`, merged back to `development`, then `development` merged into `main`, with `v<version>` tagged on the `main` merge commit.
|
|
68
78
|
- Release notes or changelog entries match the actual changes.
|
|
69
79
|
- CI-equivalent local checks pass for the relevant platform or target.
|
|
70
|
-
- Package names, install commands, and branding stay consistent.
|
|
71
|
-
-
|
|
80
|
+
- Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
|
|
81
|
+
- Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
|
|
82
|
+
- Any known release limitations are called out explicitly, including a dispatch failure in `release-scoop`/`release-winget`/`release-aur` (non-fatal — logged as a warning by `release-github.yml`) versus a hard failure in a directly tag-triggered workflow like `release-nuget` or `release-mcp-registry`.
|
|
@@ -26,7 +26,23 @@ from the name alone (e.g. `feature/tool-permission-system`, `fix/glob-mtime-sort
|
|
|
26
26
|
a task, ticket, or session id (not `feature/task-q003hm`).
|
|
27
27
|
|
|
28
28
|
**IMPORTANT — pull request target:** Always open pull requests against the `development` branch,
|
|
29
|
-
never `main`. `main` is release-only; `development` is where day-to-day work integrates
|
|
29
|
+
never `main`. `main` is release-only; `development` is where day-to-day work integrates, and the
|
|
30
|
+
release merge (see the `sigit-code-release` skill) is the one thing that ever puts commits on
|
|
31
|
+
`main`.
|
|
32
|
+
|
|
33
|
+
This rule needs help to hold, because the repository's default branch is `main`: both
|
|
34
|
+
`gh pr create` and the GitHub web form pre-select it, so a PR lands on the wrong base unless the
|
|
35
|
+
base is passed explicitly. Pass it every time, and check that it took:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
gh pr create --base development --head <branch>
|
|
39
|
+
gh pr view <number> --json baseRefName
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Retargeting a PR that was opened against `main` costs nothing before it merges:
|
|
43
|
+
`gh pr edit <number> --base development`. After it merges it costs real work — the change sits on
|
|
44
|
+
`main` while `development`, which the next release is cut from, does not have it, and someone has
|
|
45
|
+
to carry it back by hand. PR #65 went in that way.
|
|
30
46
|
|
|
31
47
|
**IMPORTANT — branch off `development`:** Start every working branch from the latest
|
|
32
48
|
`origin/development`. Exception: when new work *depends on* a feature branch that has not merged
|
|
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
|
|
|
182
198
|
`tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
|
|
183
199
|
(excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
|
|
184
200
|
server is *also* listed in the public MCP Registry as `si.sigit/sigit` — a **remote**
|
|
185
|
-
Streamable-HTTP listing
|
|
186
|
-
`release-mcp-registry.yml`)
|
|
187
|
-
forces a domain namespace (`si.sigit` ↔
|
|
188
|
-
GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
201
|
+
Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
|
|
202
|
+
(`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
|
|
203
|
+
it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
|
|
204
|
+
`sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
205
|
+
package listing.
|
|
189
206
|
- **`src/permissions.rs`** — tool permission policy. Every tool call passes through
|
|
190
207
|
`decision_for` before executing: read-only tools always run; mutating tools (and all
|
|
191
208
|
`mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.6
|
|
4
|
+
|
|
5
|
+
### What changed
|
|
6
|
+
|
|
7
|
+
- **Switching models mid-conversation no longer restarts it.** Both switching
|
|
8
|
+
to a local GGUF model and picking a different cloud tier now carry the live
|
|
9
|
+
conversation history across, repairing any half-finished tool call so
|
|
10
|
+
strict OpenAI-compatible endpoints don't reject the session
|
|
11
|
+
- **Long-running tools now show progress in ACP clients.** Synchronous tools
|
|
12
|
+
(shell commands, file operations, `read_website`, ...) run on a blocking
|
|
13
|
+
thread pool instead of the ACP connection's own task, so a slow command's
|
|
14
|
+
`in_progress` update reaches the client — and Zed's spinner — while the
|
|
15
|
+
command is still running instead of arriving with the result
|
|
16
|
+
- **Text from consecutive tool rounds no longer runs together.** ACP and
|
|
17
|
+
headless clients concatenate consecutive message chunks, so a reply that
|
|
18
|
+
continued after a tool call used to glue onto the previous round's last
|
|
19
|
+
sentence with no space. A blank line now opens the next round's text
|
|
20
|
+
- **New onde-cloud model tiers.** The model picker (`/models` in the TUI, the
|
|
21
|
+
config panel in editors) now offers five additional siGit Code Cloud tiers —
|
|
22
|
+
`flux`, `apex`, `aura`, `orbit`, and `nova` — alongside the existing Fast,
|
|
23
|
+
Balanced, Large, Mini, Air, Pro, and KKK options. The tier list mirrors
|
|
24
|
+
onde-cloud's router catalogue, so all three sides (the picker, the router,
|
|
25
|
+
and the cloud catalogue) have to be updated together when a tier is added
|
|
26
|
+
- **MCP Registry listing moved to `getsigit/si`.** The `server.json` manifest
|
|
27
|
+
and its `release-mcp-registry.yml` publish workflow now live in the
|
|
28
|
+
[`getsigit/si`](https://github.com/getsigit/si) repo alongside the si CLI's
|
|
29
|
+
other code, since the registry entry describes the si platform rather than
|
|
30
|
+
the coding agent
|
|
31
|
+
|
|
3
32
|
## 1.5.5
|
|
4
33
|
|
|
5
34
|
A distribution release. siGit Code itself is unchanged. What moved is how it
|
|
@@ -26,7 +26,23 @@ from the name alone (e.g. `feature/tool-permission-system`, `fix/glob-mtime-sort
|
|
|
26
26
|
a task, ticket, or session id (not `feature/task-q003hm`).
|
|
27
27
|
|
|
28
28
|
**IMPORTANT — pull request target:** Always open pull requests against the `development` branch,
|
|
29
|
-
never `main`. `main` is release-only; `development` is where day-to-day work integrates
|
|
29
|
+
never `main`. `main` is release-only; `development` is where day-to-day work integrates, and the
|
|
30
|
+
release merge (see the `sigit-code-release` skill) is the one thing that ever puts commits on
|
|
31
|
+
`main`.
|
|
32
|
+
|
|
33
|
+
This rule needs help to hold, because the repository's default branch is `main`: both
|
|
34
|
+
`gh pr create` and the GitHub web form pre-select it, so a PR lands on the wrong base unless the
|
|
35
|
+
base is passed explicitly. Pass it every time, and check that it took:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
gh pr create --base development --head <branch>
|
|
39
|
+
gh pr view <number> --json baseRefName
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Retargeting a PR that was opened against `main` costs nothing before it merges:
|
|
43
|
+
`gh pr edit <number> --base development`. After it merges it costs real work — the change sits on
|
|
44
|
+
`main` while `development`, which the next release is cut from, does not have it, and someone has
|
|
45
|
+
to carry it back by hand. PR #65 went in that way.
|
|
30
46
|
|
|
31
47
|
**IMPORTANT — branch off `development`:** Start every working branch from the latest
|
|
32
48
|
`origin/development`. Exception: when new work *depends on* a feature branch that has not merged
|
|
@@ -182,10 +198,11 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
|
|
|
182
198
|
`tests/mcp_stdio.rs`, driven by the test-only `src/bin/mcp_stdio_stub.rs` helper binary
|
|
183
199
|
(excluded from the published crate via `exclude` in `Cargo.toml`). The baked-in official
|
|
184
200
|
server is *also* listed in the public MCP Registry as `si.sigit/sigit` — a **remote**
|
|
185
|
-
Streamable-HTTP listing
|
|
186
|
-
`release-mcp-registry.yml`)
|
|
187
|
-
forces a domain namespace (`si.sigit` ↔
|
|
188
|
-
GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
201
|
+
Streamable-HTTP listing owned by the [`getsigit/si`](https://github.com/getsigit/si) repo
|
|
202
|
+
(`server.json` at its root, published by its `release-mcp-registry.yml`), not this one. Because
|
|
203
|
+
it's a remote server, the registry's URL-match rule forces a domain namespace (`si.sigit` ↔
|
|
204
|
+
`sigit.si`) verified by a DNS TXT record, not the GitHub-OIDC scheme `smbcloud-cli` uses for its
|
|
205
|
+
package listing.
|
|
189
206
|
- **`src/permissions.rs`** — tool permission policy. Every tool call passes through
|
|
190
207
|
`decision_for` before executing: read-only tools always run; mutating tools (and all
|
|
191
208
|
`mcp__*`/unknown tools) are governed by, in order: per-session plan mode (`/plan` — deny all
|
|
@@ -63,6 +63,8 @@ The server is published to the
|
|
|
63
63
|
[official MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
64
64
|
**`si.sigit/sigit`**, a remote Streamable-HTTP listing, so registry-aware
|
|
65
65
|
clients can add it in one click. The registry only stores metadata; the
|
|
66
|
-
`server.json` and publish workflow live in
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
`server.json` and publish workflow live in the
|
|
67
|
+
[`getsigit/si`](https://github.com/getsigit/si) repo (the `si` CLI, also an
|
|
68
|
+
MCP client of this server), and namespace ownership is proven by a DNS TXT
|
|
69
|
+
record on `sigit.si`. The internal setup and release runbook live with the
|
|
70
|
+
server (private `sigit-si` repo).
|
|
@@ -880,6 +880,89 @@ struct StreamFunctionDelta {
|
|
|
880
880
|
arguments: Option<String>,
|
|
881
881
|
}
|
|
882
882
|
|
|
883
|
+
// ── Carrying a conversation across a model switch ────────────────────────────
|
|
884
|
+
|
|
885
|
+
/// The part of a history snapshot that survives a model switch: every
|
|
886
|
+
/// non-system message, with half-finished tool plumbing repaired.
|
|
887
|
+
///
|
|
888
|
+
/// System messages are dropped because the backend being switched *to* seeds
|
|
889
|
+
/// its own — a different model's prompt, or a freshly pushed session-context
|
|
890
|
+
/// message. A switch can land mid-turn, after the assistant asked for a tool
|
|
891
|
+
/// but before the results came back; a tool call with no matching result (or a
|
|
892
|
+
/// result with no matching call) makes strict OpenAI-compatible endpoints
|
|
893
|
+
/// reject every later request in the session, so those halves are stripped
|
|
894
|
+
/// instead of carried.
|
|
895
|
+
pub fn carryover_history(snapshot: Vec<serde_json::Value>) -> Vec<serde_json::Value> {
|
|
896
|
+
let answered: std::collections::HashSet<String> = snapshot
|
|
897
|
+
.iter()
|
|
898
|
+
.filter(|message| message["role"] == "tool")
|
|
899
|
+
.filter_map(|message| message["tool_call_id"].as_str().map(str::to_string))
|
|
900
|
+
.collect();
|
|
901
|
+
|
|
902
|
+
let mut carried: Vec<serde_json::Value> = Vec::with_capacity(snapshot.len());
|
|
903
|
+
let mut called: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
904
|
+
|
|
905
|
+
for mut message in snapshot {
|
|
906
|
+
match message["role"].as_str().unwrap_or_default() {
|
|
907
|
+
"system" => continue,
|
|
908
|
+
"assistant" => {
|
|
909
|
+
if let Some(calls) = message["tool_calls"].as_array() {
|
|
910
|
+
let kept: Vec<serde_json::Value> = calls
|
|
911
|
+
.iter()
|
|
912
|
+
.filter(|call| call["id"].as_str().is_some_and(|id| answered.contains(id)))
|
|
913
|
+
.cloned()
|
|
914
|
+
.collect();
|
|
915
|
+
if kept.is_empty() {
|
|
916
|
+
// Only unanswered requests: keep whatever text came
|
|
917
|
+
// with them, drop the message if there was none.
|
|
918
|
+
if let Some(object) = message.as_object_mut() {
|
|
919
|
+
object.remove("tool_calls");
|
|
920
|
+
}
|
|
921
|
+
if message["content"].as_str().unwrap_or_default().is_empty() {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
for call in &kept {
|
|
926
|
+
called.insert(call["id"].as_str().unwrap_or_default().to_string());
|
|
927
|
+
}
|
|
928
|
+
message["tool_calls"] = serde_json::Value::Array(kept);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
carried.push(message);
|
|
932
|
+
}
|
|
933
|
+
"tool" => {
|
|
934
|
+
let answers_a_kept_call = message["tool_call_id"]
|
|
935
|
+
.as_str()
|
|
936
|
+
.is_some_and(|id| called.contains(id));
|
|
937
|
+
if answers_a_kept_call {
|
|
938
|
+
carried.push(message);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
_ => carried.push(message),
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
carried
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/// Replay `carried` (from [`carryover_history`]) into `backend`, on top of the
|
|
949
|
+
/// system messages `backend` seeded for itself. Used when a model switch
|
|
950
|
+
/// installs a new backend — or reloads the on-device engine, which wipes its
|
|
951
|
+
/// history — so the thread continues under the new model instead of restarting.
|
|
952
|
+
pub async fn adopt_carryover(backend: &dyn InferenceBackend, carried: Vec<serde_json::Value>) {
|
|
953
|
+
if carried.is_empty() {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
let mut rebuilt: Vec<serde_json::Value> = backend
|
|
957
|
+
.history_snapshot()
|
|
958
|
+
.await
|
|
959
|
+
.into_iter()
|
|
960
|
+
.take_while(|message| message["role"] == "system")
|
|
961
|
+
.collect();
|
|
962
|
+
rebuilt.extend(carried);
|
|
963
|
+
backend.restore_history(rebuilt).await;
|
|
964
|
+
}
|
|
965
|
+
|
|
883
966
|
#[cfg(test)]
|
|
884
967
|
mod tests {
|
|
885
968
|
use super::*;
|
|
@@ -968,6 +1051,125 @@ mod tests {
|
|
|
968
1051
|
assert_eq!(last["content"], "cancelled by the user");
|
|
969
1052
|
}
|
|
970
1053
|
|
|
1054
|
+
#[test]
|
|
1055
|
+
fn carryover_drops_system_messages_and_keeps_the_turns() {
|
|
1056
|
+
let snapshot = vec![
|
|
1057
|
+
serde_json::json!({ "role": "system", "content": "old prompt" }),
|
|
1058
|
+
serde_json::json!({ "role": "user", "content": "hello" }),
|
|
1059
|
+
serde_json::json!({ "role": "assistant", "content": "hi" }),
|
|
1060
|
+
];
|
|
1061
|
+
|
|
1062
|
+
let carried = carryover_history(snapshot);
|
|
1063
|
+
|
|
1064
|
+
assert_eq!(carried.len(), 2);
|
|
1065
|
+
assert_eq!(carried[0]["role"], "user");
|
|
1066
|
+
assert_eq!(carried[1]["content"], "hi");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
#[test]
|
|
1070
|
+
fn carryover_strips_a_tool_call_that_never_got_a_result() {
|
|
1071
|
+
// Switching mid-turn: the assistant asked for a tool, nothing answered.
|
|
1072
|
+
let snapshot = vec![
|
|
1073
|
+
serde_json::json!({ "role": "user", "content": "read a.rs" }),
|
|
1074
|
+
streamed_assistant_history(
|
|
1075
|
+
"on it",
|
|
1076
|
+
&[ToolCall {
|
|
1077
|
+
id: "call_1".to_string(),
|
|
1078
|
+
name: "read_file".to_string(),
|
|
1079
|
+
arguments: "{}".to_string(),
|
|
1080
|
+
}],
|
|
1081
|
+
),
|
|
1082
|
+
];
|
|
1083
|
+
|
|
1084
|
+
let carried = carryover_history(snapshot);
|
|
1085
|
+
|
|
1086
|
+
assert_eq!(carried.len(), 2);
|
|
1087
|
+
assert_eq!(carried[1]["content"], "on it");
|
|
1088
|
+
assert!(
|
|
1089
|
+
carried[1].get("tool_calls").is_none(),
|
|
1090
|
+
"an unanswered tool call must not survive the switch"
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
#[test]
|
|
1095
|
+
fn carryover_drops_a_textless_unanswered_tool_call_and_its_late_result() {
|
|
1096
|
+
let snapshot = vec![
|
|
1097
|
+
streamed_assistant_history(
|
|
1098
|
+
"",
|
|
1099
|
+
&[ToolCall {
|
|
1100
|
+
id: "call_1".to_string(),
|
|
1101
|
+
name: "read_file".to_string(),
|
|
1102
|
+
arguments: "{}".to_string(),
|
|
1103
|
+
}],
|
|
1104
|
+
),
|
|
1105
|
+
// An orphan: its assistant message is gone with the line above.
|
|
1106
|
+
serde_json::json!({
|
|
1107
|
+
"role": "tool",
|
|
1108
|
+
"tool_call_id": "call_other",
|
|
1109
|
+
"content": "file contents",
|
|
1110
|
+
}),
|
|
1111
|
+
];
|
|
1112
|
+
|
|
1113
|
+
assert!(carryover_history(snapshot).is_empty());
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
#[test]
|
|
1117
|
+
fn carryover_keeps_an_answered_tool_call_paired_with_its_result() {
|
|
1118
|
+
let snapshot = vec![
|
|
1119
|
+
streamed_assistant_history(
|
|
1120
|
+
"",
|
|
1121
|
+
&[ToolCall {
|
|
1122
|
+
id: "call_1".to_string(),
|
|
1123
|
+
name: "read_file".to_string(),
|
|
1124
|
+
arguments: "{}".to_string(),
|
|
1125
|
+
}],
|
|
1126
|
+
),
|
|
1127
|
+
serde_json::json!({
|
|
1128
|
+
"role": "tool",
|
|
1129
|
+
"tool_call_id": "call_1",
|
|
1130
|
+
"content": "file contents",
|
|
1131
|
+
}),
|
|
1132
|
+
];
|
|
1133
|
+
|
|
1134
|
+
let carried = carryover_history(snapshot);
|
|
1135
|
+
|
|
1136
|
+
assert_eq!(carried.len(), 2);
|
|
1137
|
+
assert_eq!(carried[0]["tool_calls"][0]["id"], "call_1");
|
|
1138
|
+
assert_eq!(carried[1]["tool_call_id"], "call_1");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
#[tokio::test]
|
|
1142
|
+
async fn adopt_carryover_replays_the_thread_under_the_new_system_prompt() {
|
|
1143
|
+
let new_backend =
|
|
1144
|
+
OpenAiBackend::new("http://localhost", "", "m", Some("new prompt".into()));
|
|
1145
|
+
|
|
1146
|
+
let carried = carryover_history(vec![
|
|
1147
|
+
serde_json::json!({ "role": "system", "content": "old prompt" }),
|
|
1148
|
+
serde_json::json!({ "role": "user", "content": "hello" }),
|
|
1149
|
+
serde_json::json!({ "role": "assistant", "content": "hi" }),
|
|
1150
|
+
]);
|
|
1151
|
+
adopt_carryover(&new_backend, carried).await;
|
|
1152
|
+
|
|
1153
|
+
let history = new_backend.history_snapshot().await;
|
|
1154
|
+
assert_eq!(history.len(), 3);
|
|
1155
|
+
assert_eq!(history[0]["role"], "system");
|
|
1156
|
+
assert_eq!(history[0]["content"], "new prompt");
|
|
1157
|
+
assert_eq!(history[1]["content"], "hello");
|
|
1158
|
+
assert_eq!(history[2]["content"], "hi");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
#[tokio::test]
|
|
1162
|
+
async fn adopt_carryover_leaves_a_fresh_backend_alone() {
|
|
1163
|
+
let new_backend =
|
|
1164
|
+
OpenAiBackend::new("http://localhost", "", "m", Some("new prompt".into()));
|
|
1165
|
+
|
|
1166
|
+
adopt_carryover(&new_backend, Vec::new()).await;
|
|
1167
|
+
|
|
1168
|
+
let history = new_backend.history_snapshot().await;
|
|
1169
|
+
assert_eq!(history.len(), 1);
|
|
1170
|
+
assert_eq!(history[0]["content"], "new prompt");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
971
1173
|
#[test]
|
|
972
1174
|
fn estimate_tokens_scales_with_serialized_size() {
|
|
973
1175
|
assert_eq!(estimate_tokens(&[]), 0);
|
|
@@ -228,16 +228,12 @@ async fn run_prompt(
|
|
|
228
228
|
// sink is passed and only the final message is printed.
|
|
229
229
|
let (sink, mut sink_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
|
230
230
|
let sink_opt = if config.quiet { None } else { Some(&sink) };
|
|
231
|
-
let mut
|
|
232
|
-
let mut sent = String::new();
|
|
233
|
-
let mut streamed_any = false;
|
|
231
|
+
let mut reply = crate::StreamedReply::default();
|
|
234
232
|
|
|
235
233
|
let mut result = drain_to_stdout(
|
|
236
234
|
backend.send_message_with_tools(&config.prompt, &tools, sink_opt),
|
|
237
235
|
&mut sink_rx,
|
|
238
|
-
&mut
|
|
239
|
-
&mut sent,
|
|
240
|
-
&mut streamed_any,
|
|
236
|
+
&mut reply,
|
|
241
237
|
)
|
|
242
238
|
.await?;
|
|
243
239
|
|
|
@@ -300,12 +296,14 @@ async fn run_prompt(
|
|
|
300
296
|
None // last round: force text
|
|
301
297
|
};
|
|
302
298
|
|
|
299
|
+
// Whatever this round says starts a new paragraph rather than
|
|
300
|
+
// continuing the sentence the tool calls interrupted.
|
|
301
|
+
reply.interrupt();
|
|
302
|
+
|
|
303
303
|
result = drain_to_stdout(
|
|
304
304
|
backend.send_tool_results(tool_results, next_tools, sink_opt),
|
|
305
305
|
&mut sink_rx,
|
|
306
|
-
&mut
|
|
307
|
-
&mut sent,
|
|
308
|
-
&mut streamed_any,
|
|
306
|
+
&mut reply,
|
|
309
307
|
)
|
|
310
308
|
.await?;
|
|
311
309
|
}
|
|
@@ -318,7 +316,7 @@ async fn run_prompt(
|
|
|
318
316
|
if !final_visible.is_empty() {
|
|
319
317
|
let _ = writeln!(stdout, "{final_visible}");
|
|
320
318
|
}
|
|
321
|
-
} else if streamed_any {
|
|
319
|
+
} else if reply.streamed_any {
|
|
322
320
|
// The reply is already on stdout; end the line for the shell.
|
|
323
321
|
let _ = writeln!(stdout);
|
|
324
322
|
} else if !final_visible.is_empty() {
|
|
@@ -335,9 +333,7 @@ async fn run_prompt(
|
|
|
335
333
|
async fn drain_to_stdout<F>(
|
|
336
334
|
fut: F,
|
|
337
335
|
sink_rx: &mut tokio::sync::mpsc::UnboundedReceiver<String>,
|
|
338
|
-
|
|
339
|
-
sent: &mut String,
|
|
340
|
-
streamed_any: &mut bool,
|
|
336
|
+
reply: &mut crate::StreamedReply,
|
|
341
337
|
) -> Result<TurnResult, backend::BackendError>
|
|
342
338
|
where
|
|
343
339
|
F: std::future::Future<Output = Result<TurnResult, backend::BackendError>>,
|
|
@@ -347,39 +343,23 @@ where
|
|
|
347
343
|
tokio::select! {
|
|
348
344
|
done = &mut fut => break done,
|
|
349
345
|
Some(piece) = sink_rx.recv() => {
|
|
350
|
-
emit_visible_chunk(&piece,
|
|
346
|
+
emit_visible_chunk(&piece, reply);
|
|
351
347
|
}
|
|
352
348
|
}
|
|
353
349
|
};
|
|
354
350
|
// Flush tokens that landed between the last poll and the future resolving.
|
|
355
351
|
while let Ok(piece) = sink_rx.try_recv() {
|
|
356
|
-
emit_visible_chunk(&piece,
|
|
352
|
+
emit_visible_chunk(&piece, reply);
|
|
357
353
|
}
|
|
358
354
|
result
|
|
359
355
|
}
|
|
360
356
|
|
|
361
|
-
///
|
|
362
|
-
///
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
assembled: &mut String,
|
|
368
|
-
sent: &mut String,
|
|
369
|
-
streamed_any: &mut bool,
|
|
370
|
-
) {
|
|
371
|
-
assembled.push_str(piece);
|
|
372
|
-
let (_think, visible) = crate::chat::strip_think_blocks(assembled);
|
|
373
|
-
match visible.strip_prefix(sent.as_str()) {
|
|
374
|
-
Some(extra) if !extra.is_empty() => {
|
|
375
|
-
print!("{extra}");
|
|
376
|
-
let _ = std::io::stdout().flush();
|
|
377
|
-
*sent = visible;
|
|
378
|
-
*streamed_any = true;
|
|
379
|
-
}
|
|
380
|
-
// No new visible text, or the visible prefix changed retroactively
|
|
381
|
-
// (rare, e.g. a late-closing think tag): resync without reprinting.
|
|
382
|
-
_ => *sent = visible,
|
|
357
|
+
/// Fold a streamed fragment into `reply` and print whatever it newly reveals —
|
|
358
|
+
/// the stdio counterpart of `SiGitAgent::emit_visible_chunk`.
|
|
359
|
+
fn emit_visible_chunk(piece: &str, reply: &mut crate::StreamedReply) {
|
|
360
|
+
if let Some(extra) = reply.push(piece) {
|
|
361
|
+
print!("{extra}");
|
|
362
|
+
let _ = std::io::stdout().flush();
|
|
383
363
|
}
|
|
384
364
|
}
|
|
385
365
|
|