refactor: remove repository field from issues

The repository JSONB column on the issue table is unused. This removes
it end-to-end: migration to drop the column, sqlc queries, Go handler/
service/daemon/protocol structs, TypeScript types, and the
RepositoryEditor UI component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jiayuan Zhang 2026-03-24 18:12:06 +08:00
parent e3ea7bd02c
commit e86768823e
18 changed files with 18 additions and 242 deletions

View file

@ -106,7 +106,6 @@ const mockIssue: Issue = {
parent_issue_id: null,
acceptance_criteria: [],
context_refs: [],
repository: null,
position: 0,
due_date: "2026-06-01T00:00:00Z",
created_at: "2026-01-15T00:00:00Z",

View file

@ -5,7 +5,6 @@ import Link from "next/link";
import { useRouter } from "next/navigation";
import {
ChevronRight,
GitBranch,
Link2,
Pencil,
Send,
@ -270,110 +269,6 @@ function ContextRefsEditor({
);
}
// ---------------------------------------------------------------------------
// Repository Editor
// ---------------------------------------------------------------------------
function RepositoryEditor({
repository,
onUpdate,
}: {
repository: { url: string; branch?: string; path?: string } | null;
onUpdate: (updates: Partial<UpdateIssueRequest>) => void;
}) {
const [open, setOpen] = useState(false);
const [url, setUrl] = useState("");
const [branch, setBranch] = useState("");
const [path, setPath] = useState("");
const handleOpen = (v: boolean) => {
if (v) {
setUrl(repository?.url ?? "");
setBranch(repository?.branch ?? "");
setPath(repository?.path ?? "");
}
setOpen(v);
};
const save = () => {
if (!url.trim()) {
onUpdate({ repository: null });
} else {
onUpdate({
repository: {
url: url.trim(),
branch: branch.trim() || undefined,
path: path.trim() || undefined,
},
});
}
setOpen(false);
};
const clear = () => {
onUpdate({ repository: null });
setOpen(false);
};
return (
<Popover open={open} onOpenChange={handleOpen}>
<PopoverTrigger className="flex items-center gap-1.5 cursor-pointer rounded px-1 -mx-1 hover:bg-accent/30 transition-colors">
{repository ? (
<>
<GitBranch className="h-3 w-3 shrink-0 text-muted-foreground" />
<span className="truncate text-xs">{repository.branch ?? "main"}</span>
</>
) : (
<span className="text-muted-foreground text-xs">None</span>
)}
</PopoverTrigger>
<PopoverContent align="end" className="w-auto min-w-48 p-3 space-y-2.5">
<div className="text-xs font-medium">Repository</div>
<div className="space-y-2">
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://github.com/org/repo"
className="text-xs"
autoFocus
/>
<Input
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="Branch"
className="text-xs"
/>
<Input
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="Path"
className="text-xs"
/>
</div>
<div className="flex items-center justify-between pt-1">
{repository && (
<Button
variant="ghost"
size="xs"
onClick={clear}
className="text-muted-foreground hover:text-destructive"
>
Remove
</Button>
)}
<Button
size="xs"
onClick={save}
className="ml-auto"
>
Save
</Button>
</div>
</PopoverContent>
</Popover>
);
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
@ -712,10 +607,6 @@ export default function IssueDetailPage({
<DueDatePicker dueDate={issue.due_date} onUpdate={handleUpdateField} />
</PropRow>
<PropRow label="Repository">
<RepositoryEditor repository={issue.repository} onUpdate={handleUpdateField} />
</PropRow>
<PropRow label="Created by">
<ActorAvatar
actorType={issue.creator_type}

View file

@ -61,7 +61,6 @@ const issueDefaults = {
parent_issue_id: null,
acceptance_criteria: [],
context_refs: [],
repository: null,
position: 0,
};

View file

@ -12,7 +12,6 @@ export interface CreateIssueRequest {
parent_issue_id?: string;
acceptance_criteria?: string[];
context_refs?: string[];
repository?: { url: string; branch?: string; path?: string };
}
export interface UpdateIssueRequest {
@ -26,7 +25,6 @@ export interface UpdateIssueRequest {
due_date?: string | null;
acceptance_criteria?: string[];
context_refs?: string[];
repository?: { url: string; branch?: string; path?: string } | null;
}
export interface ListIssuesParams {

View file

@ -25,11 +25,6 @@ export interface Issue {
parent_issue_id: string | null;
acceptance_criteria: string[];
context_refs: string[];
repository: {
url: string;
branch?: string;
path?: string;
} | null;
position: number;
due_date: string | null;
created_at: string;

View file

@ -267,10 +267,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task) (TaskResult, error) {
return TaskResult{}, fmt.Errorf("no agent configured for provider %q", provider)
}
workdir, err := ResolveTaskWorkdir(d.cfg.ReposRoot, task.Context.Issue.Repository)
if err != nil {
return TaskResult{}, err
}
workdir := ResolveTaskWorkdir(d.cfg.ReposRoot)
prompt := BuildPrompt(task, workdir)

View file

@ -2,8 +2,6 @@ package daemon
import (
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
@ -20,21 +18,13 @@ func TestNormalizeServerBaseURL(t *testing.T) {
}
}
func TestResolveTaskWorkdirUsesRepoPathWhenPresent(t *testing.T) {
func TestResolveTaskWorkdirReturnsRoot(t *testing.T) {
t.Parallel()
root := t.TempDir()
repoPath := filepath.Join(root, "repo")
if err := os.Mkdir(repoPath, 0o755); err != nil {
t.Fatalf("mkdir repo: %v", err)
}
got, err := ResolveTaskWorkdir(root, &RepoRef{Path: "repo"})
if err != nil {
t.Fatalf("ResolveTaskWorkdir returned error: %v", err)
}
if got != repoPath {
t.Fatalf("expected %s, got %s", repoPath, got)
got := ResolveTaskWorkdir(root)
if got != root {
t.Fatalf("expected %s, got %s", root, got)
}
}

View file

@ -2,8 +2,6 @@ package daemon
import (
"fmt"
"os"
"path/filepath"
"strings"
)
@ -41,20 +39,6 @@ func BuildPrompt(task Task, workdir string) string {
b.WriteString("\n")
}
if repo := task.Context.Issue.Repository; repo != nil {
b.WriteString("Repository context:\n")
if repo.URL != "" {
fmt.Fprintf(&b, "- url: %s\n", repo.URL)
}
if repo.Branch != "" {
fmt.Fprintf(&b, "- branch: %s\n", repo.Branch)
}
if repo.Path != "" {
fmt.Fprintf(&b, "- path: %s\n", repo.Path)
}
b.WriteString("\n")
}
if task.Context.WorkspaceContext != "" {
b.WriteString("Workspace context:\n")
b.WriteString(task.Context.WorkspaceContext)
@ -76,24 +60,6 @@ func BuildPrompt(task Task, workdir string) string {
}
// ResolveTaskWorkdir determines the working directory for a task.
func ResolveTaskWorkdir(reposRoot string, repo *RepoRef) (string, error) {
base := reposRoot
if repo == nil || strings.TrimSpace(repo.Path) == "" {
return base, nil
}
path := strings.TrimSpace(repo.Path)
if !filepath.IsAbs(path) {
path = filepath.Join(base, path)
}
path = filepath.Clean(path)
info, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("repository path not found: %s", path)
}
if !info.IsDir() {
return "", fmt.Errorf("repository path is not a directory: %s", path)
}
return path, nil
func ResolveTaskWorkdir(reposRoot string) string {
return reposRoot
}

View file

@ -58,7 +58,6 @@ type IssueContext struct {
Description string `json:"description"`
AcceptanceCriteria []string `json:"acceptance_criteria"`
ContextRefs []string `json:"context_refs"`
Repository *RepoRef `json:"repository"`
}
// AgentContext holds agent details for task execution.
@ -76,13 +75,6 @@ type RuntimeContext struct {
DeviceInfo string `json:"device_info"`
}
// RepoRef points to a repository for an issue.
type RepoRef struct {
URL string `json:"url"`
Branch string `json:"branch"`
Path string `json:"path"`
}
// TaskResult is the outcome of executing a task.
type TaskResult struct {
Status string `json:"status"`

View file

@ -28,7 +28,6 @@ type IssueResponse struct {
ParentIssueID *string `json:"parent_issue_id"`
AcceptanceCriteria []any `json:"acceptance_criteria"`
ContextRefs []any `json:"context_refs"`
Repository any `json:"repository"`
Position float64 `json:"position"`
DueDate *string `json:"due_date"`
CreatedAt string `json:"created_at"`
@ -58,11 +57,6 @@ func issueToResponse(i db.Issue) IssueResponse {
cr = []any{}
}
var repo any
if i.Repository != nil {
json.Unmarshal(i.Repository, &repo)
}
return IssueResponse{
ID: uuidToString(i.ID),
WorkspaceID: uuidToString(i.WorkspaceID),
@ -77,7 +71,6 @@ func issueToResponse(i db.Issue) IssueResponse {
ParentIssueID: uuidToPtr(i.ParentIssueID),
AcceptanceCriteria: ac,
ContextRefs: cr,
Repository: repo,
Position: i.Position,
DueDate: timestampToPtr(i.DueDate),
CreatedAt: timestampToString(i.CreatedAt),
@ -163,7 +156,6 @@ type CreateIssueRequest struct {
ParentIssueID *string `json:"parent_issue_id"`
AcceptanceCriteria []any `json:"acceptance_criteria"`
ContextRefs []any `json:"context_refs"`
Repository any `json:"repository"`
}
func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
@ -206,11 +198,6 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
if req.ContextRefs == nil {
cr = []byte("[]")
}
var repo []byte
if req.Repository != nil {
repo, _ = json.Marshal(req.Repository)
}
var assigneeType pgtype.Text
var assigneeID pgtype.UUID
if req.AssigneeType != nil {
@ -238,7 +225,6 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
ParentIssueID: parentIssueID,
AcceptanceCriteria: ac,
ContextRefs: cr,
Repository: repo,
Position: 0,
})
if err != nil {
@ -285,7 +271,6 @@ type UpdateIssueRequest struct {
DueDate *string `json:"due_date"`
AcceptanceCriteria *[]any `json:"acceptance_criteria"`
ContextRefs *[]any `json:"context_refs"`
Repository *any `json:"repository"`
}
func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
@ -344,10 +329,6 @@ func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {
cr, _ := json.Marshal(*req.ContextRefs)
params.ContextRefs = cr
}
if req.Repository != nil {
repo, _ := json.Marshal(*req.Repository)
params.Repository = repo
}
// Nullable fields — only override when explicitly present in JSON
if _, ok := rawFields["assignee_type"]; ok {

View file

@ -266,12 +266,6 @@ func buildContextSnapshot(issue db.Issue, agent db.Agent, runtime db.AgentRuntim
if issue.ContextRefs != nil {
json.Unmarshal(issue.ContextRefs, &cr)
}
var repo *protocol.RepoRef
if issue.Repository != nil {
repo = &protocol.RepoRef{}
json.Unmarshal(issue.Repository, repo)
}
var tools any
if agent.Tools != nil {
json.Unmarshal(agent.Tools, &tools)
@ -288,7 +282,6 @@ func buildContextSnapshot(issue db.Issue, agent db.Agent, runtime db.AgentRuntim
"description": issue.Description.String,
"acceptance_criteria": ac,
"context_refs": cr,
"repository": repo,
},
"agent": map[string]any{
"id": util.UUIDToString(agent.ID),
@ -418,11 +411,6 @@ func issueToMap(issue db.Issue) map[string]any {
cr = []any{}
}
var repo any
if issue.Repository != nil {
json.Unmarshal(issue.Repository, &repo)
}
return map[string]any{
"id": util.UUIDToString(issue.ID),
"workspace_id": util.UUIDToString(issue.WorkspaceID),
@ -437,7 +425,6 @@ func issueToMap(issue db.Issue) map[string]any {
"parent_issue_id": util.UUIDToPtr(issue.ParentIssueID),
"acceptance_criteria": ac,
"context_refs": cr,
"repository": repo,
"position": issue.Position,
"due_date": util.TimestampToPtr(issue.DueDate),
"created_at": util.TimestampToString(issue.CreatedAt),

View file

@ -65,7 +65,6 @@ CREATE TABLE issue (
parent_issue_id UUID REFERENCES issue(id) ON DELETE SET NULL,
acceptance_criteria JSONB NOT NULL DEFAULT '[]',
context_refs JSONB NOT NULL DEFAULT '[]',
repository JSONB,
position FLOAT NOT NULL DEFAULT 0,
due_date TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),

View file

@ -0,0 +1 @@
ALTER TABLE issue ADD COLUMN repository JSONB;

View file

@ -0,0 +1 @@
ALTER TABLE issue DROP COLUMN IF EXISTS repository;

View file

@ -16,10 +16,10 @@ INSERT INTO issue (
workspace_id, title, description, status, priority,
assignee_type, assignee_id, creator_type, creator_id,
parent_issue_id, acceptance_criteria, context_refs,
repository, position
position
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, repository, position, due_date, created_at, updated_at
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
`
type CreateIssueParams struct {
@ -35,7 +35,6 @@ type CreateIssueParams struct {
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
AcceptanceCriteria []byte `json:"acceptance_criteria"`
ContextRefs []byte `json:"context_refs"`
Repository []byte `json:"repository"`
Position float64 `json:"position"`
}
@ -53,7 +52,6 @@ func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue
arg.ParentIssueID,
arg.AcceptanceCriteria,
arg.ContextRefs,
arg.Repository,
arg.Position,
)
var i Issue
@ -71,7 +69,6 @@ func (q *Queries) CreateIssue(ctx context.Context, arg CreateIssueParams) (Issue
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Repository,
&i.Position,
&i.DueDate,
&i.CreatedAt,
@ -90,7 +87,7 @@ func (q *Queries) DeleteIssue(ctx context.Context, id pgtype.UUID) error {
}
const getIssue = `-- name: GetIssue :one
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, repository, position, due_date, created_at, updated_at FROM issue
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at FROM issue
WHERE id = $1
`
@ -111,7 +108,6 @@ func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) {
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Repository,
&i.Position,
&i.DueDate,
&i.CreatedAt,
@ -121,7 +117,7 @@ func (q *Queries) GetIssue(ctx context.Context, id pgtype.UUID) (Issue, error) {
}
const listIssues = `-- name: ListIssues :many
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, repository, position, due_date, created_at, updated_at FROM issue
SELECT id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at FROM issue
WHERE workspace_id = $1
AND ($4::text IS NULL OR status = $4)
AND ($5::text IS NULL OR priority = $5)
@ -169,7 +165,6 @@ func (q *Queries) ListIssues(ctx context.Context, arg ListIssuesParams) ([]Issue
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Repository,
&i.Position,
&i.DueDate,
&i.CreatedAt,
@ -197,10 +192,9 @@ UPDATE issue SET
due_date = $9,
acceptance_criteria = COALESCE($10, acceptance_criteria),
context_refs = COALESCE($11, context_refs),
repository = COALESCE($12, repository),
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, repository, position, due_date, created_at, updated_at
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
`
type UpdateIssueParams struct {
@ -215,7 +209,6 @@ type UpdateIssueParams struct {
DueDate pgtype.Timestamptz `json:"due_date"`
AcceptanceCriteria []byte `json:"acceptance_criteria"`
ContextRefs []byte `json:"context_refs"`
Repository []byte `json:"repository"`
}
func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue, error) {
@ -231,7 +224,6 @@ func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue
arg.DueDate,
arg.AcceptanceCriteria,
arg.ContextRefs,
arg.Repository,
)
var i Issue
err := row.Scan(
@ -248,7 +240,6 @@ func (q *Queries) UpdateIssue(ctx context.Context, arg UpdateIssueParams) (Issue
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Repository,
&i.Position,
&i.DueDate,
&i.CreatedAt,
@ -262,7 +253,7 @@ UPDATE issue SET
status = $2,
updated_at = now()
WHERE id = $1
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, repository, position, due_date, created_at, updated_at
RETURNING id, workspace_id, title, description, status, priority, assignee_type, assignee_id, creator_type, creator_id, parent_issue_id, acceptance_criteria, context_refs, position, due_date, created_at, updated_at
`
type UpdateIssueStatusParams struct {
@ -287,7 +278,6 @@ func (q *Queries) UpdateIssueStatus(ctx context.Context, arg UpdateIssueStatusPa
&i.ParentIssueID,
&i.AcceptanceCriteria,
&i.ContextRefs,
&i.Repository,
&i.Position,
&i.DueDate,
&i.CreatedAt,

View file

@ -139,7 +139,6 @@ type Issue struct {
ParentIssueID pgtype.UUID `json:"parent_issue_id"`
AcceptanceCriteria []byte `json:"acceptance_criteria"`
ContextRefs []byte `json:"context_refs"`
Repository []byte `json:"repository"`
Position float64 `json:"position"`
DueDate pgtype.Timestamptz `json:"due_date"`
CreatedAt pgtype.Timestamptz `json:"created_at"`

View file

@ -16,9 +16,9 @@ INSERT INTO issue (
workspace_id, title, description, status, priority,
assignee_type, assignee_id, creator_type, creator_id,
parent_issue_id, acceptance_criteria, context_refs,
repository, position
position
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING *;
-- name: UpdateIssue :one
@ -33,7 +33,6 @@ UPDATE issue SET
due_date = sqlc.narg('due_date'),
acceptance_criteria = COALESCE(sqlc.narg('acceptance_criteria'), acceptance_criteria),
context_refs = COALESCE(sqlc.narg('context_refs'), context_refs),
repository = COALESCE(sqlc.narg('repository'), repository),
updated_at = now()
WHERE id = $1
RETURNING *;

View file

@ -16,14 +16,6 @@ type TaskDispatchPayload struct {
Description string `json:"description"`
AcceptanceCriteria []string `json:"acceptance_criteria"`
ContextRefs []string `json:"context_refs"`
Repository *RepoRef `json:"repository,omitempty"`
}
// RepoRef points to a code repository.
type RepoRef struct {
URL string `json:"url"`
Branch string `json:"branch,omitempty"`
Path string `json:"path,omitempty"`
}
// TaskProgressPayload is sent from daemon to server during task execution.