Files
multica/server/pkg/db/generated/project.sql.go
Jiayuan Zhang a3fe6d91dd MUL-5150: add project context to Chat (#5765)
* feat(chat): add project context

Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): resolve MUL-5150 review blockers

- Renumber project-context migrations to unique prefixes after current main:
  206_chat_session_project -> 212 (column), 207_chat_session_project_index ->
  213 (concurrent index). 206/207 collided with 206_agent_disabled_runtime_skills
  and main's 207-211 client_usage_daily set.
- Add the 4 missing chat input.project_context keys to ja/ko locales so the
  locale parity test passes (en/zh-Hans already had them).
- Lock the project-context control while a send is in flight (isSubmitting),
  not just while the agent is running. A brand-new chat creates its session
  lazily during send bound to the project at click time; switching project
  mid-send would create the session against the stale project and clear the
  editor as if the send landed on the new selection. Add a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): complete project context handling

* fix(chat): pin fresh chat to open session's agent on project switch

Switching an existing session to a different project opens a fresh chat but
only cleared the active session, dropping selection back to the stored
`selectedAgentId`. When that preference was stale (open session belongs to
agent B while the persisted pick is still agent A), the lazily-created session
and its first send bound to the wrong agent (agent A).

Extract the project-switch decision into a shared `planProjectContextChange`
pure helper in use-chat-controller.ts and route both chat surfaces (the chat
tab controller and the floating ChatWindow) through it, so the fresh chat is
pinned to the open session's agent and the rule cannot drift between the two
copies. Add a dual-entry regression test (pure-fn guard + controller
integration) covering the stale selectedAgentId case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* chore(ci): re-trigger required checks on latest head

The prior push updated the branch ref but GitHub did not emit a pull_request
synchronize for it (PR head-sync lag), so CI/Mobile Verify never ran on the
commit carrying the stale-agent project-switch fix. Empty commit to force a
fresh synchronize on a head that includes it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): renumber project migrations to 213/214 after main added 212

Current main added 212_agent_service_tier; the PR's 212/213 chat migrations
collided with it on the merge ref, failing TestMigrationNumericPrefixesStay
UniqueAfterLegacySet. Merge current main and move the chat column migration to
213 and the concurrent index migration to 214 (column before index preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

* fix(chat): lock ProjectPicker clear control during send (keyboard path)

The send-pending lock only put pointer-events-none on the wrapper, which
blocks the mouse but leaves ProjectPicker's inline clear button in the tab
order — a keyboard user could Tab to "Remove from project" and press Enter
mid-send, detaching the project after the lazily-created session already went
out with the old one (reopens the mid-send retarget path via keyboard).

Add an explicit `disabled` capability to the shared ProjectPicker that locks
the trigger, the menu (forced closed), and the inline clear button (disabled +
out of the tab order). Defaults to false, so issue/create/autopilot callers
keep their hover/keyboard clear. ChatInput passes disabled while the project
selection is locked.

Tests: real-ProjectPicker regression (keyboard activation of the clear control
is inert when disabled; still works when enabled) + ChatInput wiring assertion
that the picker is disabled mid-send.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Walt <walt@multica.ai>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com>
Co-authored-by: NevilleQingNY <nevilleqing@gmail.com>
2026-07-24 11:30:27 +08:00

335 lines
8.6 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: project.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countIssuesByProject = `-- name: CountIssuesByProject :one
SELECT count(*) FROM issue
WHERE project_id = $1
`
func (q *Queries) CountIssuesByProject(ctx context.Context, projectID pgtype.UUID) (int64, error) {
row := q.db.QueryRow(ctx, countIssuesByProject, projectID)
var count int64
err := row.Scan(&count)
return count, err
}
const createProject = `-- name: CreateProject :one
INSERT INTO project (
workspace_id, title, description, icon, status,
lead_type, lead_id, priority, start_date, due_date
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority, start_date, due_date
`
type CreateProjectParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
Status string `json:"status"`
LeadType pgtype.Text `json:"lead_type"`
LeadID pgtype.UUID `json:"lead_id"`
Priority string `json:"priority"`
StartDate pgtype.Date `json:"start_date"`
DueDate pgtype.Date `json:"due_date"`
}
func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error) {
row := q.db.QueryRow(ctx, createProject,
arg.WorkspaceID,
arg.Title,
arg.Description,
arg.Icon,
arg.Status,
arg.LeadType,
arg.LeadID,
arg.Priority,
arg.StartDate,
arg.DueDate,
)
var i Project
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Icon,
&i.Status,
&i.LeadType,
&i.LeadID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.StartDate,
&i.DueDate,
)
return i, err
}
const deleteProject = `-- name: DeleteProject :exec
DELETE FROM project WHERE id = $1 AND workspace_id = $2
`
type DeleteProjectParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Defense-in-depth: workspace_id is a SQL-layer tenant guard. See DeleteIssue.
func (q *Queries) DeleteProject(ctx context.Context, arg DeleteProjectParams) error {
_, err := q.db.Exec(ctx, deleteProject, arg.ID, arg.WorkspaceID)
return err
}
const getProject = `-- name: GetProject :one
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority, start_date, due_date FROM project
WHERE id = $1
`
func (q *Queries) GetProject(ctx context.Context, id pgtype.UUID) (Project, error) {
row := q.db.QueryRow(ctx, getProject, id)
var i Project
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Icon,
&i.Status,
&i.LeadType,
&i.LeadID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.StartDate,
&i.DueDate,
)
return i, err
}
const getProjectInWorkspace = `-- name: GetProjectInWorkspace :one
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority, start_date, due_date FROM project
WHERE id = $1 AND workspace_id = $2
`
type GetProjectInWorkspaceParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
func (q *Queries) GetProjectInWorkspace(ctx context.Context, arg GetProjectInWorkspaceParams) (Project, error) {
row := q.db.QueryRow(ctx, getProjectInWorkspace, arg.ID, arg.WorkspaceID)
var i Project
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Icon,
&i.Status,
&i.LeadType,
&i.LeadID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.StartDate,
&i.DueDate,
)
return i, err
}
const getProjectIssueStats = `-- name: GetProjectIssueStats :many
SELECT project_id,
count(*)::bigint AS total_count,
count(*) FILTER (WHERE status IN ('done', 'cancelled'))::bigint AS done_count
FROM issue
WHERE project_id = ANY($1::uuid[])
GROUP BY project_id
`
type GetProjectIssueStatsRow struct {
ProjectID pgtype.UUID `json:"project_id"`
TotalCount int64 `json:"total_count"`
DoneCount int64 `json:"done_count"`
}
func (q *Queries) GetProjectIssueStats(ctx context.Context, projectIds []pgtype.UUID) ([]GetProjectIssueStatsRow, error) {
rows, err := q.db.Query(ctx, getProjectIssueStats, projectIds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetProjectIssueStatsRow{}
for rows.Next() {
var i GetProjectIssueStatsRow
if err := rows.Scan(&i.ProjectID, &i.TotalCount, &i.DoneCount); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listProjects = `-- name: ListProjects :many
SELECT id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority, start_date, due_date FROM project
WHERE workspace_id = $1
AND ($2::text IS NULL OR status = $2)
AND ($3::text IS NULL OR priority = $3)
ORDER BY created_at DESC
`
type ListProjectsParams struct {
WorkspaceID pgtype.UUID `json:"workspace_id"`
Status pgtype.Text `json:"status"`
Priority pgtype.Text `json:"priority"`
}
func (q *Queries) ListProjects(ctx context.Context, arg ListProjectsParams) ([]Project, error) {
rows, err := q.db.Query(ctx, listProjects, arg.WorkspaceID, arg.Status, arg.Priority)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Project{}
for rows.Next() {
var i Project
if err := rows.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Icon,
&i.Status,
&i.LeadType,
&i.LeadID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.StartDate,
&i.DueDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const lockProjectForChatSessionCreate = `-- name: LockProjectForChatSessionCreate :one
SELECT id FROM project
WHERE id = $1 AND workspace_id = $2
FOR KEY SHARE
`
type LockProjectForChatSessionCreateParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Conflicts with project deletion so a chat session cannot commit a soft
// project reference after the delete transaction has swept existing sessions.
func (q *Queries) LockProjectForChatSessionCreate(ctx context.Context, arg LockProjectForChatSessionCreateParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, lockProjectForChatSessionCreate, arg.ID, arg.WorkspaceID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const lockProjectForDelete = `-- name: LockProjectForDelete :one
SELECT id FROM project
WHERE id = $1 AND workspace_id = $2
FOR UPDATE
`
type LockProjectForDeleteParams struct {
ID pgtype.UUID `json:"id"`
WorkspaceID pgtype.UUID `json:"workspace_id"`
}
// Serializes project deletion with chat-session creation. The handler locks,
// clears every soft chat reference, and deletes the project in one transaction.
func (q *Queries) LockProjectForDelete(ctx context.Context, arg LockProjectForDeleteParams) (pgtype.UUID, error) {
row := q.db.QueryRow(ctx, lockProjectForDelete, arg.ID, arg.WorkspaceID)
var id pgtype.UUID
err := row.Scan(&id)
return id, err
}
const updateProject = `-- name: UpdateProject :one
UPDATE project SET
title = COALESCE($2, title),
description = $3,
icon = $4,
status = COALESCE($5, status),
priority = COALESCE($6, priority),
lead_type = $7,
lead_id = $8,
start_date = $9,
due_date = $10,
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, title, description, icon, status, lead_type, lead_id, created_at, updated_at, priority, start_date, due_date
`
type UpdateProjectParams struct {
ID pgtype.UUID `json:"id"`
Title pgtype.Text `json:"title"`
Description pgtype.Text `json:"description"`
Icon pgtype.Text `json:"icon"`
Status pgtype.Text `json:"status"`
Priority pgtype.Text `json:"priority"`
LeadType pgtype.Text `json:"lead_type"`
LeadID pgtype.UUID `json:"lead_id"`
StartDate pgtype.Date `json:"start_date"`
DueDate pgtype.Date `json:"due_date"`
}
func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (Project, error) {
row := q.db.QueryRow(ctx, updateProject,
arg.ID,
arg.Title,
arg.Description,
arg.Icon,
arg.Status,
arg.Priority,
arg.LeadType,
arg.LeadID,
arg.StartDate,
arg.DueDate,
)
var i Project
err := row.Scan(
&i.ID,
&i.WorkspaceID,
&i.Title,
&i.Description,
&i.Icon,
&i.Status,
&i.LeadType,
&i.LeadID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Priority,
&i.StartDate,
&i.DueDate,
)
return i, err
}