diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cd86e5..9f3964d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,7 @@ jobs: name: Docker Build & Push runs-on: ubuntu-latest needs: [quality, test] + if: github.event_name != 'pull_request' permissions: contents: read packages: write diff --git a/.gitignore b/.gitignore index 1bf5b94..5dcd90c 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ temp/ # AI model files and caches models/ +!pkg/ai/models/ +!pkg/ai/models/*.yaml +!pkg/ai/models/*.go *.model *.cache *.pkl diff --git a/Dockerfile b/Dockerfile index 829e8f1..542e719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,49 @@ # Build stage -FROM golang:1.24-alpine AS builder +# syntax=docker/dockerfile:1.7 +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +FROM --platform=$BUILDPLATFORM node:20-alpine AS frontend-builder +WORKDIR /workspace + +# Copy repository contents (needed because frontend build emits to ../pkg/plugin/assets) +COPY . . + +# Cache npm modules to speed up rebuilds +RUN --mount=type=cache,target=/root/.npm npm --prefix frontend ci +RUN npm --prefix frontend run build + +FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS builder +ARG TARGETOS +ARG TARGETARCH # Install build dependencies -RUN apk add --no-cache git ca-certificates tzdata nodejs npm +RUN apk add --no-cache git ca-certificates tzdata + +# Enable module caching +ENV GOMODCACHE=/go/pkg/mod +ENV GOCACHE=/root/.cache/go-build # Set working directory -WORKDIR /app +WORKDIR /workspace -# Copy all source code first (needed for go.mod replace directive) -COPY . . +# Copy go module files and download dependencies +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + go mod download -# Download dependencies (replace directive requires source to be present) -RUN go mod download +# Copy source code +COPY . . -# Build frontend assets required for Go embed -RUN npm ci --prefix frontend -RUN npm run build --prefix frontend +# Copy pre-built frontend assets +COPY --from=frontend-builder /workspace/pkg/plugin/assets ./pkg/plugin/assets -# Build the binary -RUN CGO_ENABLED=0 GOOS=linux go build \ - -ldflags="-s -w" \ - -o bin/atest-ext-ai \ - ./cmd/atest-ext-ai +# Build the binary for the target platform +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ + go build -ldflags="-s -w" -o bin/atest-ext-ai ./cmd/atest-ext-ai # Final stage FROM alpine:latest diff --git a/cmd/atest-ext-ai/main.go b/cmd/atest-ext-ai/main.go index daf812f..46712eb 100644 --- a/cmd/atest-ext-ai/main.go +++ b/cmd/atest-ext-ai/main.go @@ -287,7 +287,8 @@ func cleanupSocketFile(path string) error { func createListener(cfg listenerConfig) (net.Listener, error) { if cfg.network == "unix" { dir := filepath.Dir(cfg.address) - if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // socket directory must remain accessible to API clients + // #nosec G301 -- socket directory must remain accessible to API clients + if err := os.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("failed to create socket directory %s: %w", dir, err) } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 45e9b29..85aa898 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -152,13 +152,14 @@ async function handleTest(updatedConfig?: AIConfig) { display: flex; flex-direction: column; width: 100%; - height: 100%; - max-height: 100%; + height: 100vh; + max-height: 100vh; min-height: 0; padding: clamp(16px, 4vw, 32px); box-sizing: border-box; gap: clamp(12px, 2vw, 20px); background: var(--atest-bg-base); + overflow: hidden; } .welcome-panel { @@ -166,18 +167,18 @@ async function handleTest(updatedConfig?: AIConfig) { display: flex; align-items: center; justify-content: center; + overflow: auto; } .chat-content { flex: 1; - display: grid; - grid-template-rows: minmax(0, 1fr) auto; - gap: var(--atest-spacing-md); + display: flex; + flex-direction: column; + gap: 0; overflow: hidden; border-radius: var(--atest-radius-lg); background: var(--atest-bg-surface); box-shadow: var(--atest-shadow-md); - padding: clamp(12px, 3vw, 24px); min-height: 0; max-height: 100%; } @@ -185,6 +186,8 @@ async function handleTest(updatedConfig?: AIConfig) { @media (max-width: 1024px) { .ai-chat-container { padding: 24px; + height: 100vh; + max-height: 100vh; } } @@ -192,21 +195,20 @@ async function handleTest(updatedConfig?: AIConfig) { .ai-chat-container { padding: 20px; gap: 12px; + height: 100vh; + max-height: 100vh; } .chat-content { border-radius: var(--atest-radius-md); - padding: 16px; } } @media (max-width: 480px) { .ai-chat-container { padding: 16px; - } - - .chat-content { - padding: 12px; + height: 100vh; + max-height: 100vh; } } diff --git a/frontend/src/components/AIChatHeader.vue b/frontend/src/components/AIChatHeader.vue index 69b009e..f8fa035 100644 --- a/frontend/src/components/AIChatHeader.vue +++ b/frontend/src/components/AIChatHeader.vue @@ -41,10 +41,11 @@ const statusText = computed(() => t(`ai.status.${props.status}`))