Files
104bf7be35 MUL-7732: regroup settings by scope and consolidate scattered pages (#8874)
* feat(mcp): report how many agents use each workspace MCP server

The workspace MCP library listing now carries agent_count: the number of
live (non-archived) agents each entry is assigned to. Settings shows it so
an admin can tell whether replacing or removing a server affects anyone.

The field is optional in the client schema, so an older server that omits
it simply shows no count.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(settings): regroup settings by scope and consolidate scattered pages

Implements the MUL-7732 settings redesign:

- Navigation is grouped by who a setting affects: Personal, the current
  workspace (with Issues & workflow and Connections & extensions
  sub-groups), and This device on desktop. Pages and sections carry a
  scope badge (account, this device, workspace), and members see a lock on
  pages only owners and admins can change plus a shared read-only notice.
- Settings search in the nav indexes page and row titles and
  descriptions; opening a result scrolls to and highlights the row.
- Preferences is one page instead of three tabs, with a searchable time
  zone picker, a segmented theme control and a field grid for the
  create-issue dialogs.
- Code merges Repositories, GitHub and self-hosted Git. Repositories are
  read-only rows edited through dialogs; self-hosted Git connects in a
  dialog that ends on the one-time webhook secret.
- The PR merge rule moves to Statuses & transitions as an automatic
  transition; Code only shows its current value. Built-in statuses show
  edit and archive as unavailable instead of refusing after a click.
- Integrations becomes Messaging (IM channels only) with a breadcrumb on
  detail pages; Composio moves to Personal as Connected apps.
- Members gets tabs, search, invite and share-link dialogs and a seat
  summary. MCP servers show assignment counts and explain how MCP,
  connected apps and plugins differ. Workspace General shows members plain
  values and changes the issue prefix through an explicit dialog.
- Skill labels are managed from the Skills page; Settings > Labels keeps
  the issue catalog.
- Successful saves no longer toast; failures still do.

Old ?tab= values (github, repositories, integrations, issue, chat, labs,
and the Composio OAuth callback) resolve to the new pages, since server
redirects still use them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs: point settings paths at the regrouped settings pages

GitHub, repositories and self-hosted Git now live under Settings > Code,
IM bots under Settings > Messaging, and the PR merge rule under
Settings > Statuses & transitions. The GitHub guide also drops the old
master switch step in favor of pausing from the GitHub row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(settings): edit the PR merge rule on the Code page

The "After PRs merge, move the issue to" rule sits with the other pull
request rules on Code again instead of a separate Automatic transitions
section. It applies to every code host, so pausing GitHub features does
not lock it. The statuses page goes back to Issue Statuses, and its merge
badge links to the rule on Code.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(settings): manage skill labels under Settings → Labels again

The Skills page entry was buried at the bottom of the filter menu's Labels
submenu. Settings → Labels switches between the issue and skill catalogs
again, using the members page's segmented switch (now SettingsViewTabs),
and the Skills page keeps its shortcut.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(settings): keep GitHub resume reachable after a disconnect

Pausing GitHub features is a workspace setting, but the pause and resume
item lived in a menu that only rendered with an installation. A workspace
paused before disconnecting, or with github_enabled=false and no
installation, lost the way back, and every PR switch stayed disabled. On
deployments without a GitHub App it could not even reconnect, though
Co-authored-by works without one.

The menu now shows whenever GitHub is connected or paused, and only
Disconnect requires an installation. The paused note shows in either state.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(settings): let the URL choose the open members list

The members page read `section` only to initialize its list tab, so a
search result for invitations or share links opened while already on the
page changed the URL but not the list, and back/forward did nothing. With a
router the open list is now derived from the URL, and switching lists
replaces `section`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(settings): tell repositories on different ports apart

The manual repository dialog reused the GitHub import's identity, which
drops the port, so https://git.example.com:9443/acme/api.git read as a
duplicate of the same path on :8443 and could not be saved. Non-default
ports now stay in the identity; default ports, including 22 for ssh://,
still compare equal to the scp-like form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(comments): stop the recipient-menu tests flaking under CI load

The steering tests opened a recipient chip menu while the composer could
still re-render its chips, from the previous send settling or the preview
catching up. That closed the menu, and the menu steps also used the 1s
default wait inside the 5s default test timeout. Under parallel load
locally, about 1 in 20 runs failed the same way as CI.

Menu choices now go through a helper that reopens the menu until the item
shows. The remaining waits get the 5s the first steps already had, and each
test gets 15s. 140 of 140 loaded runs pass.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 18:15:01 +08:00

394 lines
12 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: workspace_mcp.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const addAgentMcpServer = `-- name: AddAgentMcpServer :exec
INSERT INTO agent_mcp_server (agent_id, server_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
`
type AddAgentMcpServerParams struct {
AgentID pgtype.UUID `json:"agent_id"`
ServerID pgtype.UUID `json:"server_id"`
}
func (q *Queries) AddAgentMcpServer(ctx context.Context, arg AddAgentMcpServerParams) error {
_, err := q.db.Exec(ctx, addAgentMcpServer, arg.AgentID, arg.ServerID)
return err
}
const countWorkspaceMcpServerAgents = `-- name: CountWorkspaceMcpServerAgents :many
SELECT ams.server_id, COUNT(*)::bigint AS agent_count
FROM agent_mcp_server ams
JOIN workspace_mcp_server s ON s.id = ams.server_id
JOIN agent a ON a.id = ams.agent_id
WHERE s.workspace_id = $1 AND a.archived_at IS NULL
GROUP BY ams.server_id
`
type CountWorkspaceMcpServerAgentsRow struct {
ServerID pgtype.UUID `json:"server_id"`
AgentCount int64 `json:"agent_count"`
}
// How many live agents each library entry is assigned to, for the workspace
// settings list. Archived agents never receive the server, so they are not
// counted; servers with no assignment are simply absent from the result.
func (q *Queries) CountWorkspaceMcpServerAgents(ctx context.Context, workspaceID pgtype.UUID) ([]CountWorkspaceMcpServerAgentsRow, error) {
rows, err := q.db.Query(ctx, countWorkspaceMcpServerAgents, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []CountWorkspaceMcpServerAgentsRow{}
for rows.Next() {
var i CountWorkspaceMcpServerAgentsRow
if err := rows.Scan(&i.ServerID, &i.AgentCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const createWorkspaceMcpServer = `-- name: CreateWorkspaceMcpServer :one
INSERT INTO workspace_mcp_server (workspace_id, name, config, created_by)
VALUES ($1, $2, $3, $4)
RETURNING id, workspace_id, name, config, created_by, created_at, updated_at
`
type CreateWorkspaceMcpServerParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name string `json:"name"`
Config []byte `json:"config"`
CreatedBy pgtype.UUID `json:"created_by"`
}
func (q *Queries) CreateWorkspaceMcpServer(ctx context.Context, arg CreateWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
row := q.db.QueryRow(ctx, createWorkspaceMcpServer,
arg.WorkspaceID,
arg.Name,
arg.Config,
arg.CreatedBy,
)
var i WorkspaceMcpServer
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteAgentMcpServersByServer = `-- name: DeleteAgentMcpServersByServer :exec
DELETE FROM agent_mcp_server WHERE server_id = $1
`
// Bindings carry no FK, so removing a library entry must sweep them explicitly
// in the same transaction as the delete.
func (q *Queries) DeleteAgentMcpServersByServer(ctx context.Context, serverID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteAgentMcpServersByServer, serverID)
return err
}
const deleteWorkspaceMcpServer = `-- name: DeleteWorkspaceMcpServer :execrows
DELETE FROM workspace_mcp_server
WHERE id = $1 AND workspace_id = $2
`
type DeleteWorkspaceMcpServerParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) DeleteWorkspaceMcpServer(ctx context.Context, arg DeleteWorkspaceMcpServerParams) (int64, error) {
result, err := q.db.Exec(ctx, deleteWorkspaceMcpServer, arg.ID, arg.WorkspaceID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const getWorkspaceMcpServer = `-- name: GetWorkspaceMcpServer :one
SELECT id, workspace_id, name, config, created_by, created_at, updated_at FROM workspace_mcp_server
WHERE id = $1 AND workspace_id = $2
`
type GetWorkspaceMcpServerParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetWorkspaceMcpServer(ctx context.Context, arg GetWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
row := q.db.QueryRow(ctx, getWorkspaceMcpServer, arg.ID, arg.WorkspaceID)
var i WorkspaceMcpServer
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const listAgentMcpServers = `-- name: ListAgentMcpServers :many
SELECT s.id, s.workspace_id, s.name, s.config, s.created_at, s.updated_at, ams.enabled
FROM workspace_mcp_server s
JOIN agent_mcp_server ams ON ams.server_id = s.id
WHERE ams.agent_id = $1
ORDER BY s.name ASC
`
type ListAgentMcpServersRow struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name string `json:"name"`
Config []byte `json:"config"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Enabled bool `json:"enabled"`
}
// Every workspace server bound to this agent, enabled or not, for the agent
// settings list. `enabled` comes from the binding.
func (q *Queries) ListAgentMcpServers(ctx context.Context, agentID pgtype.UUID) ([]ListAgentMcpServersRow, error) {
rows, err := q.db.Query(ctx, listAgentMcpServers, agentID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListAgentMcpServersRow{}
for rows.Next() {
var i ListAgentMcpServersRow
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Config,
&i.CreatedAt,
&i.UpdatedAt,
&i.Enabled,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listEnabledAgentMcpServers = `-- name: ListEnabledAgentMcpServers :many
SELECT s.name, s.config
FROM workspace_mcp_server s
JOIN agent_mcp_server ams ON ams.server_id = s.id
WHERE ams.agent_id = $1 AND ams.enabled = TRUE
ORDER BY s.name ASC
`
type ListEnabledAgentMcpServersRow struct {
Name string `json:"name"`
Config []byte `json:"config"`
}
// Claim path: only bound AND enabled servers reach the runtime. An unbound
// workspace server is invisible to this agent — creating one gives it to
// nobody until someone adds it here.
func (q *Queries) ListEnabledAgentMcpServers(ctx context.Context, agentID pgtype.UUID) ([]ListEnabledAgentMcpServersRow, error) {
rows, err := q.db.Query(ctx, listEnabledAgentMcpServers, agentID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ListEnabledAgentMcpServersRow{}
for rows.Next() {
var i ListEnabledAgentMcpServersRow
if err := rows.Scan(&i.Name, &i.Config); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWorkspaceMcpServers = `-- name: ListWorkspaceMcpServers :many
SELECT id, workspace_id, name, config, created_by, created_at, updated_at FROM workspace_mcp_server
WHERE workspace_id = $1
ORDER BY name ASC
`
// The workspace's MCP library. Returns `config` because the callers that need
// it (claim assembly, the write path's read-modify-write) run server-side; the
// HTTP layer projects it down to the non-secret inventory.
func (q *Queries) ListWorkspaceMcpServers(ctx context.Context, workspaceID pgtype.UUID) ([]WorkspaceMcpServer, error) {
rows, err := q.db.Query(ctx, listWorkspaceMcpServers, workspaceID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []WorkspaceMcpServer{}
for rows.Next() {
var i WorkspaceMcpServer
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockWorkspaceMcpServerForShare = `-- name: LockWorkspaceMcpServerForShare :one
SELECT id FROM workspace_mcp_server
WHERE id = $1 AND workspace_id = $2
FOR SHARE
`
type LockWorkspaceMcpServerForShareParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Taken by the assignment writer before it inserts. A shared row lock
// conflicts with the exclusive lock DeleteWorkspaceMcpServer takes, so an
// assignment can never land in the window between that delete sweeping the
// bindings and committing — the junction has no FK to catch the orphan.
func (q *Queries) LockWorkspaceMcpServerForShare(ctx context.Context, arg LockWorkspaceMcpServerForShareParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, lockWorkspaceMcpServerForShare, arg.ID, arg.WorkspaceID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const lockWorkspaceMcpServerForUpdate = `-- name: LockWorkspaceMcpServerForUpdate :one
SELECT id FROM workspace_mcp_server
WHERE id = $1 AND workspace_id = $2
FOR UPDATE
`
type LockWorkspaceMcpServerForUpdateParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// The delete half of the same protocol: take the row exclusively first, so a
// concurrent assignment either lands before this transaction (and is swept) or
// waits and then finds the server gone.
func (q *Queries) LockWorkspaceMcpServerForUpdate(ctx context.Context, arg LockWorkspaceMcpServerForUpdateParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, lockWorkspaceMcpServerForUpdate, arg.ID, arg.WorkspaceID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const removeAgentMcpServer = `-- name: RemoveAgentMcpServer :execrows
DELETE FROM agent_mcp_server
WHERE agent_id = $1 AND server_id = $2
`
type RemoveAgentMcpServerParams struct {
AgentID pgtype.UUID `json:"agent_id"`
ServerID pgtype.UUID `json:"server_id"`
}
func (q *Queries) RemoveAgentMcpServer(ctx context.Context, arg RemoveAgentMcpServerParams) (int64, error) {
result, err := q.db.Exec(ctx, removeAgentMcpServer, arg.AgentID, arg.ServerID)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const setAgentMcpServerEnabled = `-- name: SetAgentMcpServerEnabled :execrows
UPDATE agent_mcp_server
SET enabled = $3
WHERE agent_id = $1 AND server_id = $2
`
type SetAgentMcpServerEnabledParams struct {
AgentID pgtype.UUID `json:"agent_id"`
ServerID pgtype.UUID `json:"server_id"`
Enabled bool `json:"enabled"`
}
func (q *Queries) SetAgentMcpServerEnabled(ctx context.Context, arg SetAgentMcpServerEnabledParams) (int64, error) {
result, err := q.db.Exec(ctx, setAgentMcpServerEnabled, arg.AgentID, arg.ServerID, arg.Enabled)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateWorkspaceMcpServer = `-- name: UpdateWorkspaceMcpServer :one
UPDATE workspace_mcp_server SET
name = COALESCE($3, name),
config = COALESCE($4, config),
updated_at = now()
WHERE id = $1 AND workspace_id = $2
RETURNING id, workspace_id, name, config, created_by, created_at, updated_at
`
type UpdateWorkspaceMcpServerParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
Name pgtype.Text `json:"name"`
Config []byte `json:"config"`
}
// Full replace of one entry. The name can change here (unlike the previous
// document model) because bindings key off the id, so a rename cannot orphan
// the agents that use it.
func (q *Queries) UpdateWorkspaceMcpServer(ctx context.Context, arg UpdateWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
row := q.db.QueryRow(ctx, updateWorkspaceMcpServer,
arg.ID,
arg.WorkspaceID,
arg.Name,
arg.Config,
)
var i WorkspaceMcpServer
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Name,
&i.Config,
&i.CreatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}