From 5e215abdcde3a3dd4a99582b9e76a9c3996a308c Mon Sep 17 00:00:00 2001 From: lab1 Date: Sat, 30 May 2026 12:43:28 +0800 Subject: [PATCH 1/3] ci: add windows beta packaging path --- .github/workflows/release.yml | 47 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2df4c3503..62406f690 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,6 @@ jobs: target: # macOS now ships two single-arch shards so Apple Silicon and Intel # users get separate DMGs. `rust` and `tauri` stay aligned per shard. - - { os: ubuntu-22.04, label: "linux-x64", rust: "x86_64-unknown-linux-gnu", tauri: "x86_64-unknown-linux-gnu" } - - { os: macos-15-intel, label: "macos-x64", rust: "x86_64-apple-darwin", tauri: "x86_64-apple-darwin" } - - { os: macos-14, label: "macos-arm64", rust: "aarch64-apple-darwin", tauri: "aarch64-apple-darwin" } - { os: windows-latest, label: "windows-x64", rust: "x86_64-pc-windows-msvc", tauri: "x86_64-pc-windows-msvc" } runs-on: ${{ matrix.target.os }} # secrets.* isn't accessible from step-level if:, so expose presence as a @@ -116,27 +113,9 @@ jobs: test -f dist/index.html ls dist/assets/*.js >/dev/null - # tauri-action signs OS bundles whenever the corresponding env vars are - # defined — including when they're defined to the empty string, which is - # what `secrets.UNSET` evaluates to. Gate each signing block on its - # certificate being non-empty so unsigned prereleases don't trip - # `security import` / `signtool` on missing certs. - - name: Build Tauri bundle (unsigned) - if: >- - (matrix.target.os != 'windows-latest' || env.HAS_WIN_CERT != 'true') - && (!startsWith(matrix.target.os, 'macos-') || env.HAS_APPLE_CERT != 'true') - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - with: - projectPath: desktop - tagName: ${{ steps.tag.outputs.name }} - releaseName: Reasonix ${{ steps.tag.outputs.name }} - releaseDraft: true - prerelease: false - args: --target ${{ matrix.target.tauri }} + - name: Build Tauri bundle (Windows, unsigned) + working-directory: desktop + run: npm run tauri -- build --target ${{ matrix.target.tauri }} - name: Build Tauri bundle (Windows, signed) if: matrix.target.os == 'windows-latest' && env.HAS_WIN_CERT == 'true' @@ -155,6 +134,26 @@ jobs: prerelease: false args: --target ${{ matrix.target.tauri }} + - name: Upload Windows bundle artifact + uses: actions/upload-artifact@v4 + with: + name: reasonix-windows-x64-${{ steps.tag.outputs.name }} + path: | + desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi + desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe + + - name: Upload Windows bundle to draft release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.name }} + name: Reasonix ${{ steps.tag.outputs.name }} + target_commitish: ${{ github.sha }} + draft: true + prerelease: true + files: | + desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/msi/*.msi + desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe + - name: Build Tauri bundle (macOS, signed + notarized) if: startsWith(matrix.target.os, 'macos-') && env.HAS_APPLE_CERT == 'true' uses: tauri-apps/tauri-action@v0 From 2ebd147f55b235f1fd8dd842b0b9c7422df1e96e Mon Sep 17 00:00:00 2001 From: lab1 Date: Sat, 30 May 2026 12:54:48 +0800 Subject: [PATCH 2/3] ci: disable updater artifacts for unsigned windows beta --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62406f690..3326d452a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,6 +113,11 @@ jobs: test -f dist/index.html ls dist/assets/*.js >/dev/null + - name: Disable updater artifacts for unsigned Windows beta + shell: bash + run: | + node -e "const fs=require('fs'); const p='desktop/src-tauri/tauri.conf.json'; const c=JSON.parse(fs.readFileSync(p,'utf8')); c.bundle.createUpdaterArtifacts=false; fs.writeFileSync(p, JSON.stringify(c, null, 2) + '\n');" + - name: Build Tauri bundle (Windows, unsigned) working-directory: desktop run: npm run tauri -- build --target ${{ matrix.target.tauri }} From e1b6b064e07f0d8128324ae2f96d57c1a98a0ad9 Mon Sep 17 00:00:00 2001 From: lab1 Date: Sat, 30 May 2026 13:43:18 +0800 Subject: [PATCH 3/3] fix(desktop): remove mention token with closed chip --- desktop/src/ui/composer-at-popup.test.tsx | 92 +++++++++++++++++++++++ desktop/src/ui/composer.tsx | 29 ++++++- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/desktop/src/ui/composer-at-popup.test.tsx b/desktop/src/ui/composer-at-popup.test.tsx index 7a05fe89f..6ee517d71 100644 --- a/desktop/src/ui/composer-at-popup.test.tsx +++ b/desktop/src/ui/composer-at-popup.test.tsx @@ -253,4 +253,96 @@ describe("desktop Composer @ popup", () => { expect(typeof draftUpdate).toBe("function"); expect((draftUpdate as (current: string) => string)("@foo")).toBe("@src/very/deep/foo.ts "); }); + + it("removes the matching @ token when closing a picked mention chip", async () => { + const setDraft = vi.fn(); + const { container, rerender, onMentionQuery } = renderComposer({ + draft: "", + setDraft, + }); + const textarea = container.querySelector("textarea"); + if (!textarea) throw new Error("missing textarea"); + + fireEvent.change(textarea, { + target: { + value: "inspect @foo before send", + selectionStart: "inspect @foo".length, + selectionEnd: "inspect @foo".length, + }, + }); + await waitFor(() => expect(onMentionQuery).toHaveBeenCalled()); + const nonce = onMentionQuery.mock.calls[0]?.[1] as number; + + rerender( + ()} + slashCommands={[]} + onMentionQuery={onMentionQuery} + onMentionPreview={vi.fn()} + onMentionPicked={vi.fn()} + mentionResults={{ nonce, query: "foo", results: ["src/very/deep/foo.ts"] }} + workspaceDir="/repo" + />, + ); + + const row = await waitFor(() => { + const item = container.querySelector(".popup-item"); + expect(item).not.toBeNull(); + return item as HTMLElement; + }); + + fireEvent.click(row); + const pickDraftUpdate = setDraft.mock.calls.at(-1)?.[0]; + expect(typeof pickDraftUpdate).toBe("function"); + const pickedDraft = (pickDraftUpdate as (current: string) => string)( + "inspect @foo before send", + ); + expect(pickedDraft).toBe("inspect @src/very/deep/foo.ts before send"); + + rerender( + ()} + slashCommands={[]} + onMentionQuery={onMentionQuery} + onMentionPreview={vi.fn()} + onMentionPicked={vi.fn()} + mentionResults={null} + workspaceDir="/repo" + />, + ); + + const close = container.querySelector(".composer-tags .x"); + expect(close).not.toBeNull(); + fireEvent.click(close!); + + const closeDraftUpdate = setDraft.mock.calls.at(-1)?.[0]; + expect(typeof closeDraftUpdate).toBe("function"); + expect((closeDraftUpdate as (current: string) => string)(pickedDraft)).toBe( + "inspect before send", + ); + }); }); diff --git a/desktop/src/ui/composer.tsx b/desktop/src/ui/composer.tsx index f260fa102..886c5a3e5 100644 --- a/desktop/src/ui/composer.tsx +++ b/desktop/src/ui/composer.tsx @@ -79,6 +79,10 @@ export type MentionItem = { export type Chip = { kind: "at"; label: string } | { kind: "slash"; label: string }; +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + /** For long paths show only the filename; truncate filename if it's still too long. */ function chipLabel(label: string, maxLen = 32): string { if (label.length <= maxLen) return label; @@ -132,6 +136,20 @@ function highlightTokens(text: string): React.ReactNode { return parts.length > 0 ? parts : text; } +function removeAtTokenFromDraft(text: string, label: string): string { + const re = new RegExp(`(^|\\s+)@${escapeRegExp(label)}(\\s+|$)`, "g"); + let next = text; + let prev = ""; + while (next !== prev) { + prev = next; + next = next.replace(re, (_match, leading: string, trailing: string) => { + if (!leading || !trailing) return ""; + return " "; + }); + } + return next; +} + function slashIcon(cmd: string) { const m: Record = { "/clear": , @@ -258,7 +276,7 @@ export function Composer({ const result: Chip[] = []; for (const [label, kind] of pickedChips) { const sigil = kind === "at" ? "@" : "/"; - const escaped = sigil + label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const escaped = sigil + escapeRegExp(label); const re = new RegExp(`(?:^|\\s)${escaped}(?:\\s|$)`); if (re.test(draft)) { result.push({ kind, label }); @@ -737,13 +755,16 @@ export function Composer({