diff --git a/.github/workflows/build-helper.yml b/.github/workflows/build-helper.yml deleted file mode 100644 index eadb18ce77..0000000000 --- a/.github/workflows/build-helper.yml +++ /dev/null @@ -1,209 +0,0 @@ -# Build Helper workflow - Builds, signs, and packages binaries for each supported platform, then uploads to a staging bucket in S3 for wider distribution. -# For more information on the macOS signing and notarization, see https://www.electron.build/code-signing and https://www.electron.build/configuration/mac -# For more information on the Windows Code Signing, see https://docs.digicert.com/en/digicert-keylocker/ci-cd-integrations/plugins/github-custom-action-for-keypair-signing.html and https://docs.digicert.com/en/digicert-keylocker/signing-tools/sign-authenticode-with-electron-builder-using-ksp-integration.html - -name: Build Helper -run-name: Build ${{ github.ref_name }}${{ github.event_name == 'workflow_dispatch' && ' - Manual' || '' }} -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+*" - workflow_dispatch: -env: - GO_VERSION: "1.25.6" - NODE_VERSION: 22 - NODE_OPTIONS: --max-old-space-size=4096 -jobs: - build-app: - outputs: - version: ${{ steps.set-version.outputs.WAVETERM_VERSION }} - strategy: - matrix: - include: - - platform: "darwin" - runner: "macos-latest" - - platform: "linux" - runner: "ubuntu-latest" - - platform: "linux" - runner: ubuntu-24.04-arm - - platform: "windows" - runner: "windows-latest" - # - platform: "windows" - # runner: "windows-11-arm64-16core" - runs-on: ${{ matrix.runner }} - steps: - - uses: actions/checkout@v6 - - name: Install Linux Build Dependencies (Linux only) - if: matrix.platform == 'linux' - run: | - sudo apt-get update - sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm squashfs-tools - sudo snap install snapcraft --classic - sudo snap install lxd - sudo lxd init --auto - sudo snap refresh - - name: Install Zig (not Mac) - if: matrix.platform != 'darwin' - uses: mlugg/setup-zig@v2 - - # The pre-installed version of the AWS CLI has a segfault problem so we'll install it via Homebrew instead. - - name: Upgrade AWS CLI (Mac only) - if: matrix.platform == 'darwin' - run: brew install awscli - - # The version of FPM that comes bundled with electron-builder doesn't include a Linux ARM target. Installing Gems onto the runner is super quick so we'll just do this for all targets. - - name: Install FPM (not Windows) - if: matrix.platform != 'windows' - run: sudo gem install fpm - - name: Install FPM (Windows only) - if: matrix.platform == 'windows' - run: gem install fpm - - # General build dependencies - - uses: actions/setup-go@v6 - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: | - go.sum - - uses: actions/setup-node@v6 - with: - node-version: ${{env.NODE_VERSION}} - cache: npm - cache-dependency-path: package-lock.json - - name: Force git deps to HTTPS - run: | - git config --global url.https://github.com/.insteadof ssh://git@github.com/ - git config --global url.https://github.com/.insteadof git@github.com: - - uses: nick-fields/retry@v4 - name: npm ci - with: - command: npm ci --no-audit --no-fund - retry_on: error - max_attempts: 3 - timeout_minutes: 5 - env: - GIT_ASKPASS: "echo" - GIT_TERMINAL_PROMPT: "0" - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: "Set Version" - id: set-version - run: echo "WAVETERM_VERSION=$(task version)" >> "$GITHUB_OUTPUT" - shell: bash - - # Windows Code Signing Setup - - name: Set up certificate (Windows only) - if: matrix.platform == 'windows' && github.event_name != 'workflow_dispatch' - run: | - echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12 - shell: bash - - name: Set signing variables (Windows only) - if: matrix.platform == 'windows' && github.event_name != 'workflow_dispatch' - id: variables - run: | - echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" - echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" - echo "SM_CODE_SIGNING_CERT_SHA1_HASH=${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" >> "$GITHUB_ENV" - echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV" - echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_OUTPUT" - echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" - echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH - echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH - echo "C:\Program Files\DigiCert\DigiCert Keylocker Tools" >> $GITHUB_PATH - shell: bash - - name: Setup Keylocker KSP (Windows only) - if: matrix.platform == 'windows' && github.event_name != 'workflow_dispatch' - run: | - curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o Keylockertools-windows-x64.msi - msiexec /i Keylockertools-windows-x64.msi /quiet /qn - C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user - smctl windows certsync - shell: cmd - - # Build and upload packages - - name: Build (Linux) - if: matrix.platform == 'linux' - run: task package - env: - USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one. - SNAPCRAFT_BUILD_ENVIRONMENT: host - # Retry Darwin build in case of notarization failures - - uses: nick-fields/retry@v4 - name: Build (Darwin) - if: matrix.platform == 'darwin' - with: - command: task package - timeout_minutes: 120 - retry_on: error - max_attempts: 3 - env: - USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one. - CSC_LINK: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_2}} - CSC_KEY_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_CERTIFICATE_PWD_2 }} - APPLE_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_APPLE_ID_2 }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_PWD_2 }} - APPLE_TEAM_ID: ${{ matrix.platform == 'darwin' && secrets.PROD_MACOS_NOTARIZATION_TEAM_ID_2 }} - STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}} - - name: Build (Windows) - if: matrix.platform == 'windows' - run: task package - env: - USE_SYSTEM_FPM: true # Ensure that the installed version of FPM is used rather than the bundled one. - CSC_LINK: ${{ steps.variables.outputs.SM_CLIENT_CERT_FILE }} - CSC_KEY_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }} - STATIC_DOCSITE_PATH: ${{env.STATIC_DOCSITE_PATH}} - shell: powershell # electron-builder's Windows code signing package has some compatibility issues with pwsh, so we need to use Windows Powershell - - # Upload artifacts to the S3 staging and to the workflow output for the draft release job - - name: Upload to S3 staging - if: github.event_name != 'workflow_dispatch' - run: task artifacts:upload - env: - AWS_ACCESS_KEY_ID: "${{ secrets.ARTIFACTS_KEY_ID }}" - AWS_SECRET_ACCESS_KEY: "${{ secrets.ARTIFACTS_KEY_SECRET }}" - AWS_DEFAULT_REGION: us-west-2 - - name: Upload artifacts - uses: actions/upload-artifact@v5 - with: - name: ${{ matrix.runner }} - path: make - - name: Upload Snapcraft logs on failure - if: failure() - uses: actions/upload-artifact@v5 - with: - name: ${{ matrix.runner }}-log - path: /home/runner/.local/state/snapcraft/log - create-release: - runs-on: ubuntu-latest - needs: build-app - permissions: - contents: write - if: ${{ github.event_name != 'workflow_dispatch' }} - steps: - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - path: make - merge-multiple: true - - name: Create draft release - uses: softprops/action-gh-release@v2 - with: - prerelease: ${{ contains(github.ref_name, '-beta') }} - name: Wave Terminal ${{ github.ref_name }} Release - generate_release_notes: true - draft: true - files: | - make/*.zip - make/*.dmg - make/*.exe - make/*.msi - make/*.rpm - make/*.deb - make/*.pacman - make/*.snap - make/*.flatpak - make/*.AppImage diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml deleted file mode 100644 index 268e37724d..0000000000 --- a/.github/workflows/publish-release.yml +++ /dev/null @@ -1,96 +0,0 @@ -# Workflow to copy artifacts from the staging bucket to the release bucket when a new GitHub Release is published. - -name: Publish Release -run-name: Publish ${{ github.ref_name }} -on: - release: - types: [published] -jobs: - publish-s3: - name: Publish to Releases - if: ${{ startsWith(github.ref, 'refs/tags/') }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Publish from staging - run: "task artifacts:publish:${{ github.ref_name }}" - env: - AWS_ACCESS_KEY_ID: "${{ secrets.PUBLISHER_KEY_ID }}" - AWS_SECRET_ACCESS_KEY: "${{ secrets.PUBLISHER_KEY_SECRET }}" - AWS_DEFAULT_REGION: us-west-2 - shell: bash - publish-snap-amd64: - name: Publish AMD64 Snap - if: ${{ startsWith(github.ref, 'refs/tags/') }} - needs: [publish-s3] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install Snapcraft - run: sudo snap install snapcraft --classic - shell: bash - - name: Download Snap from Release - uses: robinraju/release-downloader@v1 - with: - tag: ${{github.ref_name}} - fileName: "*amd64.snap" - - name: Publish to Snapcraft - run: "task artifacts:snap:publish:${{ github.ref_name }}" - env: - SNAPCRAFT_STORE_CREDENTIALS: "${{secrets.SNAPCRAFT_LOGIN_CREDS}}" - shell: bash - publish-snap-arm64: - name: Publish ARM64 Snap - if: ${{ startsWith(github.ref, 'refs/tags/') }} - needs: [publish-s3] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install Snapcraft - run: sudo snap install snapcraft --classic - shell: bash - - name: Download Snap from Release - uses: robinraju/release-downloader@v1 - with: - tag: ${{github.ref_name}} - fileName: "*arm64.snap" - - name: Publish to Snapcraft - run: "task artifacts:snap:publish:${{ github.ref_name }}" - env: - SNAPCRAFT_STORE_CREDENTIALS: "${{secrets.SNAPCRAFT_LOGIN_CREDS}}" - shell: bash - bump-winget: - name: Submit WinGet PR - if: ${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'beta') }} - needs: [publish-s3] - runs-on: windows-latest - steps: - - uses: actions/checkout@v6 - - name: Install Task - uses: arduino/setup-task@v2 - with: - version: 3.x - repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install wingetcreate - run: winget install -e --silent --accept-package-agreements --accept-source-agreements wingetcreate - shell: pwsh - - name: Submit WinGet version bump - run: "task artifacts:winget:publish:${{ github.ref_name }}" - env: - GITHUB_TOKEN: ${{ secrets.WINGET_BUMP_PAT }} - shell: pwsh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..a1806e57fa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,181 @@ +name: Release +run-name: Release ${{ github.ref_name }} +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+*" +env: + GO_VERSION: "1.25.6" + NODE_VERSION: 22 + NODE_OPTIONS: --max-old-space-size=4096 +jobs: + build-app: + strategy: + matrix: + include: + - platform: "darwin" + runner: "macos-latest" + archs: "arm64,amd64" + - platform: "linux" + runner: "ubuntu-latest" + archs: "amd64" + - platform: "windows" + runner: "windows-latest" + archs: "amd64" + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Install Linux Build Dependencies + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y libarchive-tools libopenjp2-tools rpm squashfs-tools + + - name: Install Zig (not Mac) + if: matrix.platform != 'darwin' + uses: mlugg/setup-zig@v2 + + - name: Install FPM (not Windows) + if: matrix.platform != 'windows' + run: sudo gem install fpm + + - name: Install FPM (Windows only) + if: matrix.platform == 'windows' + run: gem install fpm + + - uses: actions/setup-go@v5 + with: + go-version: ${{env.GO_VERSION}} + cache-dependency-path: go.sum + + - uses: actions/setup-node@v4 + with: + node-version: ${{env.NODE_VERSION}} + cache: npm + cache-dependency-path: package-lock.json + + - name: Force git deps to HTTPS + run: | + git config --global url.https://github.com/.insteadof ssh://git@github.com/ + git config --global url.https://github.com/.insteadof git@github.com: + + - name: Remove docs workspace before install + run: | + node -e "const p=require('./package.json'); p.workspaces=p.workspaces.filter(w=>w!=='docs'); require('fs').writeFileSync('package.json',JSON.stringify(p,null,4))" + shell: bash + + - name: npm ci + run: npm ci --no-audit --no-fund + env: + GIT_ASKPASS: "echo" + GIT_TERMINAL_PROMPT: "0" + + - name: Get version + id: version + run: echo "VERSION=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Generate code + run: | + go run cmd/generatets/main-generatets.go + go run cmd/generatego/main-generatego.go + go run cmd/generateschema/main-generateschema.go + rm -rf dist/schema + mkdir -p dist/schema + cp schema/*.json dist/schema/ 2>/dev/null || true + shell: bash + + - name: Build wavesrv + run: | + VERSION="${{ steps.version.outputs.VERSION }}" + BUILD_TIME=$(date +'%Y%m%d%H%M') + IFS=',' read -ra ARCH_LIST <<< "${{ matrix.archs }}" + for GOARCH in "${ARCH_LIST[@]}"; do + NORMALIZED_ARCH=$GOARCH + if [ "$GOARCH" = "amd64" ]; then NORMALIZED_ARCH="x64"; fi + EXT="" + GO_ENV="" + if [ "${{ matrix.platform }}" = "windows" ]; then + EXT=".exe" + GO_ENV="CC=\"zig cc -target x86_64-windows-gnu\"" + elif [ "${{ matrix.platform }}" = "linux" ]; then + if [ "$GOARCH" = "amd64" ]; then + GO_ENV="CC=\"zig cc -target x86_64-linux-gnu.2.28\"" + else + GO_ENV="CC=\"zig cc -target aarch64-linux-gnu.2.28\"" + fi + fi + eval "CGO_ENABLED=1 GOARCH=$GOARCH $GO_ENV go build -tags 'osusergo,sqlite_omit_load_extension' -ldflags '-X main.BuildTime=$BUILD_TIME -X main.WaveVersion=$VERSION' -o dist/bin/wavesrv.${NORMALIZED_ARCH}${EXT} cmd/server/main-server.go" + done + shell: bash + + - name: Build wsh + run: | + VERSION="${{ steps.version.outputs.VERSION }}" + BUILD_TIME=$(date +'%Y%m%d%H%M') + declare -a TARGETS=( + "darwin:arm64" + "darwin:amd64" + "linux:arm64" + "linux:amd64" + "windows:amd64" + "windows:arm64" + ) + for TARGET in "${TARGETS[@]}"; do + GOOS="${TARGET%%:*}" + GOARCH="${TARGET##*:}" + NORMALIZED_ARCH=$GOARCH + if [ "$GOARCH" = "amd64" ]; then NORMALIZED_ARCH="x64"; fi + EXT="" + if [ "$GOOS" = "windows" ]; then EXT=".exe"; fi + CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="-s -w -X main.BuildTime=$BUILD_TIME -X main.WaveVersion=$VERSION" -o "dist/bin/wsh-${VERSION}-${GOOS}.${NORMALIZED_ARCH}${EXT}" cmd/wsh/main-wsh.go + done + shell: bash + + - name: Build tsunami scaffold + run: | + cd tsunami/frontend && npm run build && cd ../.. + rm -rf dist/tsunamiscaffold + mkdir -p dist/tsunamiscaffold + cp -r tsunami/frontend/scaffold/* dist/tsunamiscaffold/ 2>/dev/null || cp -r tsunami/frontend/dist/* dist/tsunamiscaffold/ 2>/dev/null || true + cp tsunami/templates/empty-gomod.tmpl dist/tsunamiscaffold/go.mod 2>/dev/null || true + shell: bash + + - name: Build frontend and package + run: npm run build:prod && npm exec electron-builder -- -c electron-builder.config.cjs -p never + env: + USE_SYSTEM_FPM: true + shell: bash + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.platform }}-artifacts + path: make/ + + create-release: + runs-on: ubuntu-latest + needs: build-app + permissions: + contents: write + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: Wave Terminal ${{ github.ref_name }} + generate_release_notes: true + draft: false + files: | + artifacts/*.zip + artifacts/*.dmg + artifacts/*.exe + artifacts/*.msi + artifacts/*.rpm + artifacts/*.deb + artifacts/*.AppImage diff --git a/frontend/app/block/blockregistry.ts b/frontend/app/block/blockregistry.ts index 5de7e05bd3..a8c27feeff 100644 --- a/frontend/app/block/blockregistry.ts +++ b/frontend/app/block/blockregistry.ts @@ -4,6 +4,7 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import type { TabModel } from "@/app/store/tab-model"; import { AiFileDiffViewModel } from "@/app/view/aifilediff/aifilediff"; +import { GitStatusViewModel } from "@/app/view/gitstatus/gitstatus"; import { LauncherViewModel } from "@/app/view/launcher/launcher"; import { PreviewModel } from "@/app/view/preview/preview-model"; import { ProcessViewerViewModel } from "@/app/view/processviewer/processviewer"; @@ -35,6 +36,7 @@ BlockRegistry.set("tsunami", TsunamiViewModel); BlockRegistry.set("aifilediff", AiFileDiffViewModel); BlockRegistry.set("waveconfig", WaveConfigViewModel); BlockRegistry.set("processviewer", ProcessViewerViewModel); +BlockRegistry.set("gitstatus", GitStatusViewModel); function makeDefaultViewModel(viewType: string): ViewModel { const viewModel: ViewModel = { diff --git a/frontend/app/block/blockutil.tsx b/frontend/app/block/blockutil.tsx index 3ef4d39821..bf48cc4c34 100644 --- a/frontend/app/block/blockutil.tsx +++ b/frontend/app/block/blockutil.tsx @@ -45,6 +45,9 @@ export function blockViewToIcon(view: string): string { if (view == "processviewer") { return "microchip"; } + if (view == "gitstatus") { + return "code-branch"; + } return "square"; } @@ -73,6 +76,9 @@ export function blockViewToName(view: string): string { if (view == "processviewer") { return "Processes"; } + if (view == "gitstatus") { + return "Git Status"; + } return view; } diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 8482be260d..b2c0b41edc 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -660,6 +660,12 @@ export class RpcApiType { return client.wshRpcCall("path", data, opts); } + // command "pintab" [call] + PinTabCommand(client: WshClient, arg1: string, arg2: string, opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "pintab", { args: [arg1, arg2] }, opts); + return client.wshRpcCall("pintab", { args: [arg1, arg2] }, opts); + } + // command "publishapp" [call] PublishAppCommand(client: WshClient, data: CommandPublishAppData, opts?: RpcOpts): Promise { if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "publishapp", data, opts); @@ -738,6 +744,18 @@ export class RpcApiType { return client.wshRpcCall("remotegetinfo", null, opts); } + // command "remotegitlinediff" [call] + RemoteGitLineDiffCommand(client: WshClient, data: CommandRemoteGitLineDiffData, opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotegitlinediff", data, opts); + return client.wshRpcCall("remotegitlinediff", data, opts); + } + + // command "remotegitstatus" [call] + RemoteGitStatusCommand(client: WshClient, data: CommandRemoteGitStatusData, opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotegitstatus", data, opts); + return client.wshRpcCall("remotegitstatus", data, opts); + } + // command "remoteinstallrcfiles" [call] RemoteInstallRcFilesCommand(client: WshClient, opts?: RpcOpts): Promise { if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remoteinstallrcfiles", null, opts); @@ -942,12 +960,24 @@ export class RpcApiType { return client.wshRpcCall("testmultiarg", { args: [arg1, arg2, arg3] }, opts); } + // command "unpintab" [call] + UnpinTabCommand(client: WshClient, arg1: string, arg2: string, opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "unpintab", { args: [arg1, arg2] }, opts); + return client.wshRpcCall("unpintab", { args: [arg1, arg2] }, opts); + } + // command "updatetabname" [call] UpdateTabNameCommand(client: WshClient, arg1: string, arg2: string, opts?: RpcOpts): Promise { if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "updatetabname", { args: [arg1, arg2] }, opts); return client.wshRpcCall("updatetabname", { args: [arg1, arg2] }, opts); } + // command "updateworkspacepinnedtabids" [call] + UpdateWorkspacePinnedTabIdsCommand(client: WshClient, arg1: string, arg2: string[], opts?: RpcOpts): Promise { + if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "updateworkspacepinnedtabids", { args: [arg1, arg2] }, opts); + return client.wshRpcCall("updateworkspacepinnedtabids", { args: [arg1, arg2] }, opts); + } + // command "updateworkspacetabids" [call] UpdateWorkspaceTabIdsCommand(client: WshClient, arg1: string, arg2: string[], opts?: RpcOpts): Promise { if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "updateworkspacetabids", { args: [arg1, arg2] }, opts); diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 4972a13daa..2cd8937967 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -23,9 +23,12 @@ export type TabEnv = WaveEnvSubset<{ SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"]; SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"]; + PinTabCommand: WaveEnv["rpc"]["PinTabCommand"]; + UnpinTabCommand: WaveEnv["rpc"]["UnpinTabCommand"]; }; atoms: { fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + workspace: WaveEnv["atoms"]["workspace"]; }; wos: WaveEnv["wos"]; getSettingsKeyAtom: WaveEnv["getSettingsKeyAtom"]; diff --git a/frontend/app/tab/tabcontextmenu.ts b/frontend/app/tab/tabcontextmenu.ts index bc87302d4c..ea8324ffb3 100644 --- a/frontend/app/tab/tabcontextmenu.ts +++ b/frontend/app/tab/tabcontextmenu.ts @@ -40,7 +40,8 @@ export function buildTabContextMenu( id: string, renameRef: React.RefObject<(() => void) | null>, onClose: (event: React.MouseEvent | null) => void, - env: TabEnv + env: TabEnv, + isPinned?: boolean ): ContextMenuItem[] { const menu: ContextMenuItem[] = []; menu.push( @@ -51,6 +52,24 @@ export function buildTabContextMenu( }, { type: "separator" } ); + + if (isPinned != null) { + const workspace = globalStore.get(env.atoms.workspace); + if (workspace != null) { + if (isPinned) { + menu.push({ + label: "Archive Tab", + click: () => fireAndForget(() => env.rpc.UnpinTabCommand(TabRpcClient, workspace.oid, id)), + }); + } else { + menu.push({ + label: "Move to Working Queue", + click: () => fireAndForget(() => env.rpc.PinTabCommand(TabRpcClient, workspace.oid, id)), + }); + } + menu.push({ type: "separator" }); + } + } const tabORef = makeORef("tab", id); const currentFlagColor = globalStore.get(getOrefMetaKeyAtom(tabORef, "tab:flagcolor")) ?? null; const flagSubmenu: ContextMenuItem[] = [ diff --git a/frontend/app/tab/vtabbar.tsx b/frontend/app/tab/vtabbar.tsx index e40bcfb374..34aae6d92b 100644 --- a/frontend/app/tab/vtabbar.tsx +++ b/frontend/app/tab/vtabbar.tsx @@ -11,7 +11,7 @@ import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { validateCssColor } from "@/util/color-validator"; import { cn, fireAndForget } from "@/util/util"; import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { buildTabBarContextMenu, buildTabContextMenu } from "./tabcontextmenu"; import { UpdateStatusBanner } from "./updatebanner"; import { VTab, VTabItem } from "./vtab"; @@ -92,6 +92,7 @@ interface VTabWrapperProps { isReordering: boolean; hoverResetVersion: number; index: number; + isPinned: boolean; onSelect: () => void; onClose: () => void; onRename: (newName: string) => void; @@ -109,6 +110,7 @@ function VTabWrapper({ isDragging, isReordering, hoverResetVersion, + isPinned, onSelect, onClose, onRename, @@ -156,10 +158,10 @@ function VTabWrapper({ (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const menu = buildTabContextMenu(tabId, renameRef, () => onClose(), env); + const menu = buildTabContextMenu(tabId, renameRef, () => onClose(), env, isPinned); env.showContextMenu(menu, e); }, - [tabId, onClose, env] + [tabId, onClose, env, isPinned] ); return ( @@ -184,51 +186,106 @@ function VTabWrapper({ ); } +interface ArchivedSectionHeaderProps { + count: number; + expanded: boolean; + onToggle: () => void; +} + +function ArchivedSectionHeader({ count, expanded, onToggle }: ArchivedSectionHeaderProps) { + return ( + + ); +} + export function VTabBar({ workspace, className }: VTabBarProps) { const env = useWaveEnv(); const activeTabId = useAtomValue(env.atoms.staticTabId); const reinitVersion = useAtomValue(env.atoms.reinitVersion); const documentHasFocus = useAtomValue(env.atoms.documentHasFocus); const tabIds = workspace?.tabids ?? []; + const pinnedTabIds = workspace?.pinnedtabids ?? []; const [orderedTabIds, setOrderedTabIds] = useState(tabIds); + const [orderedPinnedTabIds, setOrderedPinnedTabIds] = useState(pinnedTabIds); const [dragTabId, setDragTabId] = useState(null); const [dropIndex, setDropIndex] = useState(null); const [dropLineTop, setDropLineTop] = useState(null); + const [dropZone, setDropZone] = useState<"pinned" | "archived" | null>(null); const [hoverResetVersion, setHoverResetVersion] = useState(0); const [hoveredTabId, setHoveredTabId] = useState(null); const [isNewTabHovered, setIsNewTabHovered] = useState(false); + const [archivedExpanded, setArchivedExpanded] = useState(false); const dragSourceRef = useRef(null); + const dragSourceZoneRef = useRef<"pinned" | "archived" | null>(null); const didResetHoverForDragRef = useRef(false); - const scrollContainerRef = useRef(null); + const pinnedScrollRef = useRef(null); + const archivedScrollRef = useRef(null); const scrollAnimFrameRef = useRef(null); const scrollDirectionRef = useRef(0); const scrollSpeedRef = useRef(0); + const activeScrollContainerRef = useRef(null); + + const pinnedSet = useMemo(() => new Set(orderedPinnedTabIds), [orderedPinnedTabIds]); + + const archivedTabIds = useMemo(() => { + return orderedTabIds.filter((id) => !pinnedSet.has(id)); + }, [orderedTabIds, pinnedSet]); useEffect(() => { setOrderedTabIds(tabIds); }, [workspace?.tabids]); + useEffect(() => { + setOrderedPinnedTabIds(pinnedTabIds); + }, [workspace?.pinnedtabids]); + useEffect(() => { if (reinitVersion > 0) { setOrderedTabIds(workspace?.tabids ?? []); + setOrderedPinnedTabIds(workspace?.pinnedtabids ?? []); } }, [reinitVersion]); useEffect(() => { - if (activeTabId == null || scrollContainerRef.current == null) { + if (activeTabId == null) return; + const pinnedEl = pinnedScrollRef.current?.querySelector(`[data-tabid="${activeTabId}"]`); + if (pinnedEl) { + pinnedEl.scrollIntoView({ block: "nearest" }); return; } - const el = scrollContainerRef.current.querySelector(`[data-tabid="${activeTabId}"]`); - el?.scrollIntoView({ block: "nearest" }); + const archivedEl = archivedScrollRef.current?.querySelector(`[data-tabid="${activeTabId}"]`); + if (archivedEl) { + setArchivedExpanded(true); + archivedEl.scrollIntoView({ block: "nearest" }); + } }, [activeTabId]); useEffect(() => { - if (!documentHasFocus || activeTabId == null || scrollContainerRef.current == null) { + if (!documentHasFocus || activeTabId == null) return; + const pinnedEl = pinnedScrollRef.current?.querySelector(`[data-tabid="${activeTabId}"]`); + if (pinnedEl) { + pinnedEl.scrollIntoView({ block: "nearest" }); return; } - const el = scrollContainerRef.current.querySelector(`[data-tabid="${activeTabId}"]`); - el?.scrollIntoView({ block: "nearest" }); + const archivedEl = archivedScrollRef.current?.querySelector(`[data-tabid="${activeTabId}"]`); + if (archivedEl) { + archivedEl.scrollIntoView({ block: "nearest" }); + } }, [documentHasFocus]); const stopScrollLoop = useCallback(() => { @@ -240,11 +297,9 @@ export function VTabBar({ workspace, className }: VTabBarProps) { }, []); const startScrollLoop = useCallback(() => { - if (scrollAnimFrameRef.current != null) { - return; - } + if (scrollAnimFrameRef.current != null) return; const loop = () => { - const container = scrollContainerRef.current; + const container = activeScrollContainerRef.current; if (container == null || scrollDirectionRef.current === 0) { scrollAnimFrameRef.current = null; return; @@ -256,11 +311,9 @@ export function VTabBar({ workspace, className }: VTabBarProps) { }, []); const updateScrollFromDragY = useCallback( - (clientY: number) => { - const container = scrollContainerRef.current; - if (container == null) { - return; - } + (clientY: number, container: HTMLDivElement | null) => { + if (container == null) return; + activeScrollContainerRef.current = container; const EdgeZone = 60; const MaxScrollSpeed = 12; const rect = container.getBoundingClientRect(); @@ -289,32 +342,80 @@ export function VTabBar({ workspace, className }: VTabBarProps) { setHoverResetVersion((version) => version + 1); } dragSourceRef.current = null; + dragSourceZoneRef.current = null; setDragTabId(null); setDropIndex(null); setDropLineTop(null); + setDropZone(null); + activeScrollContainerRef.current = null; }; - const reorder = (targetIndex: number) => { + const reorderPinned = (targetIndex: number) => { const sourceTabId = dragSourceRef.current; - if (sourceTabId == null) { - return; - } - const sourceIndex = orderedTabIds.findIndex((id) => id === sourceTabId); - if (sourceIndex === -1) { + if (sourceTabId == null) return; + + const wasPinned = pinnedSet.has(sourceTabId); + + if (!wasPinned) { + const nextPinned = [...orderedPinnedTabIds]; + const bounded = Math.max(0, Math.min(targetIndex, nextPinned.length)); + nextPinned.splice(bounded, 0, sourceTabId); + setOrderedPinnedTabIds(nextPinned); + fireAndForget(() => env.rpc.UpdateWorkspacePinnedTabIdsCommand(TabRpcClient, workspace.oid, nextPinned)); return; } - const boundedTargetIndex = Math.max(0, Math.min(targetIndex, orderedTabIds.length)); + + const sourceIndex = orderedPinnedTabIds.findIndex((id) => id === sourceTabId); + if (sourceIndex === -1) return; + const boundedTargetIndex = Math.max(0, Math.min(targetIndex, orderedPinnedTabIds.length)); const adjustedTargetIndex = sourceIndex < boundedTargetIndex ? boundedTargetIndex - 1 : boundedTargetIndex; - if (sourceIndex === adjustedTargetIndex) { + if (sourceIndex === adjustedTargetIndex) return; + const nextPinned = [...orderedPinnedTabIds]; + const [movedId] = nextPinned.splice(sourceIndex, 1); + nextPinned.splice(adjustedTargetIndex, 0, movedId); + setOrderedPinnedTabIds(nextPinned); + fireAndForget(() => env.rpc.UpdateWorkspacePinnedTabIdsCommand(TabRpcClient, workspace.oid, nextPinned)); + }; + + const reorderArchived = (targetIndex: number) => { + const sourceTabId = dragSourceRef.current; + if (sourceTabId == null) return; + + const wasPinned = pinnedSet.has(sourceTabId); + + if (wasPinned) { + const nextPinned = orderedPinnedTabIds.filter((id) => id !== sourceTabId); + setOrderedPinnedTabIds(nextPinned); + fireAndForget(() => env.rpc.UpdateWorkspacePinnedTabIdsCommand(TabRpcClient, workspace.oid, nextPinned)); return; } - const nextTabIds = [...orderedTabIds]; - const [movedId] = nextTabIds.splice(sourceIndex, 1); - nextTabIds.splice(adjustedTargetIndex, 0, movedId); + + // reorder within archived is a reorder within the full tabIds, keeping pinned positions stable + const sourceGlobalIndex = orderedTabIds.findIndex((id) => id === sourceTabId); + if (sourceGlobalIndex === -1) return; + + const archivedIds = orderedTabIds.filter((id) => !pinnedSet.has(id)); + const sourceArchivedIndex = archivedIds.findIndex((id) => id === sourceTabId); + if (sourceArchivedIndex === -1) return; + const boundedTarget = Math.max(0, Math.min(targetIndex, archivedIds.length)); + const adjusted = sourceArchivedIndex < boundedTarget ? boundedTarget - 1 : boundedTarget; + if (sourceArchivedIndex === adjusted) return; + + const nextArchived = [...archivedIds]; + const [movedId] = nextArchived.splice(sourceArchivedIndex, 1); + nextArchived.splice(adjusted, 0, movedId); + + // rebuild full tabIds: pinned first in their order, then archived in new order + const nextTabIds = [...orderedPinnedTabIds, ...nextArchived]; setOrderedTabIds(nextTabIds); fireAndForget(() => env.rpc.UpdateWorkspaceTabIdsCommand(TabRpcClient, workspace.oid, nextTabIds)); }; + const handleSelectArchived = (tabId: string) => { + fireAndForget(() => env.rpc.PinTabCommand(TabRpcClient, workspace.oid, tabId)); + env.electron.setActiveTab(tabId); + }; + const handleTabBarContextMenu = useCallback( (e: React.MouseEvent) => { e.preventDefault(); @@ -324,6 +425,102 @@ export function VTabBar({ workspace, className }: VTabBarProps) { [env] ); + const makeDragStartHandler = (tabId: string, zone: "pinned" | "archived", index: number) => { + return (event: React.DragEvent) => { + didResetHoverForDragRef.current = false; + dragSourceRef.current = tabId; + dragSourceZoneRef.current = zone; + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", tabId); + setDragTabId(tabId); + setDropIndex(index); + setDropZone(zone); + setDropLineTop(event.currentTarget.offsetTop); + }; + }; + + const makeDragOverHandler = (zone: "pinned" | "archived", index: number) => { + return (event: React.DragEvent) => { + event.preventDefault(); + const rect = event.currentTarget.getBoundingClientRect(); + const relativeY = event.clientY - rect.top; + const midpoint = event.currentTarget.offsetHeight / 2; + const insertBefore = relativeY < midpoint; + setDropIndex(insertBefore ? index : index + 1); + setDropZone(zone); + setDropLineTop( + insertBefore + ? event.currentTarget.offsetTop + : event.currentTarget.offsetTop + event.currentTarget.offsetHeight + ); + }; + }; + + const makeDropHandler = (zone: "pinned" | "archived") => { + return (event: React.DragEvent) => { + event.preventDefault(); + if (dropIndex != null) { + if (zone === "pinned") { + reorderPinned(dropIndex); + } else { + reorderArchived(dropIndex); + } + } + clearDragState(); + }; + }; + + const renderTabList = ( + ids: string[], + zone: "pinned" | "archived", + scrollRef: React.RefObject + ) => { + return ids.map((tabId, index) => { + const isActive = tabId === activeTabId; + const isHovered = tabId === hoveredTabId; + const isLast = index === ids.length - 1; + const nextTabId = ids[index + 1]; + const isNextActive = nextTabId === activeTabId; + const isNextHovered = nextTabId === hoveredTabId; + const isPinned = zone === "pinned"; + return ( + { + if (isPinned) { + env.electron.setActiveTab(tabId); + } else { + handleSelectArchived(tabId); + } + }} + onClose={() => fireAndForget(() => env.electron.closeTab(workspace.oid, tabId, false))} + onRename={(newName) => + fireAndForget(() => env.rpc.UpdateTabNameCommand(TabRpcClient, tabId, newName)) + } + onDragStart={makeDragStartHandler(tabId, zone, index)} + onDragOver={makeDragOverHandler(zone, index)} + onDrop={makeDropHandler(zone)} + onDragEnd={clearDragState} + onHoverChanged={(isHovered) => setHoveredTabId(isHovered ? tabId : null)} + /> + ); + }); + }; + return (
{env.isMacOS() && } + {/* Pinned / Working Queue */}
{ event.preventDefault(); - updateScrollFromDragY(event.clientY); + updateScrollFromDragY(event.clientY, pinnedScrollRef.current); if (event.target === event.currentTarget) { - setDropIndex(orderedTabIds.length); + setDropIndex(orderedPinnedTabIds.length); + setDropZone("pinned"); setDropLineTop(event.currentTarget.scrollHeight); } }} onDrop={(event) => { event.preventDefault(); if (dropIndex != null) { - reorder(dropIndex); + reorderPinned(dropIndex); } clearDragState(); }} > - {orderedTabIds.map((tabId, index) => { - const isActive = tabId === activeTabId; - const isHovered = tabId === hoveredTabId; - const isLast = index === orderedTabIds.length - 1; - const nextTabId = orderedTabIds[index + 1]; - const isNextActive = nextTabId === activeTabId; - const isNextHovered = nextTabId === hoveredTabId; - return ( - env.electron.setActiveTab(tabId)} - onClose={() => fireAndForget(() => env.electron.closeTab(workspace.oid, tabId, false))} - onRename={(newName) => - fireAndForget(() => env.rpc.UpdateTabNameCommand(TabRpcClient, tabId, newName)) - } - onDragStart={(event) => { - didResetHoverForDragRef.current = false; - dragSourceRef.current = tabId; - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", tabId); - setDragTabId(tabId); - setDropIndex(index); - setDropLineTop(event.currentTarget.offsetTop); - }} + {renderTabList(orderedPinnedTabIds, "pinned", pinnedScrollRef)} + {dragTabId != null && dropZone === "pinned" && dropIndex != null && dropLineTop != null && ( +
+ )} +
+ {/* Archived / Archive Queue */} + {archivedTabIds.length > 0 && ( +
+ setArchivedExpanded((prev) => !prev)} + /> + {archivedExpanded && ( +
{ event.preventDefault(); - const rect = event.currentTarget.getBoundingClientRect(); - const relativeY = event.clientY - rect.top; - const midpoint = event.currentTarget.offsetHeight / 2; - const insertBefore = relativeY < midpoint; - setDropIndex(insertBefore ? index : index + 1); - setDropLineTop( - insertBefore - ? event.currentTarget.offsetTop - : event.currentTarget.offsetTop + event.currentTarget.offsetHeight - ); + updateScrollFromDragY(event.clientY, archivedScrollRef.current); + if (event.target === event.currentTarget) { + setDropIndex(archivedTabIds.length); + setDropZone("archived"); + setDropLineTop(event.currentTarget.scrollHeight); + } }} onDrop={(event) => { event.preventDefault(); if (dropIndex != null) { - reorder(dropIndex); + reorderArchived(dropIndex); } clearDragState(); }} - onDragEnd={clearDragState} - onHoverChanged={(isHovered) => setHoveredTabId(isHovered ? tabId : null)} - /> - ); - })} - {dragTabId != null && dropIndex != null && dropLineTop != null && ( -
- )} -
+ > + {renderTabList(archivedTabIds, "archived", archivedScrollRef)} + {dragTabId != null && + dropZone === "archived" && + dropIndex != null && + dropLineTop != null && ( +
+ )} +
+ )} +
+ )} + {/* New Tab Button */} +
+ ); + } +); +GitStatusFileRow.displayName = "GitStatusFileRow"; + +export const GitStatusView: React.FC> = React.memo( + function GitStatusView({ model }) { + const files = jotai.useAtomValue(model.filesAtom); + const branch = jotai.useAtomValue(model.branchAtom); + const error = jotai.useAtomValue(model.errorAtom); + const loading = jotai.useAtomValue(model.loadingAtom); + const cwd = jotai.useAtomValue(model.cwd); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + {error} +
+ ); + } + + if (isBlank(cwd)) { + return ( +
+ No working directory set +
+ ); + } + + return ( +
+ {branch && ( +
+ + {branch} + {files.length} changed +
+ )} +
+ {files.length === 0 ? ( +
+ Working tree clean +
+ ) : ( + files.map((file, idx) => ( + + )) + )} +
+
+ {cwd} +
+
+ ); + } +); diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index 2961771fa3..4d413f6af8 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -1,16 +1,19 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { globalStore } from "@/app/store/jotaiStore"; import { tryReinjectKey } from "@/app/store/keymodel"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; import { CodeEditor } from "@/app/view/codeeditor/codeeditor"; import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; -import { fireAndForget } from "@/util/util"; +import { fireAndForget, makeConnRoute } from "@/util/util"; import { useAtomValue, useSetAtom } from "jotai"; import type * as MonacoTypes from "monaco-editor"; import * as monaco from "monaco-editor"; import { useEffect } from "react"; import type { SpecializedViewProps } from "./preview"; +import type { PreviewModel } from "./preview-model"; export const shellFileMap: Record = { ".bashrc": "shell", @@ -36,6 +39,51 @@ export const shellFileMap: Record = { ".gvimrc": "shell", }; +async function applyGitGutter( + editor: MonacoTypes.editor.IStandaloneCodeEditor, + decorationCollection: MonacoTypes.editor.IEditorDecorationsCollection, + model: PreviewModel +) { + try { + const blockData = globalStore.get(model.blockAtom); + const filePath = blockData?.meta?.file; + const connName = blockData?.meta?.connection; + if (!filePath) return; + + const cwd = filePath.substring(0, filePath.lastIndexOf("/")) || "/"; + const route = makeConnRoute(connName); + + const resp = await RpcApi.RemoteGitLineDiffCommand(TabRpcClient, { cwd, file: filePath }, { route }); + if (!resp || resp.error || !resp.hunks?.length) return; + + const decorations: MonacoTypes.editor.IModelDeltaDecoration[] = resp.hunks.map((hunk) => { + let glyphClass: string; + if (hunk.type === "added") { + glyphClass = "git-gutter-added"; + } else if (hunk.type === "modified") { + glyphClass = "git-gutter-modified"; + } else { + glyphClass = "git-gutter-deleted"; + } + return { + range: new monaco.Range(hunk.startline, 1, hunk.endline, 1), + options: { + isWholeLine: true, + glyphMarginClassName: glyphClass, + overviewRuler: { + color: hunk.type === "added" ? "#2ea04370" : hunk.type === "modified" ? "#0078d470" : "#f8514970", + position: monaco.editor.OverviewRulerLane.Left, + }, + }, + }; + }); + + decorationCollection.set(decorations); + } catch { + // silently ignore - file might not be in a git repo + } +} + function CodeEditPreview({ model }: SpecializedViewProps) { const fileContent = useAtomValue(model.fileContent); const setNewFileContent = useSetAtom(model.newFileContent); @@ -90,8 +138,12 @@ function CodeEditPreview({ model }: SpecializedViewProps) { editor.focus(); } + const decorationCollection = editor.createDecorationsCollection([]); + applyGitGutter(editor, decorationCollection, model); + return () => { keyDownDisposer.dispose(); + decorationCollection.clear(); }; } diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index f11eca91da..c367b19611 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Tooltip } from "@/app/element/tooltip"; +import { getFocusedBlockId, globalStore, WOS } from "@/app/store/global"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; import { shouldIncludeWidgetForWorkspace } from "@/app/workspace/widgetfilter"; @@ -56,7 +57,22 @@ type WidgetPropsType = { }; async function handleWidgetSelect(widget: WidgetConfigType, env: WidgetsEnv) { - const blockDef = widget.blockdef; + let blockDef = widget.blockdef; + const focusedBlockId = getFocusedBlockId(); + if (focusedBlockId) { + const blockAtom = WOS.getWaveObjectAtom(WOS.makeORef("block", focusedBlockId)); + const blockData = globalStore.get(blockAtom); + const cwd = blockData?.meta?.["cmd:cwd"]; + if (cwd) { + if (blockDef?.meta?.view === "preview" && blockDef?.meta?.file) { + blockDef = { ...blockDef, meta: { ...blockDef.meta, file: cwd } }; + } else if (blockDef?.meta?.view === "term") { + blockDef = { ...blockDef, meta: { ...blockDef.meta, "cmd:cwd": cwd } }; + } else if (blockDef?.meta?.view === "gitstatus") { + blockDef = { ...blockDef, meta: { ...blockDef.meta, "cmd:cwd": cwd } }; + } + } + } env.createBlock(blockDef, widget.magnified); } diff --git a/frontend/tailwindsetup.css b/frontend/tailwindsetup.css index 3a0523c8ce..ff8d22122e 100644 --- a/frontend/tailwindsetup.css +++ b/frontend/tailwindsetup.css @@ -125,3 +125,27 @@ svg [aria-label="tip"] g path { opacity: 0; } } + +/* Git gutter indicators for Monaco editor */ +.git-gutter-added { + background: #2ea043; + width: 3px !important; + margin-left: 5px; + border-radius: 1px; +} + +.git-gutter-modified { + background: #0078d4; + width: 3px !important; + margin-left: 5px; + border-radius: 1px; +} + +.git-gutter-deleted { + background: #f85149; + width: 0 !important; + margin-left: 3px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #f85149; +} diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c5b870d7ed..296027f324 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -546,6 +546,17 @@ declare global { streammeta: StreamMeta; }; + // wshrpc.CommandRemoteGitLineDiffData + type CommandRemoteGitLineDiffData = { + cwd: string; + file: string; + }; + + // wshrpc.CommandRemoteGitStatusData + type CommandRemoteGitStatusData = { + cwd: string; + }; + // wshrpc.CommandRemoteListEntriesData type CommandRemoteListEntriesData = { path: string; @@ -1024,6 +1035,32 @@ declare global { buildtime: string; }; + // wshrpc.GitLineDiffHunk + type GitLineDiffHunk = { + type: string; + startline: number; + endline: number; + }; + + // wshrpc.GitLineDiffResponse + type GitLineDiffResponse = { + hunks: GitLineDiffHunk[]; + error?: string; + }; + + // wshrpc.GitStatusFile + type GitStatusFile = { + status: string; + file: string; + }; + + // wshrpc.GitStatusResponse + type GitStatusResponse = { + branch: string; + files: GitStatusFile[]; + error?: string; + }; + // waveobj.Job type Job = WaveObj & { connection: string; @@ -1589,6 +1626,7 @@ declare global { "debug:panictype"?: string; "block:view"?: string; "block:controller"?: string; + "block:subblock"?: boolean; "ai:backendtype"?: string; "ai:local"?: boolean; "wsh:cmd"?: string; @@ -2169,6 +2207,7 @@ declare global { icon?: string; color?: string; tabids: string[]; + pinnedtabids: string[]; activetabid: string; }; diff --git a/package-lock.json b/package-lock.json index 1798a0fa38..a6b9f3263f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,6 +178,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -198,6 +199,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -457,6 +459,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.37.0.tgz", "integrity": "sha512-DAFVUvEg+u7jUs6BZiVz9zdaUebYULPiQ4LM2R4n8Nujzyj7BZzGr2DCd85ip4p/cx7nAZWKM8pLcGtkTRTdsg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.37.0", "@algolia/requester-browser-xhr": "5.37.0", @@ -610,6 +613,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2468,6 +2472,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2490,6 +2495,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2599,6 +2605,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3020,6 +3027,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4071,6 +4079,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4833,7 +4842,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -4848,18 +4856,6 @@ "node": ">=14.14" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -5576,468 +5572,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6407,6 +5941,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -8248,6 +7783,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -8642,7 +8178,8 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/@table-nav/core/-/core-0.0.7.tgz", "integrity": "sha512-pCh18jHDRe3tw9sJZXfKi4cSD6VjHbn40CYdqhp5X91SIX7rakDEQAsTx6F7Fv9TUv265l+5rUDcYNaJ0N0cqQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@table-nav/react": { "version": "0.0.7", @@ -9664,6 +9201,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.2.tgz", "integrity": "sha512-lif9hF9afNk39jMUVYk5eyYEojLZQqaYX61LfuwUJJ1+qiQbh7jVaZXskYgzyjAIFDFQRf5Sd6MVM7EyXkfiRw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -9739,6 +9277,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -10020,6 +9559,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -10714,6 +10254,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10809,6 +10350,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -10873,6 +10415,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.37.0.tgz", "integrity": "sha512-y7gau/ZOQDqoInTQp0IwTOjkrHc4Aq4R8JgpmCleFwiLl+PbN2DMWoDUWZnrK8AhNJwT++dn28Bt4NZYNLAmuA==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.3.0", "@algolia/client-abtesting": "5.37.0", @@ -11829,6 +11372,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -12337,6 +11881,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", @@ -13196,8 +12741,7 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -13365,6 +12909,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13695,6 +13240,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14104,6 +13650,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -14549,6 +14096,7 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -15054,7 +14602,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -15075,7 +14622,6 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -15091,7 +14637,6 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", - "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -15102,7 +14647,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.0.0" } @@ -15432,6 +14976,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -20284,6 +19829,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -22598,7 +22144,6 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -23407,7 +22952,8 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-2.14.0.tgz", "integrity": "sha512-RjV0pqc79kYhQLC3vTcLRb5GLpI1n6qh0Oua3g+bGH4EgNOJHVBGP7u0zZtxoAa0dkHlAqTTSYRb9MMmxNLjig==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/overlayscrollbars-react": { "version": "0.5.6", @@ -24187,6 +23733,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -25090,6 +24637,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25689,7 +25237,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -25707,7 +25254,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -25808,6 +25354,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -25959,6 +25506,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26186,6 +25734,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26234,6 +25783,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -26307,6 +25857,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -26372,6 +25923,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -28120,7 +27672,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -28135,7 +27686,6 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -28157,7 +27707,6 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -28196,6 +27745,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -28365,6 +27915,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz", "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -28483,6 +28034,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -28874,61 +28426,6 @@ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", "license": "MIT" }, - "node_modules/sharp": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", - "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.4", - "semver": "^7.7.2" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.3", - "@img/sharp-darwin-x64": "0.34.3", - "@img/sharp-libvips-darwin-arm64": "1.2.0", - "@img/sharp-libvips-darwin-x64": "1.2.0", - "@img/sharp-libvips-linux-arm": "1.2.0", - "@img/sharp-libvips-linux-arm64": "1.2.0", - "@img/sharp-libvips-linux-ppc64": "1.2.0", - "@img/sharp-libvips-linux-s390x": "1.2.0", - "@img/sharp-libvips-linux-x64": "1.2.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", - "@img/sharp-libvips-linuxmusl-x64": "1.2.0", - "@img/sharp-linux-arm": "0.34.3", - "@img/sharp-linux-arm64": "0.34.3", - "@img/sharp-linux-ppc64": "0.34.3", - "@img/sharp-linux-s390x": "0.34.3", - "@img/sharp-linux-x64": "0.34.3", - "@img/sharp-linuxmusl-arm64": "0.34.3", - "@img/sharp-linuxmusl-x64": "0.34.3", - "@img/sharp-wasm32": "0.34.3", - "@img/sharp-win32-arm64": "0.34.3", - "@img/sharp-win32-ia32": "0.34.3", - "@img/sharp-win32-x64": "0.34.3" - } - }, - "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -29812,7 +29309,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", @@ -29840,7 +29336,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=16" } @@ -29968,7 +29463,6 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -30487,7 +29981,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsunami-frontend": { "resolved": "tsunami/frontend", @@ -31068,6 +30563,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -31166,6 +30662,7 @@ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", "license": "MIT", + "peer": true, "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", @@ -32048,6 +31545,7 @@ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -32208,6 +31706,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -32450,6 +31949,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -33478,6 +32978,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -33527,7 +33028,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "tsunami/frontend/node_modules/redux-thunk": { "version": "3.1.0", diff --git a/pkg/waveobj/wtype.go b/pkg/waveobj/wtype.go index 0ac9e92eb1..2bab69a9e4 100644 --- a/pkg/waveobj/wtype.go +++ b/pkg/waveobj/wtype.go @@ -172,14 +172,15 @@ type ActiveTabUpdate struct { } type Workspace struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name,omitempty"` - Icon string `json:"icon,omitempty"` - Color string `json:"color,omitempty"` - TabIds []string `json:"tabids"` - ActiveTabId string `json:"activetabid"` - Meta MetaMapType `json:"meta"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name,omitempty"` + Icon string `json:"icon,omitempty"` + Color string `json:"color,omitempty"` + TabIds []string `json:"tabids"` + PinnedTabIds []string `json:"pinnedtabids"` + ActiveTabId string `json:"activetabid"` + Meta MetaMapType `json:"meta"` } func (*Workspace) GetOType() string { diff --git a/pkg/wconfig/defaultconfig/widgets.json b/pkg/wconfig/defaultconfig/widgets.json index eb978d6448..9d2999ecae 100644 --- a/pkg/wconfig/defaultconfig/widgets.json +++ b/pkg/wconfig/defaultconfig/widgets.json @@ -50,5 +50,16 @@ "view": "processviewer" } } + }, + "defwidget@gitchanges": { + "display:order": 0, + "icon": "code-branch", + "label": "changes", + "description": "Git changed files - click to open, double-click for diff", + "blockdef": { + "meta": { + "view": "gitstatus" + } + } } } diff --git a/pkg/wcore/workspace.go b/pkg/wcore/workspace.go index c01e509a13..4c9ad23120 100644 --- a/pkg/wcore/workspace.go +++ b/pkg/wcore/workspace.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package wcore @@ -52,11 +52,12 @@ var WorkspaceIcons = [...]string{ func CreateWorkspace(ctx context.Context, name string, icon string, color string, applyDefaults bool, isInitialLaunch bool) (*waveobj.Workspace, error) { ws := &waveobj.Workspace{ - OID: uuid.NewString(), - TabIds: []string{}, - Name: "", - Icon: "", - Color: "", + OID: uuid.NewString(), + TabIds: []string{}, + PinnedTabIds: []string{}, + Name: "", + Icon: "", + Color: "", } err := wstore.DBInsert(ctx, ws) if err != nil { @@ -284,6 +285,7 @@ func createTabObj(ctx context.Context, workspaceId string, name string, meta wav OID: layoutStateId, } ws.TabIds = append(ws.TabIds, tab.OID) + ws.PinnedTabIds = append(ws.PinnedTabIds, tab.OID) wstore.DBInsert(ctx, tab) wstore.DBInsert(ctx, layoutState) wstore.DBUpdate(ctx, ws) @@ -307,6 +309,12 @@ func DeleteTab(ctx context.Context, workspaceId string, tabId string, recursive } ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...) + // remove from pinned list if present + pinnedIdx := utilfn.FindStringInSlice(ws.PinnedTabIds, tabId) + if pinnedIdx != -1 { + ws.PinnedTabIds = append(ws.PinnedTabIds[:pinnedIdx], ws.PinnedTabIds[pinnedIdx+1:]...) + } + // close blocks (sends events + stops block controllers) tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId) if tab != nil { @@ -361,6 +369,9 @@ func SetActiveTab(ctx context.Context, workspaceId string, tabId string) error { return fmt.Errorf("tab not found: %q", tabId) } workspace.ActiveTabId = tabId + if utilfn.FindStringInSlice(workspace.TabIds, tabId) != -1 && utilfn.FindStringInSlice(workspace.PinnedTabIds, tabId) == -1 { + workspace.PinnedTabIds = append(workspace.PinnedTabIds, tabId) + } wstore.DBUpdate(ctx, workspace) } return nil @@ -383,6 +394,62 @@ func UpdateWorkspaceTabIds(ctx context.Context, workspaceId string, tabIds []str return nil } +func PinTab(ctx context.Context, workspaceId string, tabId string) error { + ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + if utilfn.FindStringInSlice(ws.TabIds, tabId) == -1 { + return fmt.Errorf("tab %s not found in workspace %s", tabId, workspaceId) + } + if utilfn.FindStringInSlice(ws.PinnedTabIds, tabId) != -1 { + return nil + } + ws.PinnedTabIds = append(ws.PinnedTabIds, tabId) + wstore.DBUpdate(ctx, ws) + return nil +} + +func UnpinTab(ctx context.Context, workspaceId string, tabId string) error { + ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + pinnedIdx := utilfn.FindStringInSlice(ws.PinnedTabIds, tabId) + if pinnedIdx == -1 { + return nil + } + ws.PinnedTabIds = append(ws.PinnedTabIds[:pinnedIdx], ws.PinnedTabIds[pinnedIdx+1:]...) + if ws.ActiveTabId == tabId && len(ws.PinnedTabIds) > 0 { + ws.ActiveTabId = ws.PinnedTabIds[max(0, min(pinnedIdx-1, len(ws.PinnedTabIds)-1))] + SendActiveTabUpdate(ctx, workspaceId, ws.ActiveTabId) + } + wstore.DBUpdate(ctx, ws) + return nil +} + +func UpdateWorkspacePinnedTabIds(ctx context.Context, workspaceId string, pinnedTabIds []string) error { + ws, _ := wstore.DBGet[*waveobj.Workspace](ctx, workspaceId) + if ws == nil { + return fmt.Errorf("workspace not found: %q", workspaceId) + } + tabSet := make(map[string]bool, len(ws.TabIds)) + for _, id := range ws.TabIds { + tabSet[id] = true + } + seen := make(map[string]bool, len(pinnedTabIds)) + var filtered []string + for _, id := range pinnedTabIds { + if tabSet[id] && !seen[id] { + seen[id] = true + filtered = append(filtered, id) + } + } + ws.PinnedTabIds = filtered + wstore.DBUpdate(ctx, ws) + return nil +} + func ListWorkspaces(ctx context.Context) (waveobj.WorkspaceList, error) { workspaces, err := wstore.DBGetAllObjsByType[*waveobj.Workspace](ctx, waveobj.OType_Workspace) if err != nil { diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index d5333aec2b..11465cdc9b 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -658,6 +658,12 @@ func PathCommand(w *wshutil.WshRpc, data wshrpc.PathCommandData, opts *wshrpc.Rp return resp, err } +// command "pintab", wshserver.PinTabCommand +func PinTabCommand(w *wshutil.WshRpc, arg1 string, arg2 string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "pintab", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts) + return err +} + // command "publishapp", wshserver.PublishAppCommand func PublishAppCommand(w *wshutil.WshRpc, data wshrpc.CommandPublishAppData, opts *wshrpc.RpcOpts) (*wshrpc.CommandPublishAppRtnData, error) { resp, err := sendRpcRequestCallHelper[*wshrpc.CommandPublishAppRtnData](w, "publishapp", data, opts) @@ -736,6 +742,18 @@ func RemoteGetInfoCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (wshrpc.Remot return resp, err } +// command "remotegitlinediff", wshserver.RemoteGitLineDiffCommand +func RemoteGitLineDiffCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteGitLineDiffData, opts *wshrpc.RpcOpts) (*wshrpc.GitLineDiffResponse, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.GitLineDiffResponse](w, "remotegitlinediff", data, opts) + return resp, err +} + +// command "remotegitstatus", wshserver.RemoteGitStatusCommand +func RemoteGitStatusCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteGitStatusData, opts *wshrpc.RpcOpts) (*wshrpc.GitStatusResponse, error) { + resp, err := sendRpcRequestCallHelper[*wshrpc.GitStatusResponse](w, "remotegitstatus", data, opts) + return resp, err +} + // command "remoteinstallrcfiles", wshserver.RemoteInstallRcFilesCommand func RemoteInstallRcFilesCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "remoteinstallrcfiles", nil, opts) @@ -936,12 +954,24 @@ func TestMultiArgCommand(w *wshutil.WshRpc, arg1 string, arg2 int, arg3 bool, op return resp, err } +// command "unpintab", wshserver.UnpinTabCommand +func UnpinTabCommand(w *wshutil.WshRpc, arg1 string, arg2 string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "unpintab", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts) + return err +} + // command "updatetabname", wshserver.UpdateTabNameCommand func UpdateTabNameCommand(w *wshutil.WshRpc, arg1 string, arg2 string, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "updatetabname", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts) return err } +// command "updateworkspacepinnedtabids", wshserver.UpdateWorkspacePinnedTabIdsCommand +func UpdateWorkspacePinnedTabIdsCommand(w *wshutil.WshRpc, arg1 string, arg2 []string, opts *wshrpc.RpcOpts) error { + _, err := sendRpcRequestCallHelper[any](w, "updateworkspacepinnedtabids", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts) + return err +} + // command "updateworkspacetabids", wshserver.UpdateWorkspaceTabIdsCommand func UpdateWorkspaceTabIdsCommand(w *wshutil.WshRpc, arg1 string, arg2 []string, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "updateworkspacetabids", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts) diff --git a/pkg/wshrpc/wshremote/gitstatus.go b/pkg/wshrpc/wshremote/gitstatus.go new file mode 100644 index 0000000000..fe44eaf02a --- /dev/null +++ b/pkg/wshrpc/wshremote/gitstatus.go @@ -0,0 +1,161 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wshremote + +import ( + "bufio" + "context" + "fmt" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/wavetermdev/waveterm/pkg/wshrpc" +) + +const GitCommandTimeout = 10 * time.Second + +func (impl *ServerImpl) RemoteGitStatusCommand(ctx context.Context, data wshrpc.CommandRemoteGitStatusData) (*wshrpc.GitStatusResponse, error) { + cwd := data.Cwd + if cwd == "" { + return nil, fmt.Errorf("cwd is required") + } + + branch, err := getGitBranch(ctx, cwd) + if err != nil { + return &wshrpc.GitStatusResponse{Error: err.Error()}, nil + } + + files, err := getGitStatusFiles(ctx, cwd) + if err != nil { + return &wshrpc.GitStatusResponse{Error: err.Error()}, nil + } + + return &wshrpc.GitStatusResponse{ + Branch: branch, + Files: files, + }, nil +} + +func getGitBranch(ctx context.Context, cwd string) (string, error) { + ctx, cancel := context.WithTimeout(ctx, GitCommandTimeout) + defer cancel() + cmd := exec.CommandContext(ctx, "git", "branch", "--show-current") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("not a git repository or git not available") + } + return strings.TrimSpace(string(out)), nil +} + +func getGitStatusFiles(ctx context.Context, cwd string) ([]wshrpc.GitStatusFile, error) { + ctx, cancel := context.WithTimeout(ctx, GitCommandTimeout) + defer cancel() + cmd := exec.CommandContext(ctx, "git", "status", "--porcelain") + cmd.Dir = cwd + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("git status failed: %v", err) + } + + var files []wshrpc.GitStatusFile + scanner := bufio.NewScanner(strings.NewReader(string(out))) + for scanner.Scan() { + line := scanner.Text() + if len(line) < 4 { + continue + } + status := strings.TrimSpace(line[:2]) + file := line[3:] + if idx := strings.Index(file, " -> "); idx >= 0 { + file = file[idx+4:] + } + files = append(files, wshrpc.GitStatusFile{ + Status: status, + File: file, + }) + } + return files, nil +} + +var hunkHeaderRegex = regexp.MustCompile(`^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@`) + +func (impl *ServerImpl) RemoteGitLineDiffCommand(ctx context.Context, data wshrpc.CommandRemoteGitLineDiffData) (*wshrpc.GitLineDiffResponse, error) { + if data.Cwd == "" || data.File == "" { + return nil, fmt.Errorf("cwd and file are required") + } + + relPath := data.File + if filepath.IsAbs(relPath) { + rel, err := filepath.Rel(data.Cwd, relPath) + if err == nil { + relPath = rel + } + } + + gitCtx, cancel := context.WithTimeout(ctx, GitCommandTimeout) + defer cancel() + cmd := exec.CommandContext(gitCtx, "git", "diff", "HEAD", "--unified=0", "--", relPath) + cmd.Dir = data.Cwd + out, err := cmd.Output() + if err != nil { + exitErr, ok := err.(*exec.ExitError) + if ok && exitErr.ExitCode() == 1 { + // diff returns 1 when there are differences - that's fine, use stdout + } else { + return &wshrpc.GitLineDiffResponse{Error: fmt.Sprintf("git diff failed: %v", err)}, nil + } + } + + hunks := parseUnifiedDiffHunks(string(out)) + return &wshrpc.GitLineDiffResponse{Hunks: hunks}, nil +} + +func parseUnifiedDiffHunks(diffOutput string) []wshrpc.GitLineDiffHunk { + var hunks []wshrpc.GitLineDiffHunk + scanner := bufio.NewScanner(strings.NewReader(diffOutput)) + + for scanner.Scan() { + line := scanner.Text() + matches := hunkHeaderRegex.FindStringSubmatch(line) + if matches == nil { + continue + } + + oldCount := 1 + if matches[2] != "" { + oldCount, _ = strconv.Atoi(matches[2]) + } + newStart, _ := strconv.Atoi(matches[3]) + newCount := 1 + if matches[4] != "" { + newCount, _ = strconv.Atoi(matches[4]) + } + + if oldCount == 0 && newCount > 0 { + hunks = append(hunks, wshrpc.GitLineDiffHunk{ + Type: "added", + StartLine: newStart, + EndLine: newStart + newCount - 1, + }) + } else if newCount == 0 && oldCount > 0 { + hunks = append(hunks, wshrpc.GitLineDiffHunk{ + Type: "deleted", + StartLine: newStart, + EndLine: newStart, + }) + } else { + hunks = append(hunks, wshrpc.GitLineDiffHunk{ + Type: "modified", + StartLine: newStart, + EndLine: newStart + newCount - 1, + }) + } + } + return hunks +} diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 51e2338ba8..b5a22fd6f0 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -96,6 +96,9 @@ type WshRpcInterface interface { GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error) UpdateTabNameCommand(ctx context.Context, tabId string, newName string) error UpdateWorkspaceTabIdsCommand(ctx context.Context, workspaceId string, tabIds []string) error + PinTabCommand(ctx context.Context, workspaceId string, tabId string) error + UnpinTabCommand(ctx context.Context, workspaceId string, tabId string) error + UpdateWorkspacePinnedTabIdsCommand(ctx context.Context, workspaceId string, pinnedTabIds []string) error GetAllBadgesCommand(ctx context.Context) ([]baseds.BadgeEvent, error) // connection functions @@ -129,6 +132,8 @@ type WshRpcInterface interface { BadgeWatchPidCommand(ctx context.Context, data CommandBadgeWatchPidData) error RemoteProcessListCommand(ctx context.Context, data CommandRemoteProcessListData) (*ProcessListResponse, error) RemoteProcessSignalCommand(ctx context.Context, data CommandRemoteProcessSignalData) error + RemoteGitStatusCommand(ctx context.Context, data CommandRemoteGitStatusData) (*GitStatusResponse, error) + RemoteGitLineDiffCommand(ctx context.Context, data CommandRemoteGitLineDiffData) (*GitLineDiffResponse, error) // emain WebSelectorCommand(ctx context.Context, data CommandWebSelectorData) ([]string, error) @@ -925,3 +930,34 @@ type CommandRemoteProcessSignalData struct { Pid int32 `json:"pid"` Signal string `json:"signal"` } + +type CommandRemoteGitStatusData struct { + Cwd string `json:"cwd"` +} + +type GitStatusFile struct { + Status string `json:"status"` + File string `json:"file"` +} + +type GitStatusResponse struct { + Branch string `json:"branch"` + Files []GitStatusFile `json:"files"` + Error string `json:"error,omitempty"` +} + +type CommandRemoteGitLineDiffData struct { + Cwd string `json:"cwd"` + File string `json:"file"` +} + +type GitLineDiffHunk struct { + Type string `json:"type"` + StartLine int `json:"startline"` + EndLine int `json:"endline"` +} + +type GitLineDiffResponse struct { + Hunks []GitLineDiffHunk `json:"hunks"` + Error string `json:"error,omitempty"` +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 38006fd9a8..68960ebc46 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -175,6 +175,36 @@ func (ws *WshServer) UpdateWorkspaceTabIdsCommand(ctx context.Context, workspace return nil } +func (ws *WshServer) PinTabCommand(ctx context.Context, workspaceId string, tabId string) error { + oref := waveobj.ORef{OType: waveobj.OType_Workspace, OID: workspaceId} + err := wcore.PinTab(ctx, workspaceId, tabId) + if err != nil { + return fmt.Errorf("error pinning tab: %w", err) + } + wcore.SendWaveObjUpdate(oref) + return nil +} + +func (ws *WshServer) UnpinTabCommand(ctx context.Context, workspaceId string, tabId string) error { + oref := waveobj.ORef{OType: waveobj.OType_Workspace, OID: workspaceId} + err := wcore.UnpinTab(ctx, workspaceId, tabId) + if err != nil { + return fmt.Errorf("error unpinning tab: %w", err) + } + wcore.SendWaveObjUpdate(oref) + return nil +} + +func (ws *WshServer) UpdateWorkspacePinnedTabIdsCommand(ctx context.Context, workspaceId string, pinnedTabIds []string) error { + oref := waveobj.ORef{OType: waveobj.OType_Workspace, OID: workspaceId} + err := wcore.UpdateWorkspacePinnedTabIds(ctx, workspaceId, pinnedTabIds) + if err != nil { + return fmt.Errorf("error updating workspace pinned tab ids: %w", err) + } + wcore.SendWaveObjUpdate(oref) + return nil +} + func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error { log.Printf("SetMetaCommand: %s | %v\n", data.ORef, data.Meta) oref := data.ORef