Merge branch 'main' into assistant-builder-v2

This commit is contained in:
Nils Kruthoff 2026-07-13 13:33:31 +02:00
commit a0065e628e
No known key found for this signature in database
GPG Key ID: A5C0151B4DDB172C
17 changed files with 928 additions and 150 deletions

View File

@ -19,6 +19,13 @@ concurrency:
env:
RETENTION_INTERMEDIATE_ASSETS: 1
RETENTION_RELEASE_ASSETS: 30
FLATPAK_REPOSITORY: MindWorkAI/Flatpak
FLATPAK_WORKFLOW: flatpak.yml
FLATPAK_YQ_VERSION: 4.44.6
FLATPAK_YQ_SHA256: 0c2b24e645b57d8e7c0566d18643a6d4f5580feeea3878127354a46f2a1e4598
FLATPAK_UV_VERSION: 0.11.28
FLATPAK_UV_SHA256: e490a6464492183c5d4534a5527fb4440f7f2bb2f228162ad7e4afe076dc0224
FLATPAK_FREEDESKTOP_VERSION: "25.08"
jobs:
determine_run_mode:
@ -165,6 +172,8 @@ jobs:
formatted_build_time: ${{ steps.format_metadata.outputs.formatted_build_time }}
changelog: ${{ steps.read_changelog.outputs.changelog }}
version: ${{ steps.format_metadata.outputs.version }}
source_commit: ${{ steps.format_metadata.outputs.source_commit }}
pdfium_chromium_revision: ${{ steps.format_metadata.outputs.pdfium_chromium_revision }}
steps:
- name: Checkout repository
@ -176,6 +185,9 @@ jobs:
# Read the first two lines of the metadata file:
version=$(sed -n '1p' metadata.txt)
build_time=$(sed -n '2p' metadata.txt)
pdfium_full_version=$(sed -n '11p' metadata.txt)
pdfium_chromium_revision=$(echo "$pdfium_full_version" | cut -d'.' -f3)
source_commit=$(git rev-parse HEAD)
# Format the version:
formatted_version="v${version}"
@ -186,12 +198,16 @@ jobs:
# Log the formatted metadata:
echo "Formatted version: '${formatted_version}'"
echo "Formatted build time: '${formatted_build_time}'"
echo "Source commit: '${source_commit}'"
echo "PDFium Chromium revision: '${pdfium_chromium_revision}'"
# Set the outputs:
echo "formatted_version=${formatted_version}" >> "$GITHUB_OUTPUT"
echo "FORMATTED_VERSION=${formatted_version}" >> $GITHUB_ENV
echo "formatted_build_time=${formatted_build_time}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "source_commit=${source_commit}" >> "$GITHUB_OUTPUT"
echo "pdfium_chromium_revision=${pdfium_chromium_revision}" >> "$GITHUB_OUTPUT"
- name: Check tag vs. metadata version
if: startsWith(github.ref, 'refs/tags/v')
@ -224,6 +240,484 @@ jobs:
echo "${changelog}" >> "$GITHUB_OUTPUT"
echo "EOOOF" >> "$GITHUB_OUTPUT"
sync_flatpak_repo:
name: Sync Flatpak repo
runs-on: ubuntu-latest
needs: [determine_run_mode, read_metadata]
if: needs.determine_run_mode.outputs.is_release == 'true'
permissions:
contents: read
outputs:
flatpak_commit: ${{ steps.sync.outputs.flatpak_commit }}
env:
AI_STUDIO_TAG: ${{ needs.read_metadata.outputs.formatted_version }}
AI_STUDIO_COMMIT: ${{ needs.read_metadata.outputs.source_commit }}
PDFIUM_CHROMIUM_REVISION: ${{ needs.read_metadata.outputs.pdfium_chromium_revision }}
steps:
- name: Checkout Flatpak repository
uses: actions/checkout@v4
with:
repository: ${{ env.FLATPAK_REPOSITORY }}
token: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }}
ref: main
path: flatpak
fetch-depth: 0
persist-credentials: false
- name: Install Flatpak sync tools and SDK
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install --no-install-recommends -y flatpak
flatpak remote-add \
--user \
--if-not-exists \
flathub \
https://flathub.org/repo/flathub.flatpakrepo
flatpak install \
--user \
--noninteractive \
-y \
flathub \
"org.freedesktop.Sdk//${FLATPAK_FREEDESKTOP_VERSION}" \
"org.freedesktop.Sdk.Extension.dotnet9//${FLATPAK_FREEDESKTOP_VERSION}"
tools_dir="$RUNNER_TEMP/flatpak-sync-tools"
mkdir -p "$tools_dir"
curl -fsSL \
-o "$tools_dir/yq" \
"https://github.com/mikefarah/yq/releases/download/v${FLATPAK_YQ_VERSION}/yq_linux_amd64"
echo "${FLATPAK_YQ_SHA256} ${tools_dir}/yq" | sha256sum --check --strict
chmod +x "$tools_dir/yq"
uv_archive="$RUNNER_TEMP/uv-x86_64-unknown-linux-gnu.tar.gz"
curl -fsSL \
-o "$uv_archive" \
"https://github.com/astral-sh/uv/releases/download/${FLATPAK_UV_VERSION}/uv-x86_64-unknown-linux-gnu.tar.gz"
echo "${FLATPAK_UV_SHA256} ${uv_archive}" | sha256sum --check --strict
tar -xzf "$uv_archive" -C "$tools_dir"
install -m 0755 "$tools_dir/uv-x86_64-unknown-linux-gnu/uv" "$tools_dir/uv"
echo "$tools_dir" >> "$GITHUB_PATH"
"$tools_dir/uv" --version
"$tools_dir/yq" --version
- name: Update Flatpak release sources
working-directory: flatpak
env:
PDFIUM_X64_ARCHIVE: ${{ runner.temp }}/pdfium-linux-x64.tgz
PDFIUM_ARM64_ARCHIVE: ${{ runner.temp }}/pdfium-linux-arm64.tgz
run: |
set -euo pipefail
pdfium_base_url="https://github.com/bblanchon/pdfium-binaries/releases/download/chromium%2F${PDFIUM_CHROMIUM_REVISION}"
pdfium_x64_url="${pdfium_base_url}/pdfium-linux-x64.tgz"
pdfium_arm64_url="${pdfium_base_url}/pdfium-linux-arm64.tgz"
curl -fsSL -o "$PDFIUM_X64_ARCHIVE" "$pdfium_x64_url"
curl -fsSL -o "$PDFIUM_ARM64_ARCHIVE" "$pdfium_arm64_url"
export PDFIUM_X64_URL="$pdfium_x64_url"
export PDFIUM_ARM64_URL="$pdfium_arm64_url"
export PDFIUM_X64_SHA256
export PDFIUM_ARM64_SHA256
PDFIUM_X64_SHA256=$(sha256sum "$PDFIUM_X64_ARCHIVE" | awk '{print $1}')
PDFIUM_ARM64_SHA256=$(sha256sum "$PDFIUM_ARM64_ARCHIVE" | awk '{print $1}')
yq -i '
(.modules[] | select(.name == "mind-work-ai-studio").sources[0].tag) = strenv(AI_STUDIO_TAG) |
(.modules[] | select(.name == "mind-work-ai-studio").sources[0].commit) = strenv(AI_STUDIO_COMMIT) |
(.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").url) = strenv(PDFIUM_X64_URL) |
(.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").sha256) = strenv(PDFIUM_X64_SHA256) |
(.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").url) = strenv(PDFIUM_ARM64_URL) |
(.modules[] | select(.name == "mind-work-ai-studio").sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").sha256) = strenv(PDFIUM_ARM64_SHA256)
' org.MindWorkAI.AIStudio.yml
./update-dependencies
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[0].tag' org.MindWorkAI.AIStudio.yml)" = "$AI_STUDIO_TAG"
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[0].commit' org.MindWorkAI.AIStudio.yml)" = "$AI_STUDIO_COMMIT"
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").url' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_X64_URL"
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "x86_64").sha256' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_X64_SHA256"
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").url' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_ARM64_URL"
test "$(yq -r '(.modules[] | select(.name == "mind-work-ai-studio")).sources[] | select(.type == "archive" and .only-arches[0] == "aarch64").sha256' org.MindWorkAI.AIStudio.yml)" = "$PDFIUM_ARM64_SHA256"
for generated_source in cargo-sources.json dotnet-sources.json tauri-cli-sources.json; do
test -s "$generated_source"
jq -e 'type == "array" and length > 0' "$generated_source" > /dev/null
done
git diff --check
git diff --stat
- name: Commit and merge Flatpak sync
id: sync
working-directory: flatpak
env:
GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
branch="sync/ai-studio-${AI_STUDIO_TAG}"
git checkout -B "$branch"
git add org.MindWorkAI.AIStudio.yml cargo-sources.json dotnet-sources.json tauri-cli-sources.json
if git diff --cached --quiet; then
echo "Flatpak repository is already synced for ${AI_STUDIO_TAG}."
flatpak_commit=$(git rev-parse HEAD)
remote_main=$(git ls-remote origin refs/heads/main | awk '{print $1}')
if [ "$remote_main" != "$flatpak_commit" ]; then
echo "Flatpak main advanced from ${flatpak_commit} to ${remote_main} during synchronization."
exit 1
fi
echo "flatpak_commit=${flatpak_commit}" >> "$GITHUB_OUTPUT"
exit 0
fi
git commit -m "Sync AI Studio ${AI_STUDIO_TAG}"
sync_commit=$(git rev-parse HEAD)
basic_auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w 0)
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic ${basic_auth}"
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
remote_branch_sha=$(git ls-remote --heads origin "$branch" | awk '{print $1}')
if [ -n "$remote_branch_sha" ]; then
git push --force-with-lease="refs/heads/${branch}:${remote_branch_sha}" origin "HEAD:${branch}"
else
git push origin "HEAD:${branch}"
fi
pr_number=$(gh pr list \
--repo "$FLATPAK_REPOSITORY" \
--head "$branch" \
--state open \
--json number \
--jq '.[0].number // empty')
if [ -z "$pr_number" ]; then
pr_url=$(gh pr create \
--repo "$FLATPAK_REPOSITORY" \
--base main \
--head "$branch" \
--title "Sync AI Studio ${AI_STUDIO_TAG}" \
--body "Synchronizes the Flatpak manifest and generated dependency sources for MindWork AI Studio ${AI_STUDIO_TAG}.")
pr_number="${pr_url##*/}"
fi
mergeable="UNKNOWN"
for attempt in {1..30}; do
pr_state=$(gh pr view "$pr_number" --repo "$FLATPAK_REPOSITORY" --json mergeable,mergeStateStatus)
mergeable=$(echo "$pr_state" | jq -r '.mergeable')
merge_state_status=$(echo "$pr_state" | jq -r '.mergeStateStatus')
echo "PR #${pr_number}: mergeable=${mergeable}, mergeStateStatus=${merge_state_status}"
if [ "$mergeable" = "MERGEABLE" ]; then
break
fi
if [ "$mergeable" = "CONFLICTING" ]; then
echo "Flatpak sync PR #${pr_number} has merge conflicts."
exit 1
fi
sleep 5
done
if [ "$mergeable" != "MERGEABLE" ]; then
echo "Timed out waiting for Flatpak sync PR #${pr_number} to become mergeable."
exit 1
fi
gh pr merge "$pr_number" \
--repo "$FLATPAK_REPOSITORY" \
--merge \
--delete-branch \
--match-head-commit "$sync_commit"
for attempt in {1..120}; do
pr_state=$(gh pr view "$pr_number" \
--repo "$FLATPAK_REPOSITORY" \
--json state,mergedAt,mergeCommit)
state=$(echo "$pr_state" | jq -r '.state')
merged_at=$(echo "$pr_state" | jq -r '.mergedAt // empty')
merge_commit=$(echo "$pr_state" | jq -r '.mergeCommit.oid // empty')
echo "PR #${pr_number}: state=${state}, mergedAt=${merged_at:-pending}, mergeCommit=${merge_commit:-pending}"
if [ -n "$merged_at" ] && [ -n "$merge_commit" ]; then
echo "flatpak_commit=${merge_commit}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$state" = "CLOSED" ]; then
echo "Flatpak sync PR #${pr_number} was closed without being merged."
exit 1
fi
sleep 5
done
echo "Timed out waiting for Flatpak sync PR #${pr_number} to be merged."
exit 1
collect_flatpak_artifacts:
name: Collect Flatpak artifacts
runs-on: ubuntu-latest
needs: [determine_run_mode, read_metadata, sync_flatpak_repo]
if: needs.determine_run_mode.outputs.is_release == 'true'
permissions:
contents: read
env:
FLATPAK_COMMIT: ${{ needs.sync_flatpak_repo.outputs.flatpak_commit }}
steps:
- name: Wait for Flatpak main build
id: flatpak_run
env:
GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }}
run: |
set -euo pipefail
find_run_id() {
local created_after="${1:-}"
local runs
runs=$(gh run list \
--repo "$FLATPAK_REPOSITORY" \
--workflow "$FLATPAK_WORKFLOW" \
--branch main \
--commit "$FLATPAK_COMMIT" \
--limit 20 \
--json databaseId,event,headSha,createdAt)
if [ -n "$created_after" ]; then
echo "$runs" | jq -r \
--arg commit "$FLATPAK_COMMIT" \
--arg created_after "$created_after" \
'[.[] | select(.headSha == $commit and .event == "workflow_dispatch" and .createdAt >= $created_after)][0].databaseId // empty'
else
echo "$runs" | jq -r \
--arg commit "$FLATPAK_COMMIT" \
'[.[] | select(.headSha == $commit and (.event == "push" or .event == "workflow_dispatch"))][0].databaseId // empty'
fi
}
validate_required_artifacts() {
local run_id="$1"
local artifacts
local expected_name
local match_count
local custom_count
artifacts=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}/artifacts?per_page=100")
custom_count=$(echo "$artifacts" | jq \
'[.artifacts[] | select(.name | startswith("MindWork AI Studio Flatpak ("))] | length')
if [ "$custom_count" -ne 2 ]; then
echo "Flatpak run ${run_id} contains ${custom_count} release artifacts; expected 2."
return 1
fi
for expected_name in \
"MindWork AI Studio Flatpak (x86_64)" \
"MindWork AI Studio Flatpak (aarch64)"; do
match_count=$(echo "$artifacts" | jq \
--arg name "$expected_name" \
'[.artifacts[] | select(.name == $name and .expired == false)] | length')
if [ "$match_count" -ne 1 ]; then
echo "Flatpak run ${run_id} does not contain one active '${expected_name}' artifact."
return 1
fi
done
}
wait_for_run() {
local run_id="$1"
local run_json
local status
local conclusion
local head_sha
local url
for attempt in {1..180}; do
run_json=$(gh run view "$run_id" \
--repo "$FLATPAK_REPOSITORY" \
--json status,conclusion,headSha,url)
status=$(echo "$run_json" | jq -r '.status')
conclusion=$(echo "$run_json" | jq -r '.conclusion // empty')
head_sha=$(echo "$run_json" | jq -r '.headSha')
url=$(echo "$run_json" | jq -r '.url')
echo "Flatpak run ${run_id}: status=${status}, conclusion=${conclusion:-pending}, url=${url}"
if [ "$head_sha" != "$FLATPAK_COMMIT" ]; then
echo "Flatpak run ${run_id} targets '${head_sha}', expected '${FLATPAK_COMMIT}'."
return 1
fi
if [ "$status" = "completed" ]; then
if [ "$conclusion" = "success" ] && validate_required_artifacts "$run_id"; then
return 0
fi
echo "Flatpak run ${run_id} completed without usable release artifacts."
return 1
fi
sleep 20
done
echo "Timed out waiting for Flatpak run ${run_id}."
return 2
}
run_id=""
for attempt in {1..15}; do
run_id=$(find_run_id)
if [ -n "$run_id" ]; then
break
fi
echo "Waiting for Flatpak workflow on commit ${FLATPAK_COMMIT}..."
sleep 20
done
if [ -z "$run_id" ]; then
current_main=$(gh api "repos/${FLATPAK_REPOSITORY}/commits/main" --jq .sha)
if [ "$current_main" != "$FLATPAK_COMMIT" ]; then
echo "No Flatpak run exists for ${FLATPAK_COMMIT}, and Flatpak main has advanced to ${current_main}."
exit 1
fi
dispatch_started_at=$(date --utc +'%Y-%m-%dT%H:%M:%SZ')
gh workflow run "$FLATPAK_WORKFLOW" \
--repo "$FLATPAK_REPOSITORY" \
--ref main \
-f "artifact_retention_days=${RETENTION_INTERMEDIATE_ASSETS}"
for attempt in {1..15}; do
run_id=$(find_run_id "$dispatch_started_at")
if [ -n "$run_id" ]; then
break
fi
echo "Waiting for the dispatched Flatpak workflow on commit ${FLATPAK_COMMIT}..."
sleep 20
done
fi
if [ -z "$run_id" ]; then
echo "Timed out waiting for a Flatpak workflow to start on commit ${FLATPAK_COMMIT}."
exit 1
fi
set +e
wait_for_run "$run_id"
wait_result=$?
set -e
if [ "$wait_result" -eq 0 ]; then
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$wait_result" -eq 2 ]; then
exit 1
fi
echo "Re-running Flatpak run ${run_id} once."
previous_attempt=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}" --jq .run_attempt)
gh run rerun "$run_id" --repo "$FLATPAK_REPOSITORY"
rerun_started=false
for attempt in {1..60}; do
rerun_state=$(gh api "repos/${FLATPAK_REPOSITORY}/actions/runs/${run_id}")
current_attempt=$(echo "$rerun_state" | jq -r '.run_attempt')
current_status=$(echo "$rerun_state" | jq -r '.status')
if [ "$current_attempt" -gt "$previous_attempt" ] || [ "$current_status" != "completed" ]; then
rerun_started=true
break
fi
sleep 2
done
if [ "$rerun_started" != "true" ]; then
echo "Timed out waiting for Flatpak run ${run_id} to start its retry."
exit 1
fi
if ! wait_for_run "$run_id"; then
echo "Flatpak run ${run_id} did not succeed after one retry."
exit 1
fi
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
- name: Download Flatpak artifacts
env:
GH_TOKEN: ${{ secrets.FLATPAK_WORKFLOW_TOKEN }}
RUN_ID: ${{ steps.flatpak_run.outputs.run_id }}
run: |
set -euo pipefail
mkdir -p flatpak-artifacts
gh run download "$RUN_ID" \
--repo "$FLATPAK_REPOSITORY" \
--name "MindWork AI Studio Flatpak (x86_64)" \
--name "MindWork AI Studio Flatpak (aarch64)" \
--dir flatpak-artifacts
expected_files=(
"MindWork AI Studio_x86_64.flatpak"
"MindWork AI Studio Plugin Pandoc_x86_64.flatpak"
"MindWork AI Studio_aarch64.flatpak"
"MindWork AI Studio Plugin Pandoc_aarch64.flatpak"
)
flatpak_count=$(find flatpak-artifacts -type f -name '*.flatpak' | wc -l | tr -d ' ')
if [ "$flatpak_count" -ne 4 ]; then
echo "Downloaded ${flatpak_count} Flatpak files; expected 4."
find flatpak-artifacts -type f -print
exit 1
fi
for expected_file in "${expected_files[@]}"; do
match_count=$(find flatpak-artifacts -type f -name "$expected_file" | wc -l | tr -d ' ')
if [ "$match_count" -ne 1 ]; then
echo "Expected exactly one '${expected_file}', found ${match_count}."
exit 1
fi
matched_file=$(find flatpak-artifacts -type f -name "$expected_file" -print -quit)
test -s "$matched_file"
done
find flatpak-artifacts -type f -name '*.flatpak' -print
- name: Upload Flatpak artifacts
uses: actions/upload-artifact@v4
with:
name: MindWork AI Studio Flatpak Release
path: flatpak-artifacts/**/*.flatpak
if-no-files-found: error
overwrite: true
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
build_main:
name: Build app (${{ matrix.dotnet_runtime }})
needs: [determine_run_mode, read_metadata]
@ -789,7 +1283,7 @@ jobs:
create_release:
name: Prepare & create release
runs-on: ubuntu-latest
needs: [build_main, read_metadata]
needs: [build_main, collect_flatpak_artifacts, read_metadata]
if: startsWith(github.ref, 'refs/tags/v')
permissions: {}
steps:
@ -808,16 +1302,19 @@ jobs:
- name: Prepare release assets
env:
VERSION: ${{ needs.read_metadata.outputs.version }}
run: |
set -euo pipefail
RELEASE_DIR="$GITHUB_WORKSPACE/release/assets"
declare -A release_asset_sources
# Ensure the release directory exists:
mkdir -p "$RELEASE_DIR"
# Find and process files in the artifacts directory:
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
while IFS= read -r -d '' FILE; do
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
@ -830,10 +1327,36 @@ jobs:
TARGET_NAME="$(basename "$FILE")"
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
fi
if [ -n "${release_asset_sources[$TARGET_NAME]+x}" ]; then
echo "Duplicate release asset name '${TARGET_NAME}':"
echo " ${release_asset_sources[$TARGET_NAME]}"
echo " ${FILE}"
exit 1
fi
release_asset_sources[$TARGET_NAME]="$FILE"
cp "$FILE" "${RELEASE_DIR}/${TARGET_NAME}"
done < <(find "$GITHUB_WORKSPACE/artifacts" -type f -print0)
expected_flatpaks=(
"MindWork AI Studio_x86_64.flatpak"
"MindWork AI Studio Plugin Pandoc_x86_64.flatpak"
"MindWork AI Studio_aarch64.flatpak"
"MindWork AI Studio Plugin Pandoc_aarch64.flatpak"
)
flatpak_count=$(find "$RELEASE_DIR" -maxdepth 1 -type f -name '*.flatpak' | wc -l | tr -d ' ')
if [ "$flatpak_count" -ne 4 ]; then
echo "Prepared ${flatpak_count} Flatpak release assets; expected 4."
exit 1
fi
for expected_flatpak in "${expected_flatpaks[@]}"; do
test -s "${RELEASE_DIR}/${expected_flatpak}"
done
# Display the structure of the release directory:
ls -Rlhat $GITHUB_WORKSPACE/release/assets

View File

@ -21,6 +21,9 @@
<body style="overflow: hidden;">
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)"/>
<div id="reconnect-modal" style="display: none; position: fixed; inset: 0; z-index: 20000; align-items: center; justify-content: center; padding: 2rem; background: rgba(15, 23, 42, 0.82); color: white; font-size: 1.1rem; text-align: center;">
Reconnecting to AI Studio...
</div>
<script src="_framework/blazor.web.js" autostart="false"></script>
<script src="boot.js"></script>
<script src="system/MudBlazor/MudBlazor.min.js"></script>

View File

@ -1600,23 +1600,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
-- Ask your questions
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
-- Analyze the following text and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analyze the following text and extract my tasks:"
-- You can enter text, attach one or more documents, or use both. At least one input is required.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
-- Please provide some text as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Please provide some text as input. For example, an email."
-- Please provide some text or at least one valid document as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Please provide some text or at least one valid document as input. For example, an email."
-- Analyze text
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Analyze text"
-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."
-- Target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Target language"
-- Analyze the following text and/or attached documents and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analyze the following text and/or attached documents and extract my tasks:"
-- My Tasks
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "My Tasks"
-- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."
-- Analyze content
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Analyze content"
-- Attach documents
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Attach documents"
-- Custom target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Custom target language"

View File

@ -3,5 +3,12 @@
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.CurrentProfile"/>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Text or email")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudText Typo="Typo.h6" Class="mb-1 mt-1">@T("Attach documents")</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
@T("You can enter text, attach one or more documents, or use both. At least one input is required.")
</MudJustifiedText>
<div class="mb-3">
<AttachDocuments Name="My Tasks Documents" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" OnChange="@this.OnDocumentsChanged" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
</div>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>

View File

@ -1,3 +1,4 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings;
using AIStudio.Tools.AssistantSessions;
@ -10,34 +11,84 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
protected override string Title => T("My Tasks");
protected override string Description => T("You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.");
protected override string Description => T("You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.");
protected override string SystemPrompt =>
$"""
You are a friendly and professional business expert. You receive business emails, protocols,
reports, etc. as input. Additionally, you know the user's role in the organization. The user
wonders if any tasks arise for them in their role based on the text. You now try to give hints
and advice on whether and what the user should do. When you believe there are no tasks for the
user, you tell them this. You consider typical business etiquette in your advice.
reports, etc. as text input and/or attached documents. Additionally, you know the user's role
in the organization. The user wonders if any tasks arise for them in their role based on the
provided content. You now try to give hints and advice on whether and what the user should do.
When you believe there are no tasks for the user, you tell them this. You consider typical
business etiquette in your advice.
You write your advice in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => T("Analyze text");
protected override string SubmitText => T("Analyze content");
protected override Func<Task> SubmitAction => this.AnalyzeText;
protected override bool ShowProfileSelection => false;
protected override string SendToChatVisibleUserPromptPrefix => T("Analyze the following text and extract my tasks:");
protected override string SendToChatVisibleUserPromptPrefix => T("Analyze the following text and/or attached documents and extract my tasks:");
protected override string SendToChatVisibleUserPromptContent => this.inputText;
protected override ChatThread ConvertToChatThread
{
get
{
var originalChatThread = this.ChatThread ?? new ChatThread();
if (string.IsNullOrWhiteSpace(this.SendToChatVisibleUserPromptText))
{
return originalChatThread with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
}
var earliestBlock = originalChatThread.Blocks.MinBy(x => x.Time);
var visiblePromptTime = earliestBlock is null
? DateTimeOffset.Now
: earliestBlock.Time == DateTimeOffset.MinValue
? earliestBlock.Time
: earliestBlock.Time.AddTicks(-1);
var transferredBlocks = originalChatThread.Blocks
.Select(block => block.Role is ChatRole.USER
? this.CloneHiddenUserBlockWithoutAttachments(block)
: block.DeepClone())
.ToList();
transferredBlocks.Insert(0, new ContentBlock
{
Time = visiblePromptTime,
ContentType = ContentType.TEXT,
HideFromUser = false,
Role = ChatRole.USER,
Content = new ContentText
{
Text = this.SendToChatVisibleUserPromptText,
FileAttachments = this.loadedDocumentPaths.ToList(),
},
});
return originalChatThread with
{
ChatId = Guid.NewGuid(),
SystemPrompt = SystemPrompts.DEFAULT,
Blocks = transferredBlocks,
};
}
}
protected override void ResetForm()
{
this.inputText = string.Empty;
this.loadedDocumentPaths.Clear();
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
@ -58,9 +109,11 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
}
private string inputText = string.Empty;
private HashSet<FileAttachment> loadedDocumentPaths = [];
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
private static readonly AssistantSessionStateKey<string> INPUT_TEXT_STATE_KEY = new(nameof(inputText));
private static readonly AssistantSessionStateKey<HashSet<FileAttachment>> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths));
private static readonly AssistantSessionStateKey<CommonLanguages> SELECTED_TARGET_LANGUAGE_STATE_KEY = new(nameof(selectedTargetLanguage));
private static readonly AssistantSessionStateKey<string> CUSTOM_TARGET_LANGUAGE_STATE_KEY = new(nameof(customTargetLanguage));
@ -68,6 +121,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
{
state.Set(INPUT_TEXT_STATE_KEY, this.inputText);
state.SetHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths);
state.Set(SELECTED_TARGET_LANGUAGE_STATE_KEY, this.selectedTargetLanguage);
state.Set(CUSTOM_TARGET_LANGUAGE_STATE_KEY, this.customTargetLanguage);
}
@ -76,6 +130,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
protected override void RestoreCustomAssistantSessionState(AssistantSessionStateReader state)
{
state.Restore(INPUT_TEXT_STATE_KEY, value => this.inputText = value);
state.RestoreHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths);
state.Restore(SELECTED_TARGET_LANGUAGE_STATE_KEY, value => this.selectedTargetLanguage = value);
state.Restore(CUSTOM_TARGET_LANGUAGE_STATE_KEY, value => this.customTargetLanguage = value);
}
@ -95,12 +150,20 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return T("Please provide some text as input. For example, an email.");
if(string.IsNullOrWhiteSpace(text) && !this.HasValidInputDocuments())
return T("Please provide some text or at least one valid document as input. For example, an email.");
return null;
}
private bool HasValidInputDocuments() => this.loadedDocumentPaths.Any(n => n is { Exists: true, IsValid: true });
private async Task OnDocumentsChanged(HashSet<FileAttachment> _)
{
if(this.Form is not null)
await this.Form.Validate();
}
private string? ValidateProfile(Profile profile)
{
if(profile == Profile.NO_PROFILE)
@ -127,6 +190,23 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
return this.selectedTargetLanguage.Name();
}
private ContentBlock CloneHiddenUserBlockWithoutAttachments(ContentBlock block)
{
var clone = block.DeepClone(changeHideState: true);
if (clone.Content is ContentText text)
text.FileAttachments = [];
return clone;
}
private string BuildUserRequest()
{
if(!string.IsNullOrWhiteSpace(this.inputText))
return this.inputText;
return "Analyze the attached document(s) and extract my tasks.";
}
private async Task AnalyzeText()
{
@ -135,7 +215,7 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
return;
this.CreateChatThread();
var time = this.AddUserRequest(this.inputText);
var time = this.AddUserRequest(this.BuildUserRequest(), false, this.loadedDocumentPaths.ToList());
await this.AddAIResponseAsync(time);
}

View File

@ -1602,23 +1602,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
-- Ask your questions
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Stellen Sie ihre Fragen"
-- Analyze the following text and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analysiere den folgenden Text und extrahiere meine Aufgaben:"
-- You can enter text, attach one or more documents, or use both. At least one input is required.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "Sie können Text eingeben, ein oder mehrere Dokumente anhängen oder beides verwenden. Mindestens eine Eingabe ist erforderlich."
-- Please provide some text as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Bitte geben Sie einen Text ein. Zum Beispiel eine E-Mail."
-- Please provide some text or at least one valid document as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Bitte geben Sie einen Text oder mindestens ein gültiges Dokument als Eingabe an. Zum Beispiel eine E-Mail."
-- Analyze text
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Text analysieren"
-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "Sie haben eine kryptische E-Mail oder ein Dokument erhalten, das an viele Empfänger gesendet wurde, und fragen sich nun, ob Sie etwas tun müssen? Kopieren Sie den Text in das Eingabefeld, fügen Sie ein oder mehrere Dokumente an oder nutzen Sie beides. Außerdem müssen Sie ein persönliches Profil auswählen. In diesem Profil sollten Sie Ihre Rolle in der Organisation beschreiben. Die KI wird dann versuchen, Ihnen Hinweise darauf zu geben, welche Aufgaben Sie möglicherweise haben."
-- Target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Zielsprache"
-- Analyze the following text and/or attached documents and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analysiere den folgenden Text und/oder die angehängten Dokumente und extrahiere meine Aufgaben:"
-- My Tasks
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "Meine Aufgaben"
-- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "Sie haben eine rätselhafte E-Mail erhalten, die an viele Empfänger verschickt wurde, und fragen sich nun, ob Sie etwas unternehmen müssen? Kopieren Sie die E-Mail in das Eingabefeld. Außerdem müssen Sie ein persönliches Profil auswählen. In diesem Profil sollten Sie ihre Rolle in der Organisation beschreiben. Die KI wird Ihnen dann Hinweise geben, welche Aufgaben für Sie daraus entstehen könnten."
-- Analyze content
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Inhalt analysieren"
-- Attach documents
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Dokumente anhängen"
-- Custom target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Benutzerdefinierte Zielsprache"

View File

@ -1602,23 +1602,29 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T4254597
-- Ask your questions
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LEGALCHECK::ASSISTANTLEGALCHECK::T467099852"] = "Ask your questions"
-- Analyze the following text and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1349891364"] = "Analyze the following text and extract my tasks:"
-- You can enter text, attach one or more documents, or use both. At least one input is required.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1442535450"] = "You can enter text, attach one or more documents, or use both. At least one input is required."
-- Please provide some text as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1962809521"] = "Please provide some text as input. For example, an email."
-- Please provide some text or at least one valid document as input. For example, an email.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1635845349"] = "Please provide some text or at least one valid document as input. For example, an email."
-- Analyze text
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2268303626"] = "Analyze text"
-- You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T1918551346"] = "You received a cryptic email or document that was sent to many recipients and you are now wondering if you need to do something? Copy the text into the input field, attach one or more documents, or use both. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."
-- Target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T237828418"] = "Target language"
-- Analyze the following text and/or attached documents and extract my tasks:
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T2535924263"] = "Analyze the following text and/or attached documents and extract my tasks:"
-- My Tasks
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3011450657"] = "My Tasks"
-- You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3646084045"] = "You received a cryptic email that was sent to many recipients and you are now wondering if you need to do something? Copy the email into the input field. You also need to select a personal profile. In this profile, you should describe your role in the organization. The AI will then try to give you hints on what your tasks might be."
-- Analyze content
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3334965934"] = "Analyze content"
-- Attach documents
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3666048746"] = "Attach documents"
-- Custom target language
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::MYTASKS::ASSISTANTMYTASKS::T3848935911"] = "Custom target language"

View File

@ -155,12 +155,17 @@ internal sealed class Program
// ReSharper restore AccessToDisposedClosure
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveServerComponents(options =>
{
options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromDays(30);
options.DisconnectedCircuitMaxRetained = 2;
})
.AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = null;
options.ClientTimeoutInterval = TimeSpan.FromDays(14);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(120);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
options.KeepAliveInterval = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton(new HttpClient

View File

@ -65,7 +65,7 @@ public static partial class PluginFactory
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
}
ZipFile.ExtractToDirectory(tempDownloadFile, stagedDirectory);
ExtractConfigPluginArchive(tempDownloadFile, stagedDirectory);
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
if (Directory.Exists(configDirectory))
@ -129,4 +129,70 @@ public static partial class PluginFactory
return wasSuccessful;
}
// Compatibility shim for Windows-created ZIPs with backslashes in entry names (dotnet/runtime#27620).
// See documentation/compatibility-shims/2026-07-enterprise-config-zip-backslashes.md.
private static void ExtractConfigPluginArchive(string sourceArchiveFileName, string destinationDirectory)
{
using var archive = ZipFile.OpenRead(sourceArchiveFileName);
Directory.CreateDirectory(destinationDirectory);
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
foreach (var entry in archive.Entries)
{
var normalizedEntryName = NormalizeConfigPluginZipEntryName(entry.FullName);
var destinationPath = GetConfigPluginZipEntryDestinationPath(destinationDirectoryFullPath, normalizedEntryName);
if (normalizedEntryName.EndsWith('/'))
{
if (entry.Length != 0)
throw new InvalidDataException($"The enterprise configuration plugin archive contains a directory entry with data: '{entry.FullName}'.");
Directory.CreateDirectory(destinationPath);
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
entry.ExtractToFile(destinationPath);
}
if (!Directory.EnumerateFiles(destinationDirectory, "plugin.lua", SearchOption.AllDirectories).Any())
throw new InvalidDataException("The enterprise configuration plugin archive does not contain a plugin.lua file.");
}
private static string NormalizeConfigPluginZipEntryName(string entryName)
{
var normalizedEntryName = entryName.Replace('\\', '/');
if (string.IsNullOrWhiteSpace(normalizedEntryName))
throw new InvalidDataException("The enterprise configuration plugin archive contains an empty entry name.");
if (normalizedEntryName.Contains('\0'))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an invalid entry name: '{entryName}'.");
if (normalizedEntryName.StartsWith('/'))
throw new InvalidDataException($"The enterprise configuration plugin archive contains a rooted entry name: '{entryName}'.");
if (normalizedEntryName is [_, ':', ..])
throw new InvalidDataException($"The enterprise configuration plugin archive contains a drive-qualified entry name: '{entryName}'.");
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathSegments.Length == 0 || pathSegments.Any(segment => segment is "." or ".."))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an unsafe entry name: '{entryName}'.");
return normalizedEntryName;
}
private static string GetConfigPluginZipEntryDestinationPath(string destinationDirectoryFullPath, string normalizedEntryName)
{
var pathSegments = normalizedEntryName.Split('/', StringSplitOptions.RemoveEmptyEntries);
var relativePath = Path.Combine(pathSegments);
var destinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, relativePath));
if (!destinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.Ordinal))
throw new InvalidDataException($"The enterprise configuration plugin archive contains an entry outside the destination directory: '{normalizedEntryName}'.");
return destinationPath;
}
}

View File

@ -1,33 +1,73 @@
(() => {
const maximumRetryCount = 3;
const retryIntervalMilliseconds = 500;
const maximumRetryCount = 12;
const reconnectModal = document.getElementById('reconnect-modal');
const retryDelaysMilliseconds = [
0,
1_000,
2_000,
5_000,
10_000,
15_000,
30_000,
];
let currentReconnectionProcess = null;
let isConnectionDown = false;
const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
const getRetryDelayMilliseconds = attempt => retryDelaysMilliseconds[Math.min(attempt, retryDelaysMilliseconds.length - 1)];
const showReconnectModal = () => {
if (reconnectModal)
reconnectModal.style.display = 'flex';
};
const hideReconnectModal = () => {
if (reconnectModal)
reconnectModal.style.display = 'none';
};
const setReconnectModalText = text => {
if (reconnectModal)
reconnectModal.textContent = text;
};
const startReconnectionProcess = () => {
reconnectModal.style.display = 'block';
showReconnectModal();
let isCanceled = false;
let forceAttempt = false;
(async () => {
for (let i = 0; i < maximumRetryCount; i++) {
reconnectModal.innerText = `Attempting to reconnect: ${i + 1} of ${maximumRetryCount}`;
const waitForNextAttempt = async milliseconds => {
const startedAt = Date.now();
while (!isCanceled && !forceAttempt && Date.now() - startedAt < milliseconds)
await delay(250);
await new Promise(resolve => setTimeout(resolve, retryIntervalMilliseconds));
forceAttempt = false;
};
if (isCanceled) {
void (async () => {
for (let attempt = 0; attempt < maximumRetryCount && !isCanceled; attempt++) {
setReconnectModalText(`Reconnecting to AI Studio (${attempt + 1}/${maximumRetryCount})...`);
const retryDelayMilliseconds = getRetryDelayMilliseconds(attempt);
if (retryDelayMilliseconds > 0)
await waitForNextAttempt(retryDelayMilliseconds);
if (isCanceled)
return;
}
try {
const result = await Blazor.reconnect();
if (!result) {
if (result === false) {
// The server was reached, but the connection was rejected; reload the page.
location.reload();
return;
}
// Successfully reconnected to the server.
return;
if (result === true)
return;
} catch {
// Didn't reach the server; try again.
}
@ -40,25 +80,42 @@
return {
cancel: () => {
isCanceled = true;
reconnectModal.style.display = 'none';
hideReconnectModal();
},
triggerImmediateAttempt: () => {
forceAttempt = true;
},
};
};
let currentReconnectionProcess = null;
const triggerReconnectAfterWake = () => {
if (isConnectionDown)
currentReconnectionProcess?.triggerImmediateAttempt();
};
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible')
triggerReconnectAfterWake();
});
globalThis.addEventListener('pageshow', triggerReconnectAfterWake);
Blazor.start({
circuit: {
reconnectionHandler: {
onConnectionDown: () => currentReconnectionProcess ??= startReconnectionProcess(),
onConnectionDown: () => {
isConnectionDown = true;
currentReconnectionProcess ??= startReconnectionProcess();
},
onConnectionUp: () => {
isConnectionDown = false;
currentReconnectionProcess?.cancel();
currentReconnectionProcess = null;
}
},
configureSignalR: function (builder) {
builder.withServerTimeout(1_200_000);
builder.withServerTimeout(120_000);
builder.withKeepAliveInterval(30_000);
},
}

View File

@ -1 +1,7 @@
# v26.7.3, build 245 (2026-07-xx xx:xx UTC)
- Improved the "My Tasks Assistant": you can now provide one or more documents in addition to text or use documents alone when asking to identify tasks.
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep.
- Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder.
- Upgraded Rust to v1.97.0.
- Upgraded Tauri to v2.11.5.
- Upgraded common dependencies.

View File

@ -0,0 +1,28 @@
# Enterprise Configuration ZIP Backslashes
- Status: Active
- Introduced: 2026-07-09
- Remove after: when Microsoft fixes dotnet/runtime#27620 and dotnet/runtime#41914
- Code references:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Download.cs`
## User Impact
Some enterprise administrators create configuration plugin ZIP files on Windows. Depending on the packaging tool, entries inside the ZIP may use Windows-style backslashes, for example `O\plugin.lua`.
Without this shim, Unix systems extract those entries as files whose names contain literal backslash characters. The plugin loader then cannot find `plugin.lua`, so the enterprise configuration plugin is not activated.
## Compatibility Behavior
AI Studio manually extracts downloaded enterprise configuration plugin ZIP files. During extraction, entry names are normalized so both `/` and `\` are treated as archive path separators.
The extraction still preserves the archive structure and validates each entry before writing it to disk. Rooted paths, drive-qualified paths, and parent-directory traversal paths are rejected.
This works around the behavior described in dotnet/runtime#27620. A related upstream context for ZIP entry creation is dotnet/runtime#41914, where the ZIP specification requirement for forward slashes is discussed.
## Removal Checklist
- Confirm supported .NET runtimes and administrator packaging guidance no longer require accepting backslashes in enterprise ZIP entry names.
- Replace the manual enterprise configuration plugin ZIP extraction with `ZipFile.ExtractToDirectory(...)`.
- Remove `ExtractConfigPluginArchive(...)`, `NormalizeConfigPluginZipEntryName(...)`, and `GetConfigPluginZipEntryDestinationPath(...)`.
- Update this document's status to `Removed`.

View File

@ -3,9 +3,9 @@
244
9.0.118 (commit c8cbca4ed1)
9.0.17 (commit f2c8152eed)
1.96.1 (commit 31fca3adb)
1.97.0 (commit 2d8144b78)
8.15.0
2.11.2
2.11.5
4a15ff26655, release
osx-arm64
148.0.7763.0

117
runtime/Cargo.lock generated
View File

@ -459,11 +459,12 @@ dependencies = [
[[package]]
name = "atoi_simd"
version = "0.17.0"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad17c7c205c2c28b527b9845eeb91cf1b4d008b438f98ce0e628227a822758e"
checksum = "f3cdb3708a128e559a30fb830e8a77a5022ee6902806925c216658652b452a44"
dependencies = [
"debug_unsafe",
"rustversion",
]
[[package]]
@ -842,9 +843,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.16.0"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
@ -880,9 +881,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
dependencies = [
"serde",
]
@ -933,9 +934,9 @@ dependencies = [
[[package]]
name = "calamine"
version = "0.35.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8822fe6253ca47aa5ad9a3be09f6fe7cd20c6a74e41b0aa42e8f4e3d523508df"
checksum = "6975084f43060e56343ffba7f9731fa52a7dcf2e1cd8e2459fd4c6bf4a1bff59"
dependencies = [
"atoi_simd",
"byteorder",
@ -943,9 +944,9 @@ dependencies = [
"encoding_rs",
"fast-float2",
"log",
"quick-xml 0.39.2",
"quick-xml 0.41.0",
"serde",
"zip 7.4.0",
"zip 8.6.0",
]
[[package]]
@ -1165,9 +1166,9 @@ dependencies = [
[[package]]
name = "cmov"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
[[package]]
name = "codepage"
@ -1227,7 +1228,7 @@ dependencies = [
"ph",
"procfs",
"quick_cache",
"rand 0.10.1",
"rand 0.10.2",
"roaring",
"schemars",
"self_cell",
@ -2517,7 +2518,7 @@ dependencies = [
"i_overlay",
"log",
"num-traits",
"rand 0.10.1",
"rand 0.10.2",
"rand_pcg",
"robust",
"rstar",
@ -2765,7 +2766,7 @@ dependencies = [
"log",
"lz4_flex",
"parking_lot",
"rand 0.10.1",
"rand 0.10.2",
"serde",
"serde_cbor",
"serde_json",
@ -3874,17 +3875,11 @@ dependencies = [
"serde",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "lru-slab"
@ -4025,7 +4020,7 @@ dependencies = [
"pdfium-render",
"pptx-to-md",
"qdrant-edge",
"rand 0.10.1",
"rand 0.10.2",
"rand_chacha 0.10.0",
"rcgen",
"rustls",
@ -4034,7 +4029,7 @@ dependencies = [
"sha2 0.11.0",
"strum_macros",
"sys-locale",
"sysinfo 0.39.3",
"sysinfo 0.39.6",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
@ -5260,7 +5255,7 @@ dependencies = [
"log",
"ordered-float 5.3.0",
"parking_lot",
"rand 0.10.1",
"rand 0.10.2",
"segment",
"serde",
"serde_json",
@ -5303,7 +5298,7 @@ dependencies = [
"ordered-float 5.3.0",
"parking_lot",
"permutation_iterator",
"rand 0.10.1",
"rand 0.10.2",
"rayon",
"serde",
"serde_json",
@ -5321,9 +5316,9 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.39.2"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"encoding_rs",
"memchr",
@ -5442,9 +5437,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.2",
@ -5513,7 +5508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8"
dependencies = [
"num-traits",
"rand 0.10.1",
"rand 0.10.2",
]
[[package]]
@ -6038,7 +6033,7 @@ dependencies = [
"procfs",
"qdrant-rust-stemmers",
"quantization",
"rand 0.10.1",
"rand 0.10.2",
"rayon",
"roaring",
"schemars",
@ -6355,7 +6350,7 @@ dependencies = [
"log",
"ordered-float 5.3.0",
"parking_lot",
"rand 0.10.1",
"rand 0.10.2",
"rmp-serde",
"schemars",
"segment",
@ -6520,7 +6515,7 @@ dependencies = [
"memmap2",
"ordered-float 5.3.0",
"parking_lot",
"rand 0.10.1",
"rand 0.10.2",
"schemars",
"serde",
"serde_json",
@ -6677,9 +6672,9 @@ dependencies = [
[[package]]
name = "sysinfo"
version = "0.39.3"
version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21d0d938c10fcda3e897e28aaddf4ab462375d411f4378cd63b1c945f69aba96"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [
"libc",
"memchr",
@ -6779,9 +6774,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.11.2"
version = "2.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28"
checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5"
dependencies = [
"anyhow",
"bytes",
@ -6830,9 +6825,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7"
checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
dependencies = [
"anyhow",
"cargo_toml",
@ -6851,9 +6846,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9"
checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5"
dependencies = [
"base64 0.22.1",
"brotli",
@ -6878,9 +6873,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.6.2"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924"
checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -7071,9 +7066,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.11.2"
version = "2.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef"
checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
dependencies = [
"cookie",
"dpi",
@ -7096,9 +7091,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.11.2"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9"
checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
dependencies = [
"gtk",
"http",
@ -7122,9 +7117,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.9.2"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95"
checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
dependencies = [
"anyhow",
"brotli",
@ -7609,9 +7604,9 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.23.1"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773"
checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
dependencies = [
"crossbeam-channel",
"dirs",
@ -7933,7 +7928,7 @@ dependencies = [
"fs4",
"log",
"memmap2",
"rand 0.10.1",
"rand 0.10.2",
"rand_distr",
"rustix 1.1.4",
"serde",
@ -8434,9 +8429,9 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-native-keyring-store"
version = "1.0.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5fd986f648459dd29aa252ed3a5ad11a60c0b1251bf81625fb03a86c69d274e"
checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
dependencies = [
"byteorder",
"keyring-core",
@ -9340,9 +9335,9 @@ dependencies = [
[[package]]
name = "zip"
version = "7.4.0"
version = "8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980"
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
dependencies = [
"crc32fast",
"flate2",
@ -9366,15 +9361,13 @@ checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
[[package]]
name = "zopfli"
version = "0.8.1"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]

View File

@ -6,10 +6,10 @@ description = "MindWork AI Studio"
authors = ["Thorsten Sommer"]
[build-dependencies]
tauri-build = { version = "2.6.2", features = [] }
tauri-build = { version = "2.6.3", features = [] }
[dependencies]
tauri = { version = "2.11.2", features = [] }
tauri = { version = "2.11.5", features = [] }
tauri-plugin-window-state = { version = "2.4.1" }
tauri-plugin-shell = "2.3.5"
tauri-plugin-dialog = "2.7.1"
@ -24,12 +24,12 @@ tokio-stream = "0.1.18"
futures = "0.3.32"
async-stream = "0.3.6"
flexi_logger = "0.31.9"
log = { version = "0.4.30", features = ["kv"] }
log = { version = "0.4.33", features = ["kv"] }
once_cell = "1.21.4"
axum = { version = "0.8.9", features = ["http2", "json", "query", "tokio"] }
axum-server = { version = "0.8.0", features = ["tls-rustls"] }
rustls = { version = "0.23.28", default-features = false, features = ["aws_lc_rs"] }
rand = "0.10.1"
rand = "0.10.2"
rand_chacha = "0.10.0"
base64 = "0.22.1"
aes = "0.9.1"
@ -39,7 +39,7 @@ hmac = "0.13.0"
sha2 = "0.11.0"
rcgen = { version = "0.14.8", features = ["pem"] }
file-format = "0.29.0"
calamine = "0.35.0"
calamine = "0.36.0"
pdfium-render = "0.9.1"
sys-locale = "0.3.2"
whoami = "2.1.2"
@ -47,8 +47,8 @@ cfg-if = "1.0.4"
pptx-to-md = "0.4.0"
tempfile = "3.27.0"
strum_macros = "0.28.0"
sysinfo = "0.39.3"
bytes = "1.11.1"
sysinfo = "0.39.6"
bytes = "1.12.1"
qdrant-edge = "0.7.2"
[patch.crates-io]
@ -62,7 +62,7 @@ permutation_iterator = { git = "https://github.com/SommerEngineering/permutation
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = "0.6.1"
windows-native-keyring-store = "1.0.0"
windows-native-keyring-store = "1.1.0"
[target.'cfg(target_os = "macos")'.dependencies]
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
@ -72,7 +72,7 @@ dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rus
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-global-shortcut = "2"
tauri-plugin-updater = "2.10.0"
tauri-plugin-updater = "2.10.1"
[features]
custom-protocol = ["tauri/custom-protocol"]

View File

@ -223,7 +223,7 @@ fn file_logger_format(
write_kv_pairs(w, record)?;
// Write the log message:
write!(w, "{}", &record.args())
write!(w, "{}", record.args())
}
pub async fn get_log_paths(_token: APIToken) -> Json<LogPathsResponse> {

View File

@ -1,31 +1,23 @@
use std::error::Error;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use once_cell::sync::{Lazy, OnceCell};
use pdfium_render::prelude::Pdfium;
use log::{error, info, warn};
pub static PDFIUM_LIB_PATH: Lazy<Mutex<Option<String>>> = Lazy::new(|| Mutex::new(None));
static PDFIUM: Lazy<Mutex<Option<Pdfium>>> = Lazy::new(|| Mutex::new(None));
static PDFIUM: OnceCell<Pdfium> = OnceCell::new();
pub trait PdfiumInit {
fn ai_studio_init() -> Result<Pdfium, Box<dyn Error + Send + Sync>>;
fn ai_studio_init() -> Result<&'static Pdfium, Box<dyn Error + Send + Sync>>;
}
impl PdfiumInit for Pdfium {
/// Initializes the PDFium library for AI Studio.
fn ai_studio_init() -> Result<Pdfium, Box<dyn Error + Send + Sync>> {
let mut pdfium = PDFIUM.lock().unwrap();
if let Some(pdfium) = pdfium.as_ref() {
return Ok(pdfium.clone());
}
let loaded_pdfium = load_pdfium().map_err(|error| {
fn ai_studio_init() -> Result<&'static Pdfium, Box<dyn Error + Send + Sync>> {
PDFIUM.get_or_try_init(|| load_pdfium().map_err(|error| {
Box::new(std::io::Error::other(error)) as Box<dyn Error + Send + Sync>
})?;
*pdfium = Some(loaded_pdfium.clone());
Ok(loaded_pdfium)
}))
}
}
@ -76,4 +68,4 @@ fn load_pdfium() -> Result<Pdfium, String> {
Err(error_message)
}
}
}
}