Skip to content

feat(repositories): リポジトリ一覧表示と別名編集UI #644

@Kewton

Description

@Kewton

背景

Issue #642 で DB/API レベルのリポジトリ別名(display_name)機能を追加し、Sessions・Worktree 詳細などの表示側には反映済みです。しかし現状 Repositories 画面(/repositories)は「Add Repository」フォームと「Sync All」ボタンのみで、登録済みリポジトリの 一覧表示そのものが存在しない ため、別名を設定するには PUT /api/repositories/[id] を直接 curl で叩くしか手段がありません。

UI から登録済みリポジトリを確認・別名編集できるようにします。

現状の課題

  • /repositories 画面(src/app/repositories/page.tsx)は RepositoryManager のみを描画しているが、RepositoryManager は登録フォーム+Sync ボタンしか持たない
  • リポジトリ一覧を返す GET API が存在しない(src/app/api/repositories/route.tsDELETE のみ)
  • repositoryApisrc/lib/api-client.ts)に list 取得メソッドが存在しない
  • Issue feat: リポジトリに別名(エイリアス)を設定して表示名を変更可能にする #642 で追加した display_name を UI から編集する導線がない

要件

機能要件

  1. リポジトリ一覧表示

    • /repositories 画面に登録済みリポジトリの一覧を表示する
    • 各行に以下を表示:
      • リポジトリ名(name
      • 別名(displayName、未設定時は空欄 or プレースホルダ)
      • 絶対パス(path
      • 紐づく worktree 数(worktreeCount
      • 有効/無効状態(enabled
    • RepositoryListRepositoryManager の上部に配置する(一覧ファーストの UX、S3-009)
    • 無効化リポジトリの扱い(方針明確化・S1-003)
  2. 別名の編集

    • 一覧の各行で別名をインライン編集できる(編集ボタン → インプット → 保存/キャンセル、もしくはクリックで直接編集)
    • 入力中は楽観更新ではなく、保存成功まで既存値を維持する
    • 保存は既存の PUT /api/repositories/[id] を呼ぶ({ displayName: string }、空文字で解除)
    • バリデーション:
      • 最大 100 文字(共有定数 MAX_DISPLAY_NAME_LENGTHsrc/config/repository-config.ts に定義し、API ルートとフロント両方から import)
      • trim 後の空文字は「別名クリア」として扱う
    • 保存成功時はトースト or 成功メッセージを表示し、一覧を再取得 or 楽観更新する
    • 失敗時はエラーメッセージを表示し、入力値を保持する

非機能要件

  • 一覧取得は画面マウント時に 1 回、編集後に差分再取得 or 該当行のみ更新
  • ポーリング方針(S3-008): 本 Issue では Option A(ポーリングなし、Sync/Add/Delete イベント時のみ refresh) を採用する
    • 定期ポーリングは実装しない(負荷増を避けるため)
    • RepositoryList の再取得トリガーは「RepositoryManager による Add/Sync 完了」または「将来追加される Delete」のイベント時のみ
    • 他タブ・他ユーザーによる変更の即時反映が必要になった場合は別 Issue で WebSocket 購読(repository_deleted 等)を追加する
    • 想定件数: 20〜100 件程度。仮想スクロールは不要
  • リポジトリ数が多い場合でも再レンダリングが過剰にならないよう React.memo / useCallback を適切に使用
  • 既存の RepositoryManager コンポーネントとは責務を分離し、新規コンポーネント(例: RepositoryList.tsx)として追加する
  • ダークモード対応
  • キーボード操作(Enter で保存、Esc でキャンセル)
  • 認証・IP 制限(S3-007): GET /api/repositories は既存の認証ミドルウェア(src/middleware.ts)を流用するため、本 Issue での追加変更は不要。既存の DELETE /api/repositories / PUT /api/repositories/[id] / GET /api/repositories/excluded と同等の挙動となる

実装方針(たたき台)

1. API 追加

新規: GET /api/repositoriessrc/app/api/repositories/route.ts に追記)

  • getAllRepositories(db)(既存・src/lib/db/db-repository.ts:290)を呼び出し、worktreeCount を別途 worktrees テーブルへの集計クエリで付与して返す
  • 既存 getRepositories()(worktree-db.ts)を流用しない理由(S1-002)
    • src/lib/db/worktree-db.ts:171 の既存 getRepositories() は worktrees テーブル起点の LEFT JOIN + GROUP BY クエリで構成されており、
      • worktree 0 件のリポジトリを返さない
      • id / enabled フィールドを返さない
        という制約があるため、本 Issue の要件(全リポジトリ表示・enabled バッジ)を満たせない
    • そのため getAllRepositories(db)(repositories テーブル直接クエリ、src/lib/db/db-repository.ts:290-300、WHERE 句なしなので enabled=0 も含む全件を返す)を使用し、
    • worktreeCount は別途 worktrees テーブルへの集計クエリで付与する
  • worktreeCount 集計クエリの正しい形(S3-001 / Must Fix)
    • 重要: worktrees テーブルには repository_id カラムが 存在しないsrc/lib/db/migrations/v11-v15-feature-additions.ts:195-230 参照)。リポジトリとの紐付けは repository_path 文字列カラムで行われている(src/lib/db/worktree-db.ts:91,99,179-186)。repository_id FK は clone_jobs テーブルにのみ存在する
    • したがって集計クエリは repository_path ベースとする:
      SELECT repository_path, COUNT(*) AS count
      FROM worktrees
      WHERE repository_path IS NOT NULL
      GROUP BY repository_path
    • フロントマージ手順:
      1. getAllRepositories(db) で Repository 全件を取得
      2. 上記クエリ結果を Map<string, number>(key = repository_path)に変換
      3. 各 Repository について map.get(repo.path) ?? 0worktreeCount を付与
    • 推奨(N+1 安全な代替案): DB 層に新規ヘルパー getAllRepositoriesWithWorktreeCount(db) を追加する
      SELECT r.*, (SELECT COUNT(*) FROM worktrees w WHERE w.repository_path = r.path) AS worktree_count
      FROM repositories r
      ORDER BY r.name ASC
      こちらを採用する場合は既存 getAllRepositories(db)変更せず温存 する(S3-005、後述)
  • 既存 getAllRepositories(db) のシグネチャは変更しない(S3-005 / 後方互換)
    • getAllRepositories(db) は既に src/app/api/repositories/sync/route.ts:26src/lib/daily-summary-generator.ts:179 の 2 箇所で呼ばれている
    • 既存シグネチャを変えると上記 2 箇所への影響(型変更、不要な worktreeCount フィールド追加)が発生するため、既存関数は変更しない
    • 本 Issue で追加するロジックは次のいずれかに限定する:
      • (a) API ルート内で getAllRepositories(db) を呼び出した後、worktree 集計クエリ結果の Map を別途取得してマージする
      • (b) DB 層に新規ヘルパー getAllRepositoriesWithWorktreeCount(db) を追加する(既存 getAllRepositories は温存)
    • src/app/api/repositories/sync/route.ts:26src/lib/daily-summary-generator.ts:179 の既存呼び出し箇所は 変更しない
  • 無効化リポジトリの返却方針(S1-003)
    • getAllRepositories(db) は WHERE 句なしで全件返すため、enabled=0 のリポジトリもそのまま含めて返す
    • フロント側で enabled フィールドを参照して無効バッジを描画する
    • /api/repositories/excluded は呼ばない(excluded API は別用途のため)
  • 既存 GET /api/worktrees との棲み分け(S3-002 / Must Fix)
    • 既存 GET /api/worktreessrc/app/api/worktrees/route.ts:97)は getRepositories(db)src/lib/db/worktree-db.ts:171)経由で { path, name, displayName, worktreeCount } 形式の worktree 起点のリポジトリサマリー を返す
    • 新規 GET /api/repositoriesgetAllRepositories(db) ベースで { id, name, displayName, path, enabled, worktreeCount } 形式の repositories テーブル起点の全件 を返す
    • Single Source of Truth 方針: Repositories 画面は repositories テーブルを真の値として扱う。既存の Sessions 画面・Worktree 詳細画面・サイドバーなどで使われる WorktreesResponse.repositories(worktree 起点)の表示は 本 Issue では据え置き(既存仕様のまま)
    • 別名更新後の既存画面への反映経路(最小実装方針):
      • 本 Issue では リロード後反映を許容する(Option C)
      • 即時反映は実装せず、ユーザーが Sessions 画面等を再訪問/リロードした時点で /api/worktrees が再フェッチされて最新の名前が反映される
      • 受け入れ条件の「保存した別名が Sessions 画面・Worktree 詳細画面など既存の表示箇所に反映される」は リロード後反映 として判定する
      • 将来の拡張案(本 Issue では実装しない):
        • Option A: 保存後にフロント側で worktreeApi.getAll()/api/worktrees)のキャッシュも無効化・再取得する(WorktreesCacheProvider 経由)
        • Option B: WebSocket で repository_display_name_updated イベントをブロードキャストし、購読側で更新する
    • リポジトリディレクトリをリネーム後に worktrees が古い名前で残る場合の表示不整合(getRepositories() が返す name と getAllRepositories() が返す name の食い違い)は 本 Issue のスコープ外とし、Sync All で worktrees テーブルを再同期することで解消される既存動作に委ねる
  • レスポンス例:
    {
      "success": true,
      "repositories": [
        { "id": "...", "name": "...", "displayName": "My Project", "path": "/abs/path", "enabled": true, "worktreeCount": 3 }
      ]
    }

既存: PUT /api/repositories/[id](Issue #642 で追加済み)をそのまま利用

2. api-client 追加

src/lib/api-client.tsrepositoryApi に下記を追加:

// 一覧取得用の型(GET /api/repositories のレスポンス行)
// フロント専用のシリアライズ型。src/types/models.ts の Repository 型はそのまま維持する
// createdAt / updatedAt / cloneSource / isEnvManaged は本 Issue の機能に不要なため含めない(将来拡張は別 Issue)
export type RepositoryListItem = {
  id: string;
  name: string;
  displayName: string | null;
  path: string;
  enabled: boolean;
  worktreeCount: number;
};

// PUT /api/repositories/[id] は worktreeCount を返さないため専用の戻り値型を使用(S1-001)
export type UpdateRepositoryDisplayNameResponse = {
  success: boolean;
  repository: Omit<RepositoryListItem, 'worktreeCount'>;
};

async list(): Promise<{ success: boolean; repositories: RepositoryListItem[] }> {
  return fetchApi('/api/repositories');
},

async updateDisplayName(id: string, displayName: string | null): Promise<UpdateRepositoryDisplayNameResponse> {
  return fetchApi(`/api/repositories/${id}`, {
    method: 'PUT',
    body: JSON.stringify({ displayName }),
  });
},

レスポンス型不整合への対応(S1-001)

  • PUT /api/repositories/[id]src/app/api/repositories/[id]/route.ts:63-73)は { id, name, displayName, path, enabled } のみを返し、worktreeCount を含まない
  • そのため updateDisplayName の戻り値型は RepositoryListItem をそのまま使わず、Omit<RepositoryListItem, 'worktreeCount'> を型として分離する(上記 UpdateRepositoryDisplayNameResponse 参照)
  • フロント側は保存後に (a) 該当行のみ局所更新(worktreeCount は既存値をそのまま保持)または (b) 一覧全体を再フェッチ、のいずれかで整合性を取る

3. UI コンポーネント追加

新規: src/components/repository/RepositoryList.tsx

  • 画面マウント時に repositoryApi.list() を呼び出し、state に保持
  • 各行で別名のインライン編集(編集モード切替 → 保存/キャンセル)
  • 保存後は該当行のみ楽観更新 or 再フェッチ
  • 各行で enabled が false のリポジトリには無効バッジを描画
  • refreshKey prop(number)と onChanged prop(() => void)を受け取り、親からの再取得トリガーに対応する

更新: src/app/repositories/page.tsx

  • RepositoryListRepositoryManager上部 に配置する(一覧ファースト、S3-009)
  • 新規追加 or Sync 完了後に RepositoryList の再取得をトリガーする
  • refresh 連携パターン(S3-004 / 具体実装): page.tsxrefreshKey state を持ち、RepositoryManageronRepositoryAdded でカウントアップして RepositoryList に渡すバケツリレー方式を採用する:
    // src/app/repositories/page.tsx
    'use client';
    import { useCallback, useState } from 'react';
    
    export default function RepositoriesPage() {
      const [refreshKey, setRefreshKey] = useState(0);
      const handleChanged = useCallback(() => setRefreshKey((k) => k + 1), []);
      return (
        <>
          <RepositoryList refreshKey={refreshKey} onChanged={handleChanged} />
          <RepositoryManager onRepositoryAdded={handleChanged} />
        </>
      );
    }
  • 削除導線は 本 Issue では追加しない(既存 WorktreeList 側に集約済み)
  • Context の導入は不要(2 コンポーネント間のシンプルなバケツリレーで十分)

4. 共有定数(S1-004 / S3-003)

新規ファイル: src/config/repository-config.ts

// リポジトリ設定関連の共有定数
export const MAX_DISPLAY_NAME_LENGTH = 100;
  • 既存 src/config/auth-config.ts / review-config.ts / timer-constants.ts の命名パターンを踏襲する
  • フロント(src/components/repository/RepositoryList.tsx)からも同ファイルから import する
  • API ルート・フロント両方で同じ定数を参照することで、バリデーション値の二重管理を防ぐ

既存 PUT ルートの定数置換手順(S3-003):

  • src/app/api/repositories/[id]/route.ts:13const MAX_DISPLAY_NAME_LENGTH = 100; を削除し、src/config/repository-config.ts からの import に切り替える
  • 既存の挙動・エラーメッセージ文言を変更しないこと: `displayName must be ${MAX_DISPLAY_NAME_LENGTH} characters or less` のテンプレート文字列はそのまま残し、定数参照だけを置き換える
  • Issue feat: リポジトリに別名(エイリアス)を設定して表示名を変更可能にする #642 時点で PUT /api/repositories/[id] の Integration テストは未カバー(unit の tests/unit/db-repository-display-name.test.ts のみ)のため、本 Issue で Integration テストを新規追加 する(バリデーション・400・404・成功パス。詳細は「テスト戦略」セクション参照)

テスト戦略(S3-006 / 新規追加)

本 Issue では以下の 3 ファイルを新規追加する:

  1. tests/integration/api-repositories-list.test.ts(新規 / GET /api/repositories 用)

    • 成功パス(200 + { success: true, repositories: [...] }
    • enabled=0 を含めた全件返却の確認
    • worktreeCount 集計の正しさ(worktree 0 件・複数件・リポジトリなしケース)
    • repository_path ベースの集計クエリが正しく動作することの検証(S3-001 の回帰防止)
    • Repository 型の返却フィールドが仕様通り(id, name, displayName, path, enabled, worktreeCount
    • NextRequest 経由のテスト(既存 tests/integration/api-repository-delete.test.ts を雛形にする)
  2. tests/unit/components/repository/RepositoryList.test.tsx(新規 / RepositoryList コンポーネント用)

    • 一覧レンダリング(名前・別名・パス・worktree 数・enabled バッジ)
    • インライン編集モード切替(編集ボタン → インプット → 保存/キャンセル)
    • 保存成功時の UI 更新(該当行の局所更新 or 再フェッチ)
    • バリデーション: 100 文字超入力時にクライアント側エラー表示
    • Enter で保存、Esc でキャンセルのキーボード操作
    • enabled=false のリポジトリで「Disabled」バッジが描画される
    • 既存 tests/unit/components/repository/RepositoryManager.test.tsx(645 行)を参考にした粒度
  3. tests/integration/api-repositories-put.test.ts(新規 / PUT /api/repositories/[id] の Integration 補完・S3-003)

src/lib/api-client.ts の新メソッド(list, updateDisplayName)は RepositoryList.test.tsx 経由で間接的にカバーする(既存 api-client の慣習に合わせる)。

受け入れ条件

  • /repositories 画面で登録済みリポジトリの一覧が表示される(RepositoryListRepositoryManager の上部に配置)
  • 各行でリポジトリ名・別名・パス・worktree 数・enabled 状態が確認できる
  • 無効化リポジトリ(enabled=false)が無効バッジ付きで一覧に表示される
  • 各行で別名をインライン編集し、保存すると DB に永続化される
  • 保存した別名が Sessions 画面・Worktree 詳細画面など既存の表示箇所に反映される(リロード後反映、S3-002)
  • 空文字 / null 保存で別名がクリアされ、フォールバックとして name が表示に戻る
  • 100 文字超入力時にクライアント側でバリデーションエラーが出る(MAX_DISPLAY_NAME_LENGTHsrc/config/repository-config.ts から import して使用)
  • 既存 PUT /api/repositories/[id] ルートのローカル定数 MAX_DISPLAY_NAME_LENGTHsrc/config/repository-config.ts からの import に置換され、エラーメッセージ文言は従来通りであること(S3-003)
  • worktreeCount 集計クエリが repository_path ベースで動作し、repository_id ではないこと(S3-001)
  • 既存 getAllRepositories(db) のシグネチャが変更されていないこと、および src/app/api/repositories/sync/route.tssrc/lib/daily-summary-generator.ts の挙動が変わっていないこと(S3-005)
  • RepositoryManager の Add/Sync 完了後に RepositoryList が自動再取得される(page.tsxrefreshKey state 連携、S3-004)
  • GET /api/repositories が既存の認証ミドルウェア(src/middleware.ts)を正常に通過し、未認証時に既存 API と同等のレスポンス(307 redirect / 401 相当)を返すこと(S3-007)
  • IP 制限が有効な環境下で、許可 IP からのみアクセスできること(既存 API と同等、S3-007)
  • 保存成功/失敗時に適切なフィードバック(トースト or メッセージ)が出る
  • ダークモードで崩れなく表示される
  • npm run lint / npx tsc --noEmit / npm run test:unit / npm run test:integration がパスする
  • 新規テスト 3 ファイル(S3-006)が追加されている:
    • tests/integration/api-repositories-list.test.ts(GET /api/repositories 用、repository_path 集計検証を含む)
    • tests/unit/components/repository/RepositoryList.test.tsxRepositoryList コンポーネント用)
    • tests/integration/api-repositories-put.test.ts(PUT /api/repositories/[id] の Integration 補完)

関連

  • Issue feat: リポジトリに別名(エイリアス)を設定して表示名を変更可能にする #642: DB/API 側の display_name 実装(本 Issue の前提)
  • ブランチ: feature/642-repo-display-name
  • 関連ファイル:
    • src/app/repositories/page.tsx
    • src/components/repository/RepositoryManager.tsx
    • src/components/repository/RepositoryList.tsx(新規)
    • src/app/api/repositories/route.ts
    • src/app/api/repositories/[id]/route.ts
    • src/app/api/repositories/sync/route.ts(変更しない / S3-005 参照)
    • src/app/api/worktrees/route.ts(既存 GET との棲み分け参照 / S3-002)
    • src/lib/api-client.ts
    • src/lib/db/db-repository.ts
    • src/lib/db/worktree-db.ts(既存 getRepositories() との棲み分け参照)
    • src/lib/db/migrations/v11-v15-feature-additions.ts(worktrees スキーマ確認 / S3-001)
    • src/lib/daily-summary-generator.ts(変更しない / S3-005 参照)
    • src/types/models.ts
    • src/middleware.ts(認証ミドルウェア流用 / S3-007)
    • src/config/repository-config.ts(新規、MAX_DISPLAY_NAME_LENGTH 定義)
    • tests/integration/api-repositories-list.test.ts(新規 / S3-006)
    • tests/integration/api-repositories-put.test.ts(新規 / S3-006 / S3-003)
    • tests/unit/components/repository/RepositoryList.test.tsx(新規 / S3-006)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions