diff --git a/.env.example b/.env.example index 7bdfc8e..733584d 100755 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ NODE_VERSION=22.22.2 +CAPTURE_BASE_URL=https://test.sm-home.cloud/ +CAPTURE_REGISTERED_ACCESS_TOKEN=usr_7f3a9c2d8e1b4a6f +VITE_ALLOWED_REGISTRATION_TOKEN=usr_7f3a9c2d8e1b4a6f PHOTOPRISM_PORT=2342 PHOTOPRISM_ORIGINALS_SOURCE=/mnt/usb/photos PHOTOPRISM_SITE_URL=https://photo.sm-home.cloud/ diff --git a/AGENTS.md b/AGENTS.md index da61bad..8f7a41d 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,13 +8,16 @@ ### Codex / AI 기본 규칙 -* 사용자가 구현, 수정, 실행, 설정 변경, 문서 수정, 채팅 응답 개선, 작업 메모 반영을 요청하면 **현재 로컬 `main`에서 바로 작업**한다 +* 사용자가 구현, 수정, 실행, 설정 변경, 문서 수정, 채팅 응답 개선을 요청하면 **현재 로컬 `main`에서 바로 작업**한다 * 브라우저 확인, 화면 검증, 접속 테스트, 기본 작업 도메인은 **`https://test.sm-home.cloud/` 하나만 기준으로 사용**한다 +* 브라우저 스크린샷/캡처 검증은 기본적으로 **루트 `.env`의 `CAPTURE_BASE_URL=https://test.sm-home.cloud/`와 등록 토큰(`CAPTURE_REGISTERED_ACCESS_TOKEN`, `VITE_ALLOWED_REGISTRATION_TOKEN`)을 사용해 토큰 등록 상태에서 진행**한다 * 별도로 운영 중인 `4173` 포트의 `ai-code-app-preview` 컨테이너는 **채팅 전용 테스트 서버가 아니라 현재 프로젝트 루트 기준 화면/기능 확인용 테스트 컨테이너**로 해석한다 * `test.sm-home.cloud` nginx 프록시는 **화면 `/`만 `5174` 앱 테스트 서버로 보내고, `/api/`와 `/ws/chat`은 항상 `127.0.0.1:3100` work-server로 유지**한다 * `test.sm-home.cloud`의 `/api`, `/ws/chat`, `/.codex_chat`, `/public/.codex_chat` 프록시 대상은 사용자가 명시적으로 요청하지 않는 한 임의로 변경하지 않는다 * 별도 지시가 없으면 `sm-home.cloud`, `rel.sm-home.cloud` 같은 다른 외부 도메인은 작업 기준으로 삼지 않는다 -* `채팅`, `작업 메모`, `작업메모`, `메모 반영`, `문서 반영` 요청은 기본적으로 **브랜치 생성 없이 main 직접 수정**으로 해석한다 +* `채팅`, `메모 반영`, `문서 반영` 요청은 기본적으로 **브랜치 생성 없이 main 직접 수정**으로 해석한다 +* `자동화 작업메모`, `자동화 메모`, 자동화 접수된 작업메모는 **항상 신규 `feature/*` 브랜치를 생성해 작업**하고, 이후 `release` 반영과 `main` 일괄반영까지 진행한다 +* 자동화 작업메모의 `main` 일괄반영이 끝나면 **프로젝트 루트에서 최신 `main`을 `pull --ff-only`로 동기화**한다 * `git remote`, `fetch`, `pull`, `push`, `branch`, `switch`, `checkout`, `merge`, `rebase`, `reset`, `stash`, `tag`, `commit` 같은 Git 관리 작업은 기본적으로 수행하지 않는다 * 사용자가 Git 작업을 **명시적으로 요청한 경우에만** 필요한 명령을 최소 범위로 수행한다 * 원격 저장소 연결 복구, 브랜치 전략 복구, release/main 동기화, 자동 merge 같은 작업을 자동으로 시도하지 않는다 @@ -27,7 +30,7 @@ * 사용자가 `git`, `브랜치`, `원격`, `push`, `pull`, `merge`, `commit`를 명시적으로 언급할 때만 Git 작업으로 해석한다 * 사용자가 `Plan 등록`, `Plan 게시판 등록`, `게시판 등록`, `계획 등록`이라고 말하면 **Plan 게시판 항목 생성** 의미로 해석한다 * 사용자가 `자동화 등록`, `자동화 접수`, `자동화 실행`, `자동화 돌려줘`라고 말하면 **자동화 API 등록 또는 실제 자동화 수행** 의미로 해석한다 -* 사용자가 `자동화 메모`라고만 말하면 게시판 메모인지 자동화 접수 메모인지 문맥을 먼저 확인한다 +* 사용자가 `자동화 메모`라고만 말하면 기본적으로 **자동화 접수 작업메모**로 해석하고, 신규 `feature/*` 브랜치 생성부터 `release` 반영, `main` 일괄반영, 프로젝트 루트 `pull`까지 포함한 흐름을 적용한다 * 사용자가 `Plan 게시판에 등록만`, `자동화 없이`, `구현하지 말고 계획만 등록`, `게시판만`처럼 표현하면 **자동화 실행 없이 Plan 항목만 등록**한다 * 사용자가 `자동화해줘`, `구현해줘`, `작업 진행해줘`, `실행해줘`처럼 실제 수행을 명시한 경우에만 자동화 또는 코드 작업으로 해석한다 * 요청이 모호하면 자동화 실행을 바로 진행하지 말고, 우선 **Plan 등록**으로 안전 해석하거나 짧게 재확인한다 @@ -45,31 +48,33 @@ ## Codex Live / 채팅 / 작업 메모 규칙 -* `Codex Live`, 일반 채팅, 작업 메모 반영 요청은 모두 현재 프로젝트의 로컬 `main`을 기준으로 처리한다 +* `Codex Live`, 일반 채팅 요청은 현재 프로젝트의 로컬 `main`을 기준으로 처리한다 +* 자동화 작업메모 반영 요청은 예외적으로 **신규 `feature/*` 브랜치 생성 -> `release` 반영 -> `main` 일괄반영 -> 프로젝트 루트 `pull`** 순서를 기본으로 처리한다 * 외부 도메인 기준 동작 확인이 필요하면 기본 대상은 항상 `https://test.sm-home.cloud/`로 본다 * `https://test.sm-home.cloud/`에서 채팅/API 문제가 보이면 먼저 프런트 코드보다 **nginx의 `/api`와 `/ws/chat` 프록시가 `3100`을 가리키는지** 확인한다 * 채팅에서 나온 수정 요청도 별도 브랜치 생성 없이 바로 파일 수정으로 이어질 수 있다 -* 작업 메모는 기록 목적이든 실제 수정 지시든 우선 `main` 기준의 로컬 작업으로 연결한다 -* 채팅과 작업 메모는 Git flow를 강제하지 않고, 필요한 경우에만 사용자가 별도로 Git 단계를 요청한다 +* 일반 작업 메모는 기록 목적이면 메모로 유지하고, 실제 수정 지시라도 별도 자동화 접수가 아니면 `main` 기준 로컬 작업으로 연결한다 +* 자동화 작업메모는 Git flow를 기본으로 적용하며, 일반 채팅/수동 작업은 필요한 경우에만 사용자가 별도로 Git 단계를 요청한다 * 채팅에서 파일/문서/이미지/코드 리소스를 제공할 때는 반드시 `public/.codex_chat//resource/` 아래 세션 전용 경로를 기준으로 사용한다 * 채팅 첨부 파일은 `public/.codex_chat//resource/uploads/` 아래 경로를 기준으로 사용한다 * 원본 파일 경로만 응답해도 서버가 해당 리소스를 위 세션 경로로 복사해 공개 링크로 바꿔 줄 수 있지만, 문서와 안내 문구에는 위 공개 경로 기준을 우선 명시한다 +* 토큰 등록이 필요한 화면 검증이나 모바일 스크린샷은 **비로그인 기본 화면(Docs)으로 확인하지 말고**, `.env`에 저장된 등록 토큰을 브라우저 `localStorage`에 주입한 상태로 캡처한다 --- ## Plan / 자동화 메모 -* 기존 문서에 남아 있는 `feature`, `hotfix`, `release` 흐름은 **현재 로컬 모드에서는 기본 규칙으로 사용하지 않는다** -* Plan 게시판과 자동화 관련 기능 설명은 UI/상태 설명으로만 참고하고, 실제 Git 브랜치 운영 규칙으로 자동 적용하지 않는다 -* 사용자가 나중에 브랜치 전략 복구를 명시적으로 요청하면 그때 별도 문서 갱신 후 다시 적용한다 +* 일반 수동 작업에는 여전히 로컬 `main` 직접 수정 원칙을 유지한다 +* 다만 자동화 작업메모와 Plan 자동화에는 기존 `feature -> release -> main -> 프로젝트 루트 pull` 흐름을 기본 규칙으로 다시 적용한다 +* `hotfix/*` 흐름은 기존 예외 규칙을 유지하고, 자동화 대상이 아닌 일반 요청에는 자동 적용하지 않는다 --- ## 한 줄 요약 -👉 지금은 로컬 `main`에서 바로 수정한다 +👉 일반 채팅/수동 요청은 로컬 `main`에서 바로 수정한다 👉 외부 확인 기본 도메인은 `https://test.sm-home.cloud/` 하나다 -👉 채팅이든 작업 메모든 기본 해석은 `main` 직접 작업이다 -👉 Git 작업은 사용자가 명시적으로 요청할 때만 최소 범위로 한다 +👉 자동화 작업메모는 `feature -> release -> main -> 프로젝트 루트 pull` 흐름을 탄다 +👉 일반 Git 작업은 사용자가 명시적으로 요청할 때만 최소 범위로 한다 --- diff --git a/README.md b/README.md index 9ec1bed..1847640 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ React + Vite + TypeScript 기반의 문서/샘플 허브 애플리케이션입 - 현재 저장소는 당분간 **로컬 전용 작업 모드**로 사용합니다. - Git 원격 동기화, 브랜치 운영, 자동 병합/자동화는 잠시 중지한 상태로 간주합니다. - Codex나 자동화 도구는 기본적으로 Git 작업 없이 **현재 프로젝트 루트의 `main` 작업본을 바로 수정**합니다. -- `채팅`, `Codex Live`, `작업메모`, `메모 반영` 요청도 같은 기준으로 해석합니다. +- `채팅`, `Codex Live`, 일반 `작업메모`, `메모 반영` 요청도 같은 기준으로 해석합니다. +- 단, 자동화 접수된 작업메모는 예외적으로 **신규 `feature/*` 브랜치 생성 -> `release` 반영 -> `main` 일괄반영 -> 프로젝트 루트 `pull --ff-only`** 흐름을 사용합니다. - 채팅 리소스와 첨부 파일은 `public/.codex_chat//resource/...` 기준으로 제공하며, 업로드 파일은 `public/.codex_chat//resource/uploads/...` 아래를 사용합니다. ## 시작하기 diff --git a/docs/README.md b/docs/README.md index 6f2415b..8f9a3c2 100755 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,8 @@ - 현재 저장소는 당분간 로컬 전용으로 운영합니다. - 문서 정리나 Codex 작업 시 Git 원격 동기화, 브랜치 운영, 자동 merge 흐름은 기본 전제로 사용하지 않습니다. -- `Codex Live`, 일반 채팅, 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**하는 방식으로 처리합니다. +- `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**하는 방식으로 처리합니다. +- 자동화 작업메모는 예외적으로 **신규 `feature/*` 브랜치 생성 -> `release` 반영 -> `main` 일괄반영 -> 프로젝트 루트 `pull --ff-only`** 흐름으로 처리합니다. - Git 관련 작업은 사용자가 명시적으로 요청할 때만 수행합니다. ## 1. 작업일지 @@ -149,7 +150,7 @@ src/components - `Plan` 기능은 `src/features/planBoard`에서 관리 - 현재 앱에는 목록/상세 보드, release 검수, 차트, 스케줄 화면이 함께 포함됨 - 기본 상태는 `등록`, `작업중`, `작업완료`, `릴리즈완료`, `완료` -- 자동화 진행은 `workerStatus`로 별도 관리하며 브랜치 준비, 자동 작업, release/main 반영 상태를 표현 +- 자동화 진행은 `workerStatus`로 별도 관리하며 브랜치 준비, 자동 작업, release/main 반영, 프로젝트 루트 pull 완료 상태를 포함한 흐름을 표현 - `작업시작` 이후에는 원본 요청(`작업 ID`, 원본 메모`)을 수정하지 않고 조치 이력으로 누적 기록 - 권한 토큰이 없으면 조회 중심으로 동작하며 민감 메모와 소스 작업 일부는 제한 또는 마스킹 - 관련 기능 문서는 `docs/features/plan-board-review.md`, `plan-automation.md`, `plan-schedule.md`, `plan-usage.md` 참고 diff --git a/docs/features/plan-automation.md b/docs/features/plan-automation.md index 3c69e29..0a2bf9f 100755 --- a/docs/features/plan-automation.md +++ b/docs/features/plan-automation.md @@ -4,6 +4,8 @@ Plan 자동화 기능의 데이터 구조, API 연동 방식, release 검수 연계 방식을 정리합니다. +현재 운영 규칙에서 자동화 작업메모(`auto_worker`)는 항상 신규 `feature/*` 브랜치에서 시작하며, `release` 반영과 `main` 일괄반영, 프로젝트 루트 `pull --ff-only`까지 이어집니다. 반면 `Codex Live`나 일반 수동 요청은 여전히 로컬 `main` 직접 수정 기준을 유지합니다. + ## 구현 위치 - 화면 진입: `src/app/main/MainContent.tsx` @@ -114,6 +116,8 @@ Plan 상세에서는 최신 자동 작업 결과를 탭 형태로 확인할 수 Codex 실행 로그에 `tokens used`가 잡히면 source work 기록과 상세 상단 상태 영역에 함께 표기합니다. +자동화 작업메모가 `main` 반영 단계까지 끝나면 worker는 `PLAN_MAIN_PROJECT_REPO_PATH` 기준 프로젝트 루트에서 `pull --ff-only`를 수행해 실제 작업본을 최신 `main`으로 맞춥니다. + ## 차트 집계 방식 `charts.tsx`는 Plan 전체 목록을 주기적으로 다시 불러와 최근 성과를 집계합니다. diff --git a/docs/features/plan-board-review.md b/docs/features/plan-board-review.md index bf27ff4..b9ce7b7 100755 --- a/docs/features/plan-board-review.md +++ b/docs/features/plan-board-review.md @@ -49,7 +49,7 @@ 2. `작업시작` 또는 자동화에 따라 브랜치 준비와 작업이 진행됩니다. 3. 작업이 완료되면 release 반영 상태와 main 반영 대기 여부가 추적됩니다. 4. release 검수 화면에서 샘플/위젯/변경 파일을 확인하고 검수 상태를 갱신합니다. -5. main 반영이 끝나면 최종 완료 흐름으로 정리됩니다. +5. main 반영이 끝나면 프로젝트 루트 `pull --ff-only`까지 수행한 뒤 최종 완료 흐름으로 정리됩니다. ## 목록 기능 @@ -88,6 +88,8 @@ - `작업취소` - `main 일괄 반영 요청` +자동화 작업메모(`auto_worker`)는 항상 신규 `feature/*` 브랜치로 시작하며, `release` 반영과 `main` 일괄 반영 뒤 프로젝트 루트 동기화까지 포함합니다. + 세부 액션은 `/plan/items/:id/actions/:action` API로 실행됩니다. ## 자동 새로고침과 알림 diff --git a/etc/servers/work-server/README.md b/etc/servers/work-server/README.md index 9904e79..52bf80a 100644 --- a/etc/servers/work-server/README.md +++ b/etc/servers/work-server/README.md @@ -57,7 +57,15 @@ npm run server-command:runner 소스 수정이 필요하면 **현재 실행 환경에서 실제로 연결된 `main_project` 저장소 루트의 `AGENTS.md` 규칙을 먼저 확인한 뒤**, 그 작업 트리에서 바로 수정합니다. -현재 운영 기준에서는 `Codex Live`, 일반 채팅, 작업메모 반영 요청 모두 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**합니다. 별도 브랜치 생성이나 `release -> main` 동기화는 기본 전제로 사용하지 않으며, Git 관련 작업은 사용자가 명시적으로 요청할 때만 수행합니다. +현재 운영 기준에서는 `Codex Live`, 일반 채팅, 일반 작업메모 반영 요청은 **현재 프로젝트 루트의 로컬 `main` 작업본을 바로 수정**합니다. + +단, 자동화 작업메모(`auto_worker`)는 예외적으로 아래 Git 흐름을 기본 동작으로 사용합니다. + +- 신규 `feature/*` 브랜치 생성 +- 자동 작업 수행 +- `release` 브랜치 반영 +- `main` 일괄반영 +- `PLAN_MAIN_PROJECT_REPO_PATH` 기준 프로젝트 루트에서 `pull --ff-only` 브라우저 기준 접속 확인, 화면 검증, 외부 도메인 테스트는 **`https://test.sm-home.cloud/`를 기본 작업 도메인으로 사용**합니다. 별도 요청이 없는 한 `sm-home.cloud`나 `rel.sm-home.cloud`는 기본 검증 대상으로 삼지 않습니다. @@ -69,14 +77,15 @@ npm run server-command:runner `Plan` 게시판 항목을 작업 큐처럼 읽어 자동화할 수 있습니다. -현재 로컬 운영 모드에서는 아래 자동 브랜치 흐름을 기본 동작으로 강제하지 않습니다. 필요 시 사용자가 별도로 요청한 경우에만 사용합니다. +현재 운영 기준에서 자동화 작업메모는 아래 브랜치 흐름을 기본 동작으로 사용합니다. -- `등록` 상태: worker가 읽어서 `feature/plan-{id}-{workId}` 브랜치 생성 시도 +- `등록` 상태: worker가 읽어서 신규 `feature/plan-{id}-{workId}` 브랜치 생성 - 성공 시: `작업중`, `브랜치준비` - 실패 시: `이슈`, 최근 오류 기록 - `개발완료` 상태: worker가 `release` 브랜치 병합 시도 - 병합 성공 시: `완료` - 병합 실패 시: `이슈` +- `main` 반영 성공 시: `PLAN_MAIN_PROJECT_REPO_PATH`에서 `main` 브랜치 `pull --ff-only`까지 수행 안전 조건: diff --git a/etc/servers/work-server/src/services/plan-policy.test.ts b/etc/servers/work-server/src/services/plan-policy.test.ts index 604fff5..ff7b5dc 100755 --- a/etc/servers/work-server/src/services/plan-policy.test.ts +++ b/etc/servers/work-server/src/services/plan-policy.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { buildPlanNotificationData } from './plan-notification-service.js'; import { shouldNotifyPlanRestart } from './plan-notification-policy.js'; import { shouldTriggerRetryFromActionNote } from './plan-retry-policy.js'; -import { issueActionSchema } from './plan-service.js'; +import { issueActionSchema, shouldUseLocalMainPlanMode } from './plan-service.js'; test('shouldTriggerRetryFromActionNote detects missing-fix and verification follow-up requests', () => { assert.equal(shouldTriggerRetryFromActionNote('누락된 거 다시 고쳐서 테스트해 줘'), true); @@ -52,3 +52,9 @@ test('buildPlanNotificationData uses stable task key per plan', () => { notificationKey: 'plan:17', }); }); + +test('shouldUseLocalMainPlanMode keeps auto_worker on branch workflow', () => { + process.env.PLAN_LOCAL_MAIN_MODE = 'true'; + assert.equal(shouldUseLocalMainPlanMode('auto_worker'), false); + assert.equal(shouldUseLocalMainPlanMode('none'), true); +}); diff --git a/etc/servers/work-server/src/services/plan-service.ts b/etc/servers/work-server/src/services/plan-service.ts index 6d4baf2..2f7df2c 100755 --- a/etc/servers/work-server/src/services/plan-service.ts +++ b/etc/servers/work-server/src/services/plan-service.ts @@ -268,6 +268,11 @@ export function buildPlanBranchName(workId: string, id: number) { return `${prefix}/plan-${id}-${token}`; } +export function shouldUseLocalMainPlanMode(automationType: unknown) { + const env = getEnv(); + return Boolean(env.PLAN_LOCAL_MAIN_MODE) && normalizePlanAutomationType(automationType) !== 'auto_worker'; +} + export function mapPlanRow( row: Record, options?: PlanRowOptions, @@ -2399,7 +2404,6 @@ export async function listPlanIssueSummaries(planItemIds: number[]) { export async function claimNextPlanForBranch(workerId: string) { await ensurePlanTable(); - const env = getEnv(); return db.transaction(async (trx) => { const row = await trx(PLAN_TABLE) @@ -2417,7 +2421,9 @@ export async function claimNextPlanForBranch(workerId: string) { return null; } - const assignedBranch = env.PLAN_LOCAL_MAIN_MODE ? env.PLAN_MAIN_BRANCH : buildPlanBranchName(String(row.work_id), Number(row.id)); + const assignedBranch = shouldUseLocalMainPlanMode(row.automation_type) + ? String(getEnv().PLAN_MAIN_BRANCH) + : buildPlanBranchName(String(row.work_id), Number(row.id)); const rows = await trx(PLAN_TABLE) .where({ id: row.id }) .update({ diff --git a/etc/servers/work-server/src/workers/plan-worker.ts b/etc/servers/work-server/src/workers/plan-worker.ts index 71b443d..9f82d39 100755 --- a/etc/servers/work-server/src/workers/plan-worker.ts +++ b/etc/servers/work-server/src/workers/plan-worker.ts @@ -30,6 +30,7 @@ import { markPlanReleaseMerged, markPlanMerged, markPlanWorkCompleted, + shouldUseLocalMainPlanMode, upsertAutoPlanItem, } from '../services/plan-service.js'; import { @@ -200,6 +201,10 @@ export class PlanWorker { return Boolean(getEnv().PLAN_LOCAL_MAIN_MODE); } + private shouldUseLocalMainModeForPlan(item: { automationType?: unknown }) { + return shouldUseLocalMainPlanMode(item.automationType); + } + start() { const env = getEnv(); @@ -613,8 +618,10 @@ export class PlanWorker { planId: number, workId: string, note: unknown, + automationType: unknown, ) { const env = getEnv(); + const useLocalMainMode = shouldUseLocalMainPlanMode(automationType); const runCodexCommandAttempt = async (attempt: number) => await new Promise((resolve, reject) => { let settled = false; @@ -629,7 +636,7 @@ export class PlanWorker { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, - PLAN_REPO_PATH: env.PLAN_LOCAL_MAIN_MODE ? env.PLAN_MAIN_PROJECT_REPO_PATH : env.PLAN_GIT_REPO_PATH, + PLAN_REPO_PATH: useLocalMainMode ? env.PLAN_MAIN_PROJECT_REPO_PATH : env.PLAN_GIT_REPO_PATH, PLAN_API_BASE_URL: 'http://127.0.0.1:3100/api', PLAN_ACCESS_TOKEN: ERROR_LOG_VIEW_TOKEN, PLAN_ITEM_ID: String(planId), @@ -637,7 +644,7 @@ export class PlanWorker { PLAN_CODEX_TEMPLATE_HOME: env.PLAN_CODEX_TEMPLATE_HOME, PLAN_GIT_USER_NAME: env.PLAN_GIT_USER_NAME, PLAN_GIT_USER_EMAIL: env.PLAN_GIT_USER_EMAIL, - PLAN_LOCAL_MAIN_MODE: env.PLAN_LOCAL_MAIN_MODE ? 'true' : 'false', + PLAN_LOCAL_MAIN_MODE: useLocalMainMode ? 'true' : 'false', PLAN_SKIP_WORK_COMPLETE: 'true', }, }); @@ -907,7 +914,7 @@ export class PlanWorker { const releaseTarget = String(item.releaseTarget ?? env.PLAN_RELEASE_BRANCH); try { - if (!this.isLocalMainMode()) { + if (!this.shouldUseLocalMainModeForPlan(item)) { await cleanAutomationWorktree(env.PLAN_GIT_REPO_PATH); await ensureBranchExists( { @@ -926,8 +933,8 @@ export class PlanWorker { return; } this.logger.info( - { planId, branch: assignedBranch, localMainMode: this.isLocalMainMode() }, - this.isLocalMainMode() ? 'Plan local main execution prepared' : 'Plan branch created', + { planId, branch: assignedBranch, localMainMode: this.shouldUseLocalMainModeForPlan(item) }, + this.shouldUseLocalMainModeForPlan(item) ? 'Plan local main execution prepared' : 'Plan branch created', ); } catch (error) { const message = error instanceof Error ? error.message : '브랜치 생성에 실패했습니다.'; @@ -975,7 +982,7 @@ export class PlanWorker { const autoDeployToMain = Boolean(item.autoDeployToMain ?? true); try { - if (this.isLocalMainMode()) { + if (this.shouldUseLocalMainModeForPlan(item)) { const completedRow = await markPlanAsCompleted( planId, '로컬 main 직접 작업 모드에서 release/main 반영 단계를 건너뛰고 완료 처리했습니다.', @@ -1071,7 +1078,7 @@ export class PlanWorker { const planLabel = formatPlanNotificationLabel(workId, planId); try { - if (this.isLocalMainMode()) { + if (this.shouldUseLocalMainModeForPlan(item)) { const completedRow = await markPlanAsCompleted( planId, '로컬 main 직접 작업 모드에서 main 반영 단계를 건너뛰고 완료 처리했습니다.', @@ -1201,7 +1208,7 @@ export class PlanWorker { 'work-started', ); - const output = await this.runCodexCommandWithProgressNotifications(planId, workId, item.note); + const output = await this.runCodexCommandWithProgressNotifications(planId, workId, item.note, item.automationType); if (output.includes('처리할 Plan 항목이 없습니다.')) { throw new Error('자동 작업 대상 Plan 항목을 찾지 못했습니다. 상태 전환 로직을 확인해 주세요.'); @@ -1229,7 +1236,7 @@ export class PlanWorker { return; } - const finalCompletedRow = this.isLocalMainMode() + const finalCompletedRow = this.shouldUseLocalMainModeForPlan(item) ? await markPlanAsCompleted(planId, '로컬 main 직접 작업으로 자동 작업을 완료했습니다.') : await markPlanWorkCompleted(planId, this.workerId, '자동 작업을 완료했습니다.'); if (!finalCompletedRow) { @@ -1240,7 +1247,9 @@ export class PlanWorker { planId, workId, planLabel, - this.isLocalMainMode() ? '자동 작업이 로컬 main 작업본에 직접 반영되어 완료되었습니다.' : this.buildExecutionCompletedBody(autoDeployToMain), + this.shouldUseLocalMainModeForPlan(item) + ? '자동 작업이 로컬 main 작업본에 직접 반영되어 완료되었습니다.' + : this.buildExecutionCompletedBody(autoDeployToMain), 'work-completed', ); this.logger.info({ planId }, 'Plan Codex execution completed'); diff --git a/scripts/capture-auth-utils.mjs b/scripts/capture-auth-utils.mjs new file mode 100644 index 0000000..392af5a --- /dev/null +++ b/scripts/capture-auth-utils.mjs @@ -0,0 +1,94 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const DEFAULT_CAPTURE_STORAGE_KEY = 'work-app.token-access.registered-token'; +const DEFAULT_CAPTURE_BASE_URL = 'https://test.sm-home.cloud/'; + +function stripWrappingQuotes(value) { + if (!value) { + return value; + } + + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); + } + + return value; +} + +function applyEnvFile(filePath) { + if (!fs.existsSync(filePath)) { + return; + } + + const content = fs.readFileSync(filePath, 'utf8'); + + for (const rawLine of content.split(/\r?\n/u)) { + const line = rawLine.trim(); + + if (!line || line.startsWith('#')) { + continue; + } + + const separatorIndex = line.indexOf('='); + + if (separatorIndex <= 0) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + const value = stripWrappingQuotes(line.slice(separatorIndex + 1).trim()); + + if (!key || process.env[key] !== undefined) { + continue; + } + + process.env[key] = value; + } +} + +function loadCaptureEnv(cwd = process.cwd()) { + applyEnvFile(path.join(cwd, '.env')); + applyEnvFile(path.join(cwd, 'etc/servers/work-server/.env')); +} + +export function getCaptureRuntimeConfig(cwd = process.cwd()) { + loadCaptureEnv(cwd); + + return { + baseUrl: + process.env.CAPTURE_BASE_URL?.trim() || + process.env.SERVER_COMMAND_TEST_URL?.trim() || + DEFAULT_CAPTURE_BASE_URL, + tokenAccessStorageKey: + process.env.CAPTURE_REGISTERED_ACCESS_STORAGE_KEY?.trim() || DEFAULT_CAPTURE_STORAGE_KEY, + registeredAccessToken: + process.env.CAPTURE_REGISTERED_ACCESS_TOKEN?.trim() || + process.env.VITE_ALLOWED_REGISTRATION_TOKEN?.trim() || + process.env.SERVER_COMMAND_ACCESS_TOKEN?.trim() || + '', + }; +} + +export async function createCaptureContext(browser, options = {}) { + const { tokenAccessStorageKey, registeredAccessToken } = getCaptureRuntimeConfig(); + const context = await browser.newContext(options); + + if (registeredAccessToken) { + await context.addInitScript( + ({ accessToken, storageKey }) => { + window.localStorage.setItem(storageKey, accessToken); + }, + { + accessToken: registeredAccessToken, + storageKey: tokenAccessStorageKey, + }, + ); + } + + return context; +} diff --git a/scripts/capture-component-screenshot.mjs b/scripts/capture-component-screenshot.mjs index 188a33a..554a724 100755 --- a/scripts/capture-component-screenshot.mjs +++ b/scripts/capture-component-screenshot.mjs @@ -1,11 +1,12 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const cwd = process.cwd(); const componentId = process.argv[2]; const captureDate = process.argv[3] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); if (!componentId) { console.error('Usage: node scripts/capture-component-screenshot.mjs [YYYY-MM-DD]'); @@ -21,13 +22,14 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv const targetSelector = `#component-sample-${componentId}`; const browser = await chromium.launch({ headless: true }); -const page = await browser.newPage({ +const context = await createCaptureContext(browser, { viewport: { width: 1600, height: 1200, }, deviceScaleFactor: 2, }); +const page = await context.newPage(); try { await ensureDirectory(screenshotDir); @@ -59,5 +61,6 @@ try { console.log(`Saved: ${screenshotPath}`); console.log(`Linked in: ${worklogPath}`); } finally { + await context.close(); await browser.close(); } diff --git a/scripts/capture-feature-screenshot.mjs b/scripts/capture-feature-screenshot.mjs index 96b335a..89b5aa1 100755 --- a/scripts/capture-feature-screenshot.mjs +++ b/scripts/capture-feature-screenshot.mjs @@ -1,5 +1,6 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const FEATURE_CAPTURE_PRESETS = { @@ -56,7 +57,7 @@ const FEATURE_CAPTURE_PRESETS = { const cwd = process.cwd(); const presetKey = process.argv[2]; const captureDate = process.argv[3] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); if (!presetKey || !(presetKey in FEATURE_CAPTURE_PRESETS)) { console.error(`Usage: node scripts/capture-feature-screenshot.mjs <${Object.keys(FEATURE_CAPTURE_PRESETS).join('|')}> [YYYY-MM-DD]`); @@ -71,10 +72,11 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const page = await browser.newPage({ +const context = await createCaptureContext(browser, { viewport: { width: 1600, height: 1200 }, deviceScaleFactor: 2, }); +const page = await context.newPage(); try { await ensureDirectory(screenshotDir); @@ -113,5 +115,6 @@ try { console.log(`Saved: ${screenshotPath}`); console.log(`Linked in: ${worklogPath}`); } finally { + await context.close(); await browser.close(); } diff --git a/scripts/capture-fullscreen-toggle-screenshot.mjs b/scripts/capture-fullscreen-toggle-screenshot.mjs index 6fdd5d2..4bf6642 100755 --- a/scripts/capture-fullscreen-toggle-screenshot.mjs +++ b/scripts/capture-fullscreen-toggle-screenshot.mjs @@ -1,10 +1,11 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const cwd = process.cwd(); const captureDate = process.argv[2] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); const screenshotFileName = 'main-content-fullscreen-toggle.png'; const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({ cwd, @@ -13,10 +14,11 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const page = await browser.newPage({ +const context = await createCaptureContext(browser, { viewport: { width: 1600, height: 1200 }, deviceScaleFactor: 2, }); +const page = await context.newPage(); try { await ensureDirectory(screenshotDir); @@ -41,5 +43,6 @@ try { console.log(`Saved: ${screenshotPath}`); console.log(`Linked in: ${worklogPath}`); } finally { + await context.close(); await browser.close(); } diff --git a/scripts/capture-menu-screenshot.mjs b/scripts/capture-menu-screenshot.mjs index b7b520b..1b6dd79 100755 --- a/scripts/capture-menu-screenshot.mjs +++ b/scripts/capture-menu-screenshot.mjs @@ -1,11 +1,12 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const cwd = process.cwd(); const menuGroup = process.argv[2] ?? 'docs'; const captureDate = process.argv[3] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); const supportedMenuGroups = new Set(['docs', 'plans']); @@ -22,10 +23,11 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const page = await browser.newPage({ +const context = await createCaptureContext(browser, { viewport: { width: 1600, height: 1200 }, deviceScaleFactor: 2, }); +const page = await context.newPage(); try { await ensureDirectory(screenshotDir); @@ -57,5 +59,6 @@ try { console.log(`Saved: ${screenshotPath}`); console.log(`Linked in: ${worklogPath}`); } finally { + await context.close(); await browser.close(); } diff --git a/scripts/capture-plan-board-mobile-screenshot.mjs b/scripts/capture-plan-board-mobile-screenshot.mjs index f5f2979..b9c87be 100755 --- a/scripts/capture-plan-board-mobile-screenshot.mjs +++ b/scripts/capture-plan-board-mobile-screenshot.mjs @@ -1,10 +1,11 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const cwd = process.cwd(); const captureDate = process.argv[2] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); const screenshotFileName = 'plan-board-mobile-memo-detail.png'; const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({ cwd, @@ -13,7 +14,7 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const context = await browser.newContext({ +const context = await createCaptureContext(browser, { viewport: { width: 430, height: 932 }, isMobile: true, hasTouch: true, diff --git a/scripts/capture-search-command-screenshot.mjs b/scripts/capture-search-command-screenshot.mjs index 08171fe..f7d44b7 100755 --- a/scripts/capture-search-command-screenshot.mjs +++ b/scripts/capture-search-command-screenshot.mjs @@ -1,10 +1,11 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; const cwd = process.cwd(); const captureDate = process.argv[2] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:5174'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); const screenshotFileName = 'search-command.png'; const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolveCapturePaths({ cwd, @@ -13,7 +14,7 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const context = await browser.newContext({ +const context = await createCaptureContext(browser, { viewport: { width: 430, height: 932 }, isMobile: true, hasTouch: true, diff --git a/scripts/capture-settings-screenshot.mjs b/scripts/capture-settings-screenshot.mjs index 5b0efc7..7aa14d9 100755 --- a/scripts/capture-settings-screenshot.mjs +++ b/scripts/capture-settings-screenshot.mjs @@ -1,10 +1,8 @@ import process from 'node:process'; import { chromium } from 'playwright'; +import { createCaptureContext, getCaptureRuntimeConfig } from './capture-auth-utils.mjs'; import { ensureDirectory, getKstDate, resolveCapturePaths, updateWorklogCaptureSection } from './worklog-capture-utils.mjs'; -const ACCESS_TOKEN = 'usr_7f3a9c2d8e1b4a6f'; -const TOKEN_ACCESS_STORAGE_KEY = 'work-app.token-access.registered-token'; - const SETTINGS_CAPTURE_PRESETS = { automation: { screenshotFileName: 'settings-app.png', @@ -19,7 +17,7 @@ const SETTINGS_CAPTURE_PRESETS = { const cwd = process.cwd(); const presetKey = process.argv[2]; const captureDate = process.argv[3] ?? getKstDate(); -const baseUrl = process.env.CAPTURE_BASE_URL ?? 'http://127.0.0.1:4173'; +const { baseUrl } = getCaptureRuntimeConfig(cwd); if (!presetKey || !(presetKey in SETTINGS_CAPTURE_PRESETS)) { console.error(`Usage: node scripts/capture-settings-screenshot.mjs <${Object.keys(SETTINGS_CAPTURE_PRESETS).join('|')}> [YYYY-MM-DD]`); @@ -34,21 +32,11 @@ const { screenshotDir, screenshotPath, worklogPath, markdownImagePath } = resolv }); const browser = await chromium.launch({ headless: true }); -const context = await browser.newContext({ +const context = await createCaptureContext(browser, { viewport: { width: 1600, height: 1200 }, deviceScaleFactor: 2, }); -await context.addInitScript( - ({ tokenAccessStorageKey, accessToken }) => { - window.localStorage.setItem(tokenAccessStorageKey, accessToken); - }, - { - tokenAccessStorageKey: TOKEN_ACCESS_STORAGE_KEY, - accessToken: ACCESS_TOKEN, - }, -); - const page = await context.newPage(); try { diff --git a/src/app/main/ChatSourceChangesPage.tsx b/src/app/main/ChatSourceChangesPage.tsx index c73c94a..6e83b84 100644 --- a/src/app/main/ChatSourceChangesPage.tsx +++ b/src/app/main/ChatSourceChangesPage.tsx @@ -676,7 +676,9 @@ export function ChatSourceChangesPage() { > - {entry.conversationTitle} + + {entry.conversationTitle} + {entry.status} {entry.currentSourceStatus === 'applied' ? '현재 소스 적용' : '현재 소스 미적용'} @@ -702,7 +704,7 @@ export function ChatSourceChangesPage() { {selectedEntry ? ( - + <Title level={5} className="chat-source-changes-page__detail-title"> {selectedEntry.requestTitle} diff --git a/src/app/main/MainChatPanel.hotfix.css b/src/app/main/MainChatPanel.hotfix.css index d4aa44f..16915d2 100644 --- a/src/app/main/MainChatPanel.hotfix.css +++ b/src/app/main/MainChatPanel.hotfix.css @@ -2149,6 +2149,22 @@ overflow: auto; } +.app-chat-panel__resource-strip-filter { + display: flex; + align-items: center; + min-width: 0; + color: #334155; + font-size: 11px; + line-height: 1.4; +} + +.app-chat-panel__resource-strip-filter .ant-checkbox-wrapper { + width: 100%; + margin-inline-start: 0; + font-size: inherit; + color: inherit; +} + .app-chat-panel__resource-strip-empty.ant-typography { margin: 0; font-size: 11px; diff --git a/src/app/main/MainHeader.tsx b/src/app/main/MainHeader.tsx index 6aea2c6..d33a46a 100755 --- a/src/app/main/MainHeader.tsx +++ b/src/app/main/MainHeader.tsx @@ -484,12 +484,18 @@ function formatDateTimeLabel(value: string | null) { function getServerVersionStatusClassName(item: ServerCommandItem | null) { if (!item) { - return 'app-header__server-version-indicator--stale'; + return 'app-header__server-version-indicator--unknown'; } - return item.buildRequired || item.updateAvailable - ? 'app-header__server-version-indicator--stale' - : 'app-header__server-version-indicator--latest'; + if (item.buildRequired) { + return 'app-header__server-version-indicator--build-required'; + } + + if (item.updateAvailable) { + return 'app-header__server-version-indicator--update-available'; + } + + return 'app-header__server-version-indicator--latest'; } function getServerLastSourceChangedDateLabel(item: ServerCommandItem | null) { @@ -501,8 +507,12 @@ function getServerVersionStatusTitle(item: ServerCommandItem | null, label: stri return `${label} 최신 버전 확인 전`; } - if (item.buildRequired || item.updateAvailable) { - return `${label} 최신 버전 아님`; + if (item.buildRequired) { + return `${label} 커밋 미반영 상태`; + } + + if (item.updateAvailable) { + return `${label} 운영 반영 대기 상태`; } return `${label} 최신 버전`; diff --git a/src/app/main/MainLayout.css b/src/app/main/MainLayout.css index 40ccb0f..aa59a30 100755 --- a/src/app/main/MainLayout.css +++ b/src/app/main/MainLayout.css @@ -228,7 +228,15 @@ background: #2563eb; } -.app-header__server-version-indicator--stale { +.app-header__server-version-indicator--unknown { + background: #94a3b8; +} + +.app-header__server-version-indicator--update-available { + background: #f59e0b; +} + +.app-header__server-version-indicator--build-required { background: #dc2626; } diff --git a/src/app/main/mainChatPanel/ChatConversationView.tsx b/src/app/main/mainChatPanel/ChatConversationView.tsx index 9594eee..711dadf 100755 --- a/src/app/main/mainChatPanel/ChatConversationView.tsx +++ b/src/app/main/mainChatPanel/ChatConversationView.tsx @@ -17,7 +17,7 @@ import { ThunderboltOutlined, UpOutlined, } from '@ant-design/icons'; -import { Alert, Button, Input, Select, Spin, message } from 'antd'; +import { Alert, Button, Checkbox, Input, Select, Spin, message } from 'antd'; import type { TextAreaRef } from 'antd/es/input/TextArea'; import { useEffect, @@ -159,6 +159,16 @@ function buildInlinePreviewLabel(url: string) { } } +function buildPreviewFileName(item: PreviewOption) { + try { + const parsed = new URL(item.url, typeof window !== 'undefined' ? window.location.origin : 'https://local.invalid'); + const fileName = parsed.pathname.split('/').filter(Boolean).at(-1)?.trim(); + return fileName || item.label.trim() || item.url; + } catch { + return item.label.trim() || item.url; + } +} + async function createPreviewFetchError(response: Response): Promise { const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''; let responseMessage = ''; @@ -750,6 +760,7 @@ export function ChatConversationView({ const [collapsedActivityRequestIds, setCollapsedActivityRequestIds] = useState([]); const [collapsibleMessageIds, setCollapsibleMessageIds] = useState([]); const [showBusyOverlay, setShowBusyOverlay] = useState(false); + const [showLatestResourceOnly, setShowLatestResourceOnly] = useState(true); const fileInputRef = useRef(null); const activitySectionRefs = useRef(new Map()); const messageBodyRefs = useRef(new Map()); @@ -808,6 +819,23 @@ export function ChatConversationView({ return [...ordered, ...orphanActivityMessages]; }, [visibleMessages]); const previewItemsByUrl = useMemo(() => new Map(previewItems.map((item) => [item.url, item])), [previewItems]); + const visiblePreviewItems = useMemo(() => { + if (!showLatestResourceOnly) { + return previewItems; + } + + const seenFileNames = new Set(); + return previewItems.filter((item) => { + const fileName = buildPreviewFileName(item); + + if (seenFileNames.has(fileName)) { + return false; + } + + seenFileNames.add(fileName); + return true; + }); + }, [previewItems, showLatestResourceOnly]); useEffect(() => { if (typeof document === 'undefined') { @@ -1150,8 +1178,19 @@ export function ChatConversationView({ {isResourceStripOpen ? (
{previewItems.length > 0 ? ( -
- {previewItems.map((item) => ( + <> + +
+ {visiblePreviewItems.map((item) => ( - ))} -
+ ))} +
+ ) : ( 현재 대화에 바로 열 수 있는 리소스가 없습니다. diff --git a/src/app/main/tokenAccess.ts b/src/app/main/tokenAccess.ts index 285e24b..0cccdcd 100755 --- a/src/app/main/tokenAccess.ts +++ b/src/app/main/tokenAccess.ts @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; export const TOKEN_ACCESS_STORAGE_KEY = 'work-app.token-access.registered-token'; export const TOKEN_ACCESS_SYNC_EVENT = 'work-app:token-access-changed'; -export const ALLOWED_REGISTRATION_TOKEN = 'usr_7f3a9c2d8e1b4a6f'; +export const ALLOWED_REGISTRATION_TOKEN = + import.meta.env.VITE_ALLOWED_REGISTRATION_TOKEN?.trim() || 'usr_7f3a9c2d8e1b4a6f'; function normalizeToken(value: string | null | undefined) { return value?.trim() ?? ''; diff --git a/src/styles.css b/src/styles.css index 81a5798..9be81bd 100755 --- a/src/styles.css +++ b/src/styles.css @@ -992,6 +992,12 @@ button, border-color: rgba(22, 93, 255, 0.22); } +.chat-source-changes-page__list-title.ant-typography, +.chat-source-changes-page__detail-title.ant-typography { + margin: 0; + word-break: break-word; +} + .chat-source-changes-page__detail { width: 100%; min-width: 0; @@ -1628,6 +1634,16 @@ button, } @media (max-width: 640px) { + .chat-source-changes-page__list-title.ant-typography { + font-size: 13px; + line-height: 1.45; + } + + .chat-source-changes-page__detail-title.ant-typography { + font-size: 16px; + line-height: 1.35; + } + .release-review-page__grid { gap: 12px; } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 64251fb..5acc4db 100755 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,2 +1,10 @@ /// /// + +interface ImportMetaEnv { + readonly VITE_ALLOWED_REGISTRATION_TOKEN?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}