From 69e926441eebf4f2e2768177a4945b041d93670d Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Sat, 14 Mar 2026 08:16:54 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20leave=20channe?= =?UTF-8?q?l,=20thread=20panel=20lifecycle,=20agent=20form,=20and=20text?= =?UTF-8?q?=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix leave channel: handle ErrOwnerCannotLeave with error display, remove all owned agents from channel - Close thread panel when navigating between channels/DMs - Remove Type dropdown from agent registration (agents are always AI; human accounts created via CLI) - Fix dashboard showing "Untitled conversation" — now shows last agent name - Fix "1 msgs" → "1 msg" singular form on dashboard - Fix conversation detail "-- N messages" → "— N message(s)" with em-dash - Hide "done" status badge in MessageList and conversation detail (consistent with DM view behavior) - Add thread reply buttons and reply count to channel and DM messages - Add agent selector for multi-agent users in channel and DM compose - Add "Join Channel" prompt for non-members in channel compose area - Show "(you)" indicator on channel member list for owned agents - Add agent detail page with messages endpoint - Improve release workflow and Dockerfile Co-Authored-By: Claude Opus 4.6 --- .github/workflows/release.yml | 176 ++++++++++++++---- Dockerfile | 3 +- Makefile | 3 +- cmd/synapbus/main.go | 14 +- internal/api/agents_handler.go | 41 ++++ internal/api/router.go | 1 + internal/web/dist/index.html | 12 +- web/src/lib/api/client.ts | 4 +- web/src/lib/components/AgentCard.svelte | 5 +- web/src/lib/components/MessageList.svelte | 2 + web/src/lib/components/Sidebar.svelte | 1 + web/src/routes/+page.svelte | 6 +- web/src/routes/agents/+page.svelte | 12 +- web/src/routes/agents/[name]/+page.svelte | 98 ++++++++++ web/src/routes/channels/[name]/+page.svelte | 96 +++++++++- web/src/routes/conversations/+page.svelte | 16 +- .../routes/conversations/[id]/+page.svelte | 4 +- web/src/routes/dm/[name]/+page.svelte | 58 +++++- 18 files changed, 462 insertions(+), 90 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2ea241..cb7ea26 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,40 +5,41 @@ on: tags: - "v*" +permissions: + contents: write + packages: write + +env: + GO_VERSION: "1.25" + NODE_VERSION: "20" + jobs: - release: - name: Build & Release + build: + name: Build ${{ matrix.goos }}/${{ matrix.goarch }} runs-on: ubuntu-latest - permissions: - contents: write strategy: matrix: include: - goos: linux goarch: amd64 - suffix: "" - goos: linux goarch: arm64 - suffix: "" - goos: darwin goarch: amd64 - suffix: "" - goos: darwin goarch: arm64 - suffix: "" - goos: windows goarch: amd64 - suffix: ".exe" steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: ${{ env.GO_VERSION }} - uses: actions/setup-node@v4 with: - node-version: "20" + node-version: ${{ env.NODE_VERSION }} cache: npm cache-dependency-path: web/package-lock.json @@ -51,60 +52,154 @@ jobs: rm -rf internal/web/dist cp -r web/build internal/web/dist + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Build binary env: CGO_ENABLED: "0" GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} run: | - go build -ldflags "-s -w" -o synapbus-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }} ./cmd/synapbus/ + SUFFIX="" + if [ "${{ matrix.goos }}" = "windows" ]; then + SUFFIX=".exe" + fi + go build \ + -ldflags "-s -w -X main.version=${{ steps.version.outputs.VERSION }}" \ + -o "synapbus${SUFFIX}" \ + ./cmd/synapbus/ + + - name: Create archive + run: | + ARCHIVE_NAME="synapbus-${{ matrix.goos }}-${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + zip "${ARCHIVE_NAME}.zip" synapbus.exe + else + tar czf "${ARCHIVE_NAME}.tar.gz" synapbus + fi - uses: actions/upload-artifact@v4 with: name: synapbus-${{ matrix.goos }}-${{ matrix.goarch }} - path: synapbus-${{ matrix.goos }}-${{ matrix.goarch }}${{ matrix.suffix }} + path: synapbus-*.tar.gz + if-no-files-found: ignore - publish: - name: Publish Release - needs: release + - uses: actions/upload-artifact@v4 + if: matrix.goos == 'windows' + with: + name: synapbus-${{ matrix.goos }}-${{ matrix.goarch }}-zip + path: synapbus-*.zip + if-no-files-found: ignore + + homebrew: + name: Generate Homebrew Formula + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Calculate checksums and generate formula + run: | + DARWIN_AMD64_SHA=$(sha256sum artifacts/synapbus-darwin-amd64.tar.gz | awk '{print $1}') + DARWIN_ARM64_SHA=$(sha256sum artifacts/synapbus-darwin-arm64.tar.gz | awk '{print $1}') + LINUX_AMD64_SHA=$(sha256sum artifacts/synapbus-linux-amd64.tar.gz | awk '{print $1}') + LINUX_ARM64_SHA=$(sha256sum artifacts/synapbus-linux-arm64.tar.gz | awk '{print $1}') + VERSION="${{ steps.version.outputs.VERSION }}" + REPO="https://github.com/${{ github.repository }}" + + cat > synapbus.rb <> "$GITHUB_OUTPUT" - uses: docker/setup-qemu-action@v3 @@ -116,13 +211,14 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract version tag + - name: Docker metadata id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/smart-mcp-proxy/synapbus + images: ghcr.io/synapbus/synapbus tags: | type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} type=raw,value=latest - uses: docker/build-push-action@v6 @@ -130,6 +226,8 @@ jobs: context: . push: true platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.version.outputs.VERSION }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/Dockerfile b/Dockerfile index 459bad8..a8868ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,14 @@ RUN npm run build # Stage 2: Build Go binary FROM golang:1.25-alpine AS go-builder +ARG VERSION=dev RUN apk add --no-cache tzdata WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . COPY --from=web-builder /app/web/build internal/web/dist/ -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /synapbus ./cmd/synapbus/ +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X main.version=${VERSION}" -o /synapbus ./cmd/synapbus/ # Stage 3: Runtime FROM scratch diff --git a/Makefile b/Makefile index 8f8a831..f894da9 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ BINARY := synapbus MODULE := github.com/synapbus/synapbus BUILD_DIR := bin -LDFLAGS := -s -w +VERSION ?= dev +LDFLAGS := -s -w -X main.version=$(VERSION) CGO_ENABLED := 0 diff --git a/cmd/synapbus/main.go b/cmd/synapbus/main.go index 0cd60b2..f27e048 100644 --- a/cmd/synapbus/main.go +++ b/cmd/synapbus/main.go @@ -38,6 +38,9 @@ import ( "github.com/synapbus/synapbus/internal/web" ) +// version is set at build time via -ldflags "-X main.version=..." +var version = "dev" + var ( host string port int @@ -50,9 +53,10 @@ var ( func main() { rootCmd := &cobra.Command{ - Use: "synapbus", - Short: "SynapBus — MCP-native agent-to-agent messaging", - Long: "Local-first, MCP-native messaging service for AI agents. Single binary with embedded storage, semantic search, and a Slack-like Web UI.", + Use: "synapbus", + Short: "SynapBus — MCP-native agent-to-agent messaging", + Long: "Local-first, MCP-native messaging service for AI agents. Single binary with embedded storage, semantic search, and a Slack-like Web UI.", + Version: version, } serveCmd := &cobra.Command{ @@ -378,7 +382,7 @@ func runServe(cmd *cobra.Command, args []string) error { slog.Info("task expiry worker started") // Create health checker - healthChecker := health.NewChecker(db.DB, "0.1.0") + healthChecker := health.NewChecker(db.DB, version) // Set up chi router r := chi.NewRouter() @@ -390,7 +394,7 @@ func runServe(cmd *cobra.Command, args []string) error { } // Health endpoint (no auth) - r.Get("/health", mcpserver.NewHealthHandler(mcpSrv.ConnectionManager(), "0.1.0", startTime)) + r.Get("/health", mcpserver.NewHealthHandler(mcpSrv.ConnectionManager(), version, startTime)) // Kubernetes-style health probes (no auth, always registered) r.Get("/healthz", healthChecker.Healthz) diff --git a/internal/api/agents_handler.go b/internal/api/agents_handler.go index 9053c7e..ca0f423 100644 --- a/internal/api/agents_handler.go +++ b/internal/api/agents_handler.go @@ -123,6 +123,47 @@ func (h *AgentsHandler) RegisterAgent(w http.ResponseWriter, r *http.Request) { }) } +// UpdateAgent handles PUT /api/agents/{name}. +func (h *AgentsHandler) UpdateAgent(w http.ResponseWriter, r *http.Request) { + ownerID, ok := OwnerIDFromContext(r.Context()) + if !ok { + writeJSON(w, http.StatusUnauthorized, errorBody("unauthorized", "Authentication required")) + return + } + + name := chi.URLParam(r, "name") + + // Verify ownership + agent, err := h.agentService.GetAgent(r.Context(), name) + if err != nil { + writeJSON(w, http.StatusNotFound, errorBody("not_found", "Agent not found")) + return + } + if agent.OwnerID != ownerID { + writeJSON(w, http.StatusForbidden, errorBody("forbidden", "You do not have access to this agent")) + return + } + + var req struct { + DisplayName string `json:"display_name"` + Capabilities json.RawMessage `json:"capabilities,omitempty"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, errorBody("invalid_request", "Invalid JSON body")) + return + } + + updated, err := h.agentService.UpdateAgent(r.Context(), name, req.DisplayName, req.Capabilities) + if err != nil { + h.logger.Error("update agent failed", "error", err) + writeJSON(w, http.StatusBadRequest, errorBody("update_failed", err.Error())) + return + } + + writeJSON(w, http.StatusOK, map[string]any{"agent": updated}) +} + // DeleteAgent handles DELETE /api/agents/{name}. func (h *AgentsHandler) DeleteAgent(w http.ResponseWriter, r *http.Request) { ownerID, ok := OwnerIDFromContext(r.Context()) diff --git a/internal/api/router.go b/internal/api/router.go index a4fc98b..e0f24f8 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -98,6 +98,7 @@ func NewRouterWithConfig(cfg RouterConfig) chi.Router { r.Get("/api/agents", agentsHandler.ListAgents) r.Get("/api/agents/{name}", agentsHandler.GetAgent) r.Post("/api/agents", agentsHandler.RegisterAgent) + r.Put("/api/agents/{name}", agentsHandler.UpdateAgent) r.Delete("/api/agents/{name}", agentsHandler.DeleteAgent) r.Post("/api/agents/{name}/revoke-key", agentsHandler.RevokeKey) r.Get("/api/agents/{name}/messages", messagesHandler.DMMessages) diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 0895219..e98a1c4 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -8,29 +8,29 @@ - - + + - +
@@ -22,6 +22,9 @@ {#if agent.display_name}

@{agent.name}

{/if} + {#if ownerName} +

Owner: {ownerName}

+ {/if}
diff --git a/web/src/lib/components/MessageList.svelte b/web/src/lib/components/MessageList.svelte index bd860b7..1332459 100644 --- a/web/src/lib/components/MessageList.svelte +++ b/web/src/lib/components/MessageList.svelte @@ -91,7 +91,9 @@ {msg.to_agent} {/if} {formatTime(msg.created_at)} + {#if msg.status !== 'done'} {msg.status} + {/if} {#if msg.priority >= 8} P{msg.priority} {:else if msg.priority >= 5} diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte index 5acdb0b..b903711 100644 --- a/web/src/lib/components/Sidebar.svelte +++ b/web/src/lib/components/Sidebar.svelte @@ -199,6 +199,7 @@ > {agent.display_name || agent.name} + (you) {#if agent.type === 'ai'} AI {/if} diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index b201e03..d67841a 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -92,14 +92,14 @@

- {conv.subject || 'Untitled conversation'} + {conv.last_agent}

- {conv.last_agent}: {conv.last_message} + {conv.last_message}

- {conv.message_count} msgs + {conv.message_count} {conv.message_count === 1 ? 'msg' : 'msgs'}
diff --git a/web/src/routes/agents/+page.svelte b/web/src/routes/agents/+page.svelte index e50d0cc..950de1c 100644 --- a/web/src/routes/agents/+page.svelte +++ b/web/src/routes/agents/+page.svelte @@ -1,5 +1,6 @@
@@ -156,6 +195,65 @@
+ +
+
+

Access Rights

+
+
+ {#if accessError} +
{accessError}
+ {/if} + {#if accessSaved} +
Access rights saved successfully.
+ {/if} + +
+ + +

Comma-separated list of channel names this agent can access. Leave empty for all channels.

+
+ +
+ +

Agent can read messages but cannot send.

+
+ +
+ + +
+ + +
+
+
diff --git a/web/src/routes/channels/[name]/+page.svelte b/web/src/routes/channels/[name]/+page.svelte index e135e4b..4ca23fe 100644 --- a/web/src/routes/channels/[name]/+page.svelte +++ b/web/src/routes/channels/[name]/+page.svelte @@ -1,6 +1,7 @@