diff --git a/.github/workflows/android-release.yaml b/.github/workflows/android-release.yaml new file mode 100644 index 00000000..5eec486b --- /dev/null +++ b/.github/workflows/android-release.yaml @@ -0,0 +1,127 @@ +name: Android Build & Release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + release: + types: + - created + +jobs: + build: + name: Build Android App + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: write + defaults: + run: + working-directory: RxCodeAndroid + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Write Firebase config + env: + FIREBASE_ANDROID_B64: ${{ secrets.FIREBASE_ANDROID_B64 }} + # The shared script writes RxCodeAndroid/app/google-services.json from + # the secret. Without it the build silently skips the Firebase plugins. + run: ../scripts/ci/write-firebase-config.sh + + - name: Decode upload keystore + id: keystore + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + # Optional: PRs from forks won't have the secret, so the build falls + # back to debug signing (see app/build.gradle.kts). Only release events + # strictly require it (enforced below). + run: | + if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then + echo "No ANDROID_KEYSTORE_BASE64 secret; release build will use debug signing." + exit 0 + fi + KS_PATH="$RUNNER_TEMP/rxcode-upload.jks" + echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$KS_PATH" + echo "path=$KS_PATH" >> "$GITHUB_OUTPUT" + + - name: Resolve version (release only) + if: github.event_name == 'release' + # versionName = release tag without leading 'v'; versionCode = monotonic + # CI run number so every published bundle is strictly increasing. + run: | + VERSION_NAME="${GITHUB_REF_NAME#v}" + echo "ANDROID_VERSION_NAME=${VERSION_NAME}" >> "$GITHUB_ENV" + echo "ANDROID_VERSION_CODE=${{ github.run_number }}" >> "$GITHUB_ENV" + + - name: Require keystore for release + if: github.event_name == 'release' && steps.keystore.outputs.path == '' + run: | + echo "::error::ANDROID_KEYSTORE_BASE64 is required to publish a release." + exit 1 + + - name: Build release AAB + env: + ANDROID_KEYSTORE_PATH: ${{ steps.keystore.outputs.path }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: ./gradlew :app:bundleRelease --no-daemon --stacktrace + + - name: Locate AAB + id: aab + run: | + AAB_PATH="$(find app/build/outputs/bundle/release -name '*.aab' | head -n1)" + if [ -z "$AAB_PATH" ]; then + echo "::error::No AAB produced." + exit 1 + fi + echo "path=$(pwd)/$AAB_PATH" >> "$GITHUB_OUTPUT" + echo "Built $AAB_PATH" + + # Smoke-test artifact for pushes/PRs (1 day retention) + - name: Upload AAB artifact (non-release) + if: github.event_name != 'release' + uses: actions/upload-artifact@v7 + with: + name: RxCode-Android-${{ github.run_number }} + path: ${{ steps.aab.outputs.path }} + retention-days: 1 + + - name: Write Play service account + if: github.event_name == 'release' + env: + PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }} + run: | + if [ -z "$PLAY_SERVICE_ACCOUNT_JSON" ]; then + echo "::error::PLAY_SERVICE_ACCOUNT_JSON is required to publish to Google Play." + exit 1 + fi + printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > "$RUNNER_TEMP/play-service-account.json" + + - name: Publish to Google Play (internal track) + if: github.event_name == 'release' + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJson: ${{ runner.temp }}/play-service-account.json + packageName: app.rxlab.rxcode + releaseFiles: ${{ steps.aab.outputs.path }} + track: internal + status: completed + + - name: Attach AAB to GitHub Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v3 + with: + files: ${{ steps.aab.outputs.path }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Packages/Sources/RxCodeChatKit/Resources/Localizable.xcstrings b/Packages/Sources/RxCodeChatKit/Resources/Localizable.xcstrings index 86cb5f50..79f05939 100644 --- a/Packages/Sources/RxCodeChatKit/Resources/Localizable.xcstrings +++ b/Packages/Sources/RxCodeChatKit/Resources/Localizable.xcstrings @@ -13,6 +13,12 @@ }, " · " : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : " · " + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -23,6 +29,12 @@ }, "--" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "--" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -33,6 +45,12 @@ }, "·" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "·" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -43,6 +61,12 @@ }, "(no output)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "(출력 없음)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -53,6 +77,12 @@ }, "/" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "/" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -63,6 +93,12 @@ }, "%@ — " : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ — " + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -79,6 +115,12 @@ "value" : "%1$@ %2$@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -89,6 +131,12 @@ }, "%@ in progress" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 진행 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -99,6 +147,12 @@ }, "%lld" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -131,6 +185,12 @@ }, "%lld messages queued" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 메시지 %lld개" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -191,6 +251,12 @@ "value" : "%1$lld/%2$lld" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld/%lld" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -201,6 +267,12 @@ }, "%lld%%" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld%%" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -217,6 +289,12 @@ "value" : "+%d more pending" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "+%d개 더 대기 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -226,10 +304,29 @@ } }, "⚠︎ %@" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠︎ %@" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "⚠︎ %@" + } + } + } }, "$" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "$" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -356,6 +453,12 @@ "value" : "Accept + Auto" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "수락 + 자동" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -372,6 +475,12 @@ "value" : "Accept Ask" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "질문 수락" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -382,6 +491,12 @@ }, "Accept Edits" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "편집 수락" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -480,6 +595,12 @@ }, "Add — attach file or toggle plan mode" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "추가 — 파일 첨부 또는 계획 모드 전환" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -578,6 +699,12 @@ }, "Agent" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -589,6 +716,12 @@ "Agent asked %d questions" : { "comment" : "Inline summary when the assistant invokes AskUserQuestion with multiple questions. %d is the number of questions.", "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트가 질문 %d개를 했습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -599,6 +732,12 @@ }, "Agent asked a question" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트가 질문했습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -615,6 +754,12 @@ "value" : "agentProvider: agent this command applies to (claudeCode, codex, acp). Use null or omit it for all agents." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "agentProvider: 이 명령이 적용되는 에이전트 (claudeCode, codex, acp). 모든 에이전트에 적용하려면 null을 사용하거나 생략하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -625,6 +770,12 @@ }, "All Agents" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 에이전트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -635,6 +786,12 @@ }, "All Agents (Global)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 에이전트 (전역)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -733,6 +890,12 @@ }, "Answered" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "답변됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -743,6 +906,12 @@ }, "Assistant: %@" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "어시스턴트: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -759,6 +928,12 @@ "value" : "Attach file…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 첨부…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -791,6 +966,12 @@ }, "Bash" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bash" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -823,6 +1004,12 @@ }, "Built-in commands are Claude Code only. Custom commands can target one agent or all." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "내장 명령은 Claude Code 전용입니다. 사용자 지정 명령은 하나의 에이전트 또는 전체를 대상으로 할 수 있습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -921,6 +1108,12 @@ }, "Client: %@" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "클라이언트: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -959,6 +1152,12 @@ "value" : "Close — you can review later from the banner" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닫기 — 나중에 배너에서 검토할 수 있습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -991,6 +1190,12 @@ }, "Combine and send all queued messages as a single turn" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 모든 메시지를 한 번에 합쳐서 보내기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1441,6 +1646,12 @@ }, "Copy output" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "출력 복사" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1457,6 +1668,12 @@ "value" : "Copy plan" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 복사" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1583,6 +1800,12 @@ "value" : "Decided" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "결정됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1857,6 +2080,12 @@ }, "Drafting plan…" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 작성 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2197,6 +2426,12 @@ }, "error" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "오류" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2405,6 +2640,12 @@ }, "Files" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2525,6 +2766,12 @@ }, "Image not available" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이미지를 사용할 수 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2535,6 +2782,12 @@ }, "Image unavailable" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이미지를 사용할 수 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2940,7 +3193,20 @@ } }, "json" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "json" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "json" + } + } + } }, "List available skills" : { "localizations" : { @@ -3427,7 +3693,20 @@ } }, "Next change" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 변경" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一处更改" + } + } + } }, "No changes" : { "localizations" : { @@ -3503,6 +3782,12 @@ "value" : "No todos" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "할 일 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3513,6 +3798,12 @@ }, "ok" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3567,6 +3858,12 @@ }, "Open the plan to accept or reject" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획을 열어 수락하거나 거부하세요" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3577,6 +3874,12 @@ }, "Open the plan to review the decision" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획을 열어 결정을 검토하세요" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3703,6 +4006,12 @@ "value" : "Plan content unavailable." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 내용을 사용할 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3713,6 +4022,12 @@ }, "Plan is still drafting…" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획을 아직 작성 중입니다…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3729,6 +4044,12 @@ "value" : "Plan mode" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 모드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3739,6 +4060,12 @@ }, "Plan mode is on — Add menu" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 모드가 켜져 있습니다 — 추가 메뉴" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3749,6 +4076,12 @@ }, "Plan ready" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 준비됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3802,7 +4135,20 @@ } }, "Previous change" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이전 변경" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "上一处更改" + } + } + } }, "Privacy settings (Pro/Max)" : { "localizations" : { @@ -3922,6 +4268,12 @@ "value" : "Reject" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "거부" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3938,6 +4290,12 @@ "value" : "Reject with Reason" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사유와 함께 거부" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3992,6 +4350,12 @@ }, "Remove" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제거" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4134,6 +4498,12 @@ }, "Response in progress" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "응답 진행 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4188,6 +4558,12 @@ }, "Review" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "검토" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4484,6 +4860,12 @@ }, "Send all as one" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "하나로 모두 보내기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4500,6 +4882,12 @@ "value" : "Send feedback" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "피드백 보내기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4510,6 +4898,12 @@ }, "Send now — cancels current response" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "지금 보내기 — 현재 응답 취소" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4784,6 +5178,12 @@ }, "Shortcuts" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "단축키" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4970,6 +5370,12 @@ }, "Start in plan mode" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 모드로 시작" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5023,10 +5429,36 @@ } }, "Switch to horizontal scroll" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "가로 스크롤로 전환" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换到水平滚动" + } + } + } }, "Switch to wrap" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "줄 바꿈으로 전환" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "切换到自动换行" + } + } + } }, "Tell Claude what to change" : { "localizations" : { @@ -5036,6 +5468,12 @@ "value" : "Tell Claude what to change" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude에게 변경할 내용을 알려주세요" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5140,6 +5578,12 @@ "value" : "Todos" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "할 일" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5304,6 +5748,12 @@ }, "Tracks usage against Codex's rolling 5-hour limit. Resets gradually as older requests age out." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codex의 롤링 5시간 한도에 대한 사용량을 추적합니다. 오래된 요청이 만료되면서 점진적으로 재설정됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5314,6 +5764,12 @@ }, "Tracks usage against Codex's rolling 7-day limit. Resets gradually as older requests age out." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codex의 롤링 7일 한도에 대한 사용량을 추적합니다. 오래된 요청이 만료되면서 점진적으로 재설정됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5478,6 +5934,12 @@ }, "Use the Add menu or Shift-Tab to ask the agent for a read-only plan before edits begin." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "추가 메뉴나 Shift-Tab을 사용하여 편집을 시작하기 전에 에이전트에게 읽기 전용 계획을 요청하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5510,6 +5972,12 @@ }, "View" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5608,6 +6076,12 @@ }, "Waiting for answer" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "답변 대기 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5624,6 +6098,12 @@ "value" : "What should we build in %@?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에서 무엇을 만들까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5640,6 +6120,12 @@ "value" : "What would you like changed?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "무엇을 변경하시겠습니까?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", diff --git a/RxCode/App/AppState+PullRequest.swift b/RxCode/App/AppState+PullRequest.swift index cbf71a3b..fbbd54a5 100644 --- a/RxCode/App/AppState+PullRequest.swift +++ b/RxCode/App/AppState+PullRequest.swift @@ -80,6 +80,16 @@ extension AppState { return url } + /// Convenience for the project context menu, which has no branch in hand: + /// resolve the project's current branch (the same way the CI poller does) + /// and open a PR for it via ``createPullRequestForBranch(project:branch:)``. + func createPullRequestForCurrentBranch(project: Project) async throws -> URL { + guard let branch = await GitHelper.currentBranch(at: project.path), !branch.isEmpty else { + throw PullRequestError.createFailed("Couldn't determine the current branch for this project.") + } + return try await createPullRequestForBranch(project: project, branch: branch) + } + // MARK: - Title / body generation /// Generate raw PR text (title on the first line, blank line, then a markdown diff --git a/RxCode/Resources/Localizable.xcstrings b/RxCode/Resources/Localizable.xcstrings index 47066cf8..e20ca436 100644 --- a/RxCode/Resources/Localizable.xcstrings +++ b/RxCode/Resources/Localizable.xcstrings @@ -3963,6 +3963,22 @@ } } }, + "Create Pull Request" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트 생성" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建拉取请求" + } + } + } + }, "Create Release" : { "localizations" : { "ko" : { @@ -4059,6 +4075,22 @@ } } }, + "Creating Pull Request…" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트 생성 중…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在创建拉取请求…" + } + } + } + }, "Creating…" : { "localizations" : { "ko" : { @@ -9725,6 +9757,7 @@ } }, "Open in GitHub" : { + "extractionState" : "stale", "localizations" : { "ko" : { "stringUnit" : { @@ -9868,6 +9901,22 @@ } } }, + "Open Repository" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장소 열기" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开仓库" + } + } + } + }, "Open RxCode" : { "localizations" : { "ko" : { @@ -13014,7 +13063,20 @@ } }, "Set Up Docs" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 설정" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置文档" + } + } + } }, "Set Up Docs Search" : { "extractionState" : "stale", @@ -15422,7 +15484,20 @@ } }, "You're already in this setup chat" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이미 이 설정 대화에 있습니다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你已在此设置对话中" + } + } + } } }, "version" : "1.1" diff --git a/RxCode/Views/Sidebar/ProjectListView.swift b/RxCode/Views/Sidebar/ProjectListView.swift index a04d6f04..69db88a4 100644 --- a/RxCode/Views/Sidebar/ProjectListView.swift +++ b/RxCode/Views/Sidebar/ProjectListView.swift @@ -9,6 +9,36 @@ struct ProjectListView: View { @State private var projectToDelete: Project? = nil @State private var projectToRename: Project? = nil @State private var renameText: String = "" + @State private var creatingPRProjectId: UUID? = nil + @State private var prError: PRErrorAlert? = nil + + private struct PRErrorAlert: Identifiable { + let id = UUID() + let message: String + } + + /// Mirror the briefing card's gate: offer "Create PR" only for a + /// GitHub-linked project whose current branch has no PR yet. + private func canCreatePR(_ project: Project) -> Bool { + guard project.gitHubRepo != nil else { return false } + let _ = appState.ciStatusRevision + return appState.ciStatusByProject[project.id]?.pullRequestState == nil + } + + /// Push the current branch and open a PR for it, then reveal it in the browser. + private func startCreatePR(_ project: Project) { + guard creatingPRProjectId == nil else { return } + creatingPRProjectId = project.id + Task { @MainActor in + defer { creatingPRProjectId = nil } + do { + let url = try await appState.createPullRequestForCurrentBranch(project: project) + NSWorkspace.shared.open(url) + } catch { + prError = PRErrorAlert(message: error.localizedDescription) + } + } + } var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -20,6 +50,16 @@ struct ProjectListView: View { projectRow(project) .tag(project.id) .contextMenu { + if canCreatePR(project) { + Button { startCreatePR(project) } label: { + Label( + creatingPRProjectId == project.id ? "Creating Pull Request…" : "Create Pull Request", + systemImage: "arrow.triangle.pull" + ) + } + .disabled(creatingPRProjectId == project.id) + Divider() + } let hookItems = appState.projectContextMenuItems(for: project) if !hookItems.isEmpty { HookContextMenuItems(items: hookItems) @@ -65,6 +105,13 @@ struct ProjectListView: View { Task { await appState.renameProject(project, to: renameText) } } } + .alert(item: $prError) { error in + Alert( + title: Text("Couldn't Create Pull Request"), + message: Text(error.message), + dismissButton: .default(Text("OK")) + ) + } } } diff --git a/RxCode/Views/Sidebar/ProjectTreeView.swift b/RxCode/Views/Sidebar/ProjectTreeView.swift index 61819c33..e355fa18 100644 --- a/RxCode/Views/Sidebar/ProjectTreeView.swift +++ b/RxCode/Views/Sidebar/ProjectTreeView.swift @@ -295,6 +295,8 @@ struct ProjectTreeView: View { // MARK: - ProjectTreeRow private struct ProjectTreeRow: View { + @Environment(AppState.self) private var appState + let project: Project @Binding var isExpanded: Bool let isSelected: Bool @@ -308,6 +310,22 @@ private struct ProjectTreeRow: View { @State private var isHovered = false @State private var showLocationPopover = false @State private var hoverTask: Task? + @State private var creatingPR = false + @State private var prError: PRErrorAlert? + + private struct PRErrorAlert: Identifiable { + let id = UUID() + let message: String + } + + /// Mirror the briefing card's gate: offer "Create PR" only for a + /// GitHub-linked project whose current branch has no PR yet. Reading + /// `ciStatusRevision` keeps the menu in sync as the CI poller updates. + private var canCreatePR: Bool { + guard project.gitHubRepo != nil else { return false } + let _ = appState.ciStatusRevision + return appState.ciStatusByProject[project.id]?.pullRequestState == nil + } private var displayPath: String { let home = FileManager.default.homeDirectoryForCurrentUser.path @@ -413,6 +431,13 @@ private struct ProjectTreeRow: View { .contextMenu { projectMenuItems } + .alert(item: $prError) { error in + Alert( + title: Text("Couldn't Create Pull Request"), + message: Text(error.message), + dismissButton: .default(Text("OK")) + ) + } } /// Shared items for both the "More" (ellipsis) menu and the right-click @@ -425,6 +450,13 @@ private struct ProjectTreeRow: View { Button { onOpenInNewWindow() } label: { Label("Open in New Window", systemImage: "macwindow.badge.plus") } + if canCreatePR { + Button { startCreatePR() } label: { + Label(creatingPR ? "Creating Pull Request…" : "Create Pull Request", + systemImage: "arrow.triangle.pull") + } + .disabled(creatingPR) + } if !hookMenuItems.isEmpty { Divider() HookContextMenuItems(items: hookMenuItems) @@ -437,6 +469,22 @@ private struct ProjectTreeRow: View { Label("Delete Project", systemImage: "trash") } } + + /// Push the current branch and open a PR for it (mirrors the briefing card's + /// "Create PR" button), then reveal the new PR in the browser. + private func startCreatePR() { + guard !creatingPR else { return } + creatingPR = true + Task { @MainActor in + defer { creatingPR = false } + do { + let url = try await appState.createPullRequestForCurrentBranch(project: project) + NSWorkspace.shared.open(url) + } catch { + prError = PRErrorAlert(message: error.localizedDescription) + } + } + } } // MARK: - ProjectChatsList diff --git a/RxCodeAndroid/app/build.gradle.kts b/RxCodeAndroid/app/build.gradle.kts index 8606d5f7..e2bea595 100644 --- a/RxCodeAndroid/app/build.gradle.kts +++ b/RxCodeAndroid/app/build.gradle.kts @@ -12,6 +12,12 @@ if (file("google-services.json").exists()) { apply(plugin = "com.google.firebase.crashlytics") } +// Release signing is configured only when the upload keystore is available +// (CI decodes it from the ANDROID_KEYSTORE_BASE64 secret). Local/debug builds +// and PRs without the secret fall back to debug signing so the build still runs. +val releaseKeystorePath: String? = System.getenv("ANDROID_KEYSTORE_PATH") + ?: project.findProperty("android.keystore.path")?.toString() + android { namespace = "app.rxlab.rxcode" compileSdk = 36 @@ -20,12 +26,27 @@ android { applicationId = "app.rxlab.rxcode" minSdk = 26 targetSdk = 36 - versionCode = 1 - versionName = "1.0" + // versionName comes from the release tag and versionCode from the CI run + // number; both are overridable so local builds keep working with defaults. + versionCode = (System.getenv("ANDROID_VERSION_CODE") + ?: project.findProperty("android.versionCode")?.toString() ?: "1").toInt() + versionName = System.getenv("ANDROID_VERSION_NAME") + ?: project.findProperty("android.versionName")?.toString() ?: "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + if (releaseKeystorePath != null) { + create("release") { + storeFile = file(releaseKeystorePath) + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") + } + } + } + buildTypes { release { isMinifyEnabled = false @@ -33,6 +54,12 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + // Use the upload keystore in CI; fall back to debug signing locally. + signingConfig = if (releaseKeystorePath != null) { + signingConfigs.getByName("release") + } else { + signingConfigs.getByName("debug") + } } } compileOptions { diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Models.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Models.kt index f2071912..742887b8 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Models.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Models.kt @@ -145,3 +145,30 @@ data class ProjectBranchInfo( val currentBranch: String, val availableBranches: List? = null, ) + +/** + * Per-repo CI / pull-request status mirrored from the desktop, used to gate the + * "Create Pull Request" menu item (only offered when a branch has no PR yet). + * Mirrors a subset of Swift `ProjectCIStatus`; unknown keys (workflows, failing, + * etc.) are ignored by [RxJson]. + */ +@Serializable +data class ProjectCIStatus( + val owner: String = "", + val repo: String = "", + val branch: String? = null, + val found: Boolean = false, + val prNumber: Int? = null, + val prState: String? = null, + val prUrl: String? = null, +) + +/** + * One project's CI status, keyed by project id. Mirrors Swift + * `MobileProjectCIStatus` from the snapshot payload. + */ +@Serializable +data class MobileProjectCIStatus( + @Serializable(with = UuidSerializer::class) val projectId: UUID, + val status: ProjectCIStatus, +) diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt index e7ff8ef9..486fef8e 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt @@ -130,6 +130,48 @@ sealed class Payload { data class AutopilotResult(val data: AutopilotResultPayload) : Payload() { override val type = "autopilot_result" } + data class SkillCatalogRequest(val data: SkillCatalogRequestPayload) : Payload() { + override val type = "skill_catalog_request" + } + data class SkillCatalogResult(val data: SkillCatalogResultPayload) : Payload() { + override val type = "skill_catalog_result" + } + data class SkillMutationRequest(val data: SkillMutationRequestPayload) : Payload() { + override val type = "skill_mutation_request" + } + data class SkillMutationResult(val data: SkillMutationResultPayload) : Payload() { + override val type = "skill_mutation_result" + } + data class SkillSourceMutationRequest(val data: SkillSourceMutationRequestPayload) : Payload() { + override val type = "skill_source_mutation_request" + } + data class SkillSourceMutationResult(val data: SkillSourceMutationResultPayload) : Payload() { + override val type = "skill_source_mutation_result" + } + data class ACPRegistryRequest(val data: ACPRegistryRequestPayload) : Payload() { + override val type = "acp_registry_request" + } + data class ACPRegistryResult(val data: ACPRegistryResultPayload) : Payload() { + override val type = "acp_registry_result" + } + data class ACPMutationRequest(val data: ACPMutationRequestPayload) : Payload() { + override val type = "acp_mutation_request" + } + data class ACPMutationResult(val data: ACPMutationResultPayload) : Payload() { + override val type = "acp_mutation_result" + } + data class MCPConfigRequest(val data: MCPConfigRequestPayload) : Payload() { + override val type = "mcp_config_request" + } + data class MCPConfigResult(val data: MCPConfigResultPayload) : Payload() { + override val type = "mcp_config_result" + } + data class MCPMutationRequest(val data: MCPMutationRequestPayload) : Payload() { + override val type = "mcp_mutation_request" + } + data class MCPMutationResult(val data: MCPMutationResultPayload) : Payload() { + override val type = "mcp_mutation_result" + } data class Ping(val data: PingPayload) : Payload() { override val type = "ping" } @@ -177,6 +219,7 @@ data class SnapshotPayload( val branchBriefings: List? = null, val threadSummaries: List? = null, val projectBranches: List? = null, + val ciStatuses: List? = null, val activeSessionID: String? = null, val activeSessionMessages: List? = null, val activeSessionHasMore: Boolean? = null, @@ -531,6 +574,20 @@ object PayloadSerializer : KSerializer { "thread_changes_result" -> Payload.ThreadChangesResult(json.decodeFromJsonElement(ThreadChangesResultPayload.serializer(), data)) "autopilot_request" -> Payload.AutopilotRequest(json.decodeFromJsonElement(AutopilotRequestPayload.serializer(), data)) "autopilot_result" -> Payload.AutopilotResult(json.decodeFromJsonElement(AutopilotResultPayload.serializer(), data)) + "skill_catalog_request" -> Payload.SkillCatalogRequest(json.decodeFromJsonElement(SkillCatalogRequestPayload.serializer(), data)) + "skill_catalog_result" -> Payload.SkillCatalogResult(json.decodeFromJsonElement(SkillCatalogResultPayload.serializer(), data)) + "skill_mutation_request" -> Payload.SkillMutationRequest(json.decodeFromJsonElement(SkillMutationRequestPayload.serializer(), data)) + "skill_mutation_result" -> Payload.SkillMutationResult(json.decodeFromJsonElement(SkillMutationResultPayload.serializer(), data)) + "skill_source_mutation_request" -> Payload.SkillSourceMutationRequest(json.decodeFromJsonElement(SkillSourceMutationRequestPayload.serializer(), data)) + "skill_source_mutation_result" -> Payload.SkillSourceMutationResult(json.decodeFromJsonElement(SkillSourceMutationResultPayload.serializer(), data)) + "acp_registry_request" -> Payload.ACPRegistryRequest(json.decodeFromJsonElement(ACPRegistryRequestPayload.serializer(), data)) + "acp_registry_result" -> Payload.ACPRegistryResult(json.decodeFromJsonElement(ACPRegistryResultPayload.serializer(), data)) + "acp_mutation_request" -> Payload.ACPMutationRequest(json.decodeFromJsonElement(ACPMutationRequestPayload.serializer(), data)) + "acp_mutation_result" -> Payload.ACPMutationResult(json.decodeFromJsonElement(ACPMutationResultPayload.serializer(), data)) + "mcp_config_request" -> Payload.MCPConfigRequest(json.decodeFromJsonElement(MCPConfigRequestPayload.serializer(), data)) + "mcp_config_result" -> Payload.MCPConfigResult(json.decodeFromJsonElement(MCPConfigResultPayload.serializer(), data)) + "mcp_mutation_request" -> Payload.MCPMutationRequest(json.decodeFromJsonElement(MCPMutationRequestPayload.serializer(), data)) + "mcp_mutation_result" -> Payload.MCPMutationResult(json.decodeFromJsonElement(MCPMutationResultPayload.serializer(), data)) "ping" -> Payload.Ping(json.decodeFromJsonElement(PingPayload.serializer(), data)) "pong" -> Payload.Pong(json.decodeFromJsonElement(PongPayload.serializer(), data)) else -> Payload.Unknown(type, data) @@ -574,6 +631,20 @@ object PayloadSerializer : KSerializer { is Payload.ThreadChangesResult -> value.type to json.encodeToJsonElement(ThreadChangesResultPayload.serializer(), value.data) is Payload.AutopilotRequest -> value.type to json.encodeToJsonElement(AutopilotRequestPayload.serializer(), value.data) is Payload.AutopilotResult -> value.type to json.encodeToJsonElement(AutopilotResultPayload.serializer(), value.data) + is Payload.SkillCatalogRequest -> value.type to json.encodeToJsonElement(SkillCatalogRequestPayload.serializer(), value.data) + is Payload.SkillCatalogResult -> value.type to json.encodeToJsonElement(SkillCatalogResultPayload.serializer(), value.data) + is Payload.SkillMutationRequest -> value.type to json.encodeToJsonElement(SkillMutationRequestPayload.serializer(), value.data) + is Payload.SkillMutationResult -> value.type to json.encodeToJsonElement(SkillMutationResultPayload.serializer(), value.data) + is Payload.SkillSourceMutationRequest -> value.type to json.encodeToJsonElement(SkillSourceMutationRequestPayload.serializer(), value.data) + is Payload.SkillSourceMutationResult -> value.type to json.encodeToJsonElement(SkillSourceMutationResultPayload.serializer(), value.data) + is Payload.ACPRegistryRequest -> value.type to json.encodeToJsonElement(ACPRegistryRequestPayload.serializer(), value.data) + is Payload.ACPRegistryResult -> value.type to json.encodeToJsonElement(ACPRegistryResultPayload.serializer(), value.data) + is Payload.ACPMutationRequest -> value.type to json.encodeToJsonElement(ACPMutationRequestPayload.serializer(), value.data) + is Payload.ACPMutationResult -> value.type to json.encodeToJsonElement(ACPMutationResultPayload.serializer(), value.data) + is Payload.MCPConfigRequest -> value.type to json.encodeToJsonElement(MCPConfigRequestPayload.serializer(), value.data) + is Payload.MCPConfigResult -> value.type to json.encodeToJsonElement(MCPConfigResultPayload.serializer(), value.data) + is Payload.MCPMutationRequest -> value.type to json.encodeToJsonElement(MCPMutationRequestPayload.serializer(), value.data) + is Payload.MCPMutationResult -> value.type to json.encodeToJsonElement(MCPMutationResultPayload.serializer(), value.data) is Payload.Ping -> value.type to json.encodeToJsonElement(PingPayload.serializer(), value.data) is Payload.Pong -> value.type to json.encodeToJsonElement(PongPayload.serializer(), value.data) is Payload.Unknown -> value.type to (value.raw ?: JsonObject(emptyMap())) diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/RemoteConfigPayloads.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/RemoteConfigPayloads.kt new file mode 100644 index 00000000..64897d84 --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/RemoteConfigPayloads.kt @@ -0,0 +1,235 @@ +package app.rxlab.rxcode.proto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.util.UUID + +/** + * Skills / ACP / MCP remote-management payloads, mirroring the desktop's + * `Payload+RemoteManagement.swift`. Each round trip is correlated by + * `clientRequestID`; the desktop is the source of truth and replies with the + * authoritative list so mobile never re-derives state locally. + */ + +// MARK: - Skills + +/** One marketplace plugin flattened from the desktop, plus install state. */ +@Serializable +data class MobileSkillPlugin( + val id: String, + val name: String, + val summary: String, + val author: String, + val category: String, + val categoryLabel: String, + val marketplace: String, + val marketplaceLabel: String, + val homepage: String, + val isInstalled: Boolean, +) + +@Serializable +data class MobileSkillSource( + val id: String, + val displayName: String, +) + +@Serializable +data class SkillCatalogRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val forceRefresh: Boolean = false, +) + +@Serializable +data class SkillCatalogResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val ok: Boolean, + val errorMessage: String? = null, + val plugins: List = emptyList(), + val sources: List = emptyList(), +) + +@Serializable +data class SkillMutationRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val operation: Operation, + val pluginID: String, +) { + @Serializable + enum class Operation { + @SerialName("install") INSTALL, + @SerialName("uninstall") UNINSTALL, + } +} + +@Serializable +data class SkillMutationResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val operation: SkillMutationRequestPayload.Operation, + val pluginID: String, + val ok: Boolean, + val errorMessage: String? = null, + val plugins: List = emptyList(), + val sources: List = emptyList(), +) + +@Serializable +data class SkillSourceMutationRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val operation: Operation, + val sourceID: String? = null, + val gitURL: String? = null, + val ref: String? = null, +) { + @Serializable + enum class Operation { + @SerialName("add") ADD, + @SerialName("remove") REMOVE, + } +} + +@Serializable +data class SkillSourceMutationResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val operation: SkillSourceMutationRequestPayload.Operation, + val sourceID: String? = null, + val ok: Boolean, + val errorMessage: String? = null, + val plugins: List = emptyList(), + val sources: List = emptyList(), +) + +// MARK: - ACP agent clients + +@Serializable +data class MobileACPRegistryAgent( + val id: String, + val name: String, + val version: String, + val summary: String, + val authors: List = emptyList(), + val license: String? = null, + val website: String? = null, + val iconURL: String? = null, + val isInstalled: Boolean, + val hasBinary: Boolean, + val hasNpx: Boolean, + val hasUvx: Boolean, +) + +@Serializable +data class MobileACPClient( + val id: String, + val registryId: String? = null, + val displayName: String, + val enabled: Boolean, + val launchKind: String, + val modelCount: Int, + val iconURL: String? = null, +) + +@Serializable +data class ACPRegistryRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val forceRefresh: Boolean = false, +) + +@Serializable +data class ACPRegistryResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val ok: Boolean, + val errorMessage: String? = null, + val registryAgents: List = emptyList(), + val installedClients: List = emptyList(), +) + +@Serializable +data class ACPMutationRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val operation: Operation, + val registryAgentID: String? = null, + val clientID: String? = null, + val enabled: Boolean? = null, +) { + @Serializable + enum class Operation { + @SerialName("install") INSTALL, + @SerialName("uninstall") UNINSTALL, + @SerialName("setEnabled") SET_ENABLED, + } +} + +@Serializable +data class ACPMutationResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val operation: ACPMutationRequestPayload.Operation, + val ok: Boolean, + val errorMessage: String? = null, + val registryAgents: List = emptyList(), + val installedClients: List = emptyList(), +) + +// MARK: - MCP servers + +/** Plain key/value pair for MCP environment variables and headers. */ +@Serializable +data class MobileMCPKeyValue( + val key: String, + val value: String, +) + +/** One global MCP server flattened from the desktop's `MCPServerRecord`. */ +@Serializable +data class MobileMCPServer( + val name: String, + val transport: String, + val url: String? = null, + val command: String? = null, + val args: List = emptyList(), + val env: List = emptyList(), + val headers: List = emptyList(), + val isGloballyEnabled: Boolean, + val endpoint: String, +) { + /** Mirrors the iOS `id` computed property (servers are keyed by name). */ + val id: String get() = name +} + +@Serializable +data class MCPConfigRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), +) + +@Serializable +data class MCPConfigResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val ok: Boolean, + val errorMessage: String? = null, + val servers: List = emptyList(), +) + +@Serializable +data class MCPMutationRequestPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID = UUID.randomUUID(), + val operation: Operation, + val serverName: String, + val server: MobileMCPServer? = null, + val enabled: Boolean? = null, +) { + @Serializable + enum class Operation { + @SerialName("add") ADD, + @SerialName("remove") REMOVE, + @SerialName("setEnabled") SET_ENABLED, + } +} + +@Serializable +data class MCPMutationResultPayload( + @Serializable(with = UuidSerializer::class) val clientRequestID: UUID, + val operation: MCPMutationRequestPayload.Operation, + val serverName: String, + val ok: Boolean, + val errorMessage: String? = null, + val servers: List = emptyList(), +) diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt index e2ef661b..4f24e8a4 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt @@ -77,6 +77,15 @@ class MobileAppState @Inject constructor( private var pendingDeleteProjectId: UUID? = null private var searchJob: Job? = null + // Remote-config (MCP / ACP / Skills) request correlation, mirroring the + // iOS `MobileAppState+RemoteConfig` stored properties. Touched only from + // viewModelScope coroutines (Main dispatcher), so no extra synchronization. + private var pendingMCPConfigRequestID: UUID? = null + private var pendingACPRegistryRequestID: UUID? = null + private var pendingSkillCatalogRequestID: UUID? = null + private val acpMutationKeys = mutableMapOf() + private val skillSourceMutationKeys = mutableMapOf() + /** * Autopilot remote-management round-trips (mirrors the desktop's Autopilot * settings tab). Sends through the same encrypted [client]; replies are @@ -207,6 +216,34 @@ class MobileAppState @Inject constructor( if (!isActiveDesktop(fromHex)) return autopilot.handleResult(payload.data) } + is Payload.MCPConfigResult -> { + if (!isActiveDesktop(fromHex)) return + applyMCPConfigResult(payload.data) + } + is Payload.MCPMutationResult -> { + if (!isActiveDesktop(fromHex)) return + applyMCPMutationResult(payload.data) + } + is Payload.ACPRegistryResult -> { + if (!isActiveDesktop(fromHex)) return + applyACPRegistryResult(payload.data) + } + is Payload.ACPMutationResult -> { + if (!isActiveDesktop(fromHex)) return + applyACPMutationResult(payload.data) + } + is Payload.SkillCatalogResult -> { + if (!isActiveDesktop(fromHex)) return + applySkillCatalogResult(payload.data) + } + is Payload.SkillMutationResult -> { + if (!isActiveDesktop(fromHex)) return + applySkillMutationResult(payload.data) + } + is Payload.SkillSourceMutationResult -> { + if (!isActiveDesktop(fromHex)) return + applySkillSourceMutationResult(payload.data) + } is Payload.Ping -> { // Reply with pong to satisfy the desktop's liveness check. client.send(Payload.Pong(PongPayload()), fromHex) @@ -316,6 +353,9 @@ class MobileAppState @Inject constructor( projectBranches = snap.data.projectBranches ?.associateBy { it.projectId } ?: current.projectBranches, + ciStatusByProject = snap.data.ciStatuses + ?.associate { it.projectId to it.status } + ?: current.ciStatusByProject, runProfilesByProject = snap.data.runProfiles ?.associate { it.projectId to it.profiles } ?: current.runProfilesByProject, @@ -867,6 +907,419 @@ class MobileAppState @Inject constructor( _state.update { it.copy(threadChanges = result, isLoadingThreadChanges = false) } } + // MARK: - Remote desktop configuration (MCP / ACP / Skills) + // + // 1:1 with iOS `MobileAppState+RemoteConfig`/`+Inbound`: each request is + // correlated by `clientRequestID`; the desktop replies with the + // authoritative list. A `delay` timeout clears a stuck request and surfaces + // an error, exactly like iOS `scheduleTimeout`. + + // ---- MCP servers ---- + + fun requestMCPConfig() { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(mcpConfigError = "Connect a Mac to manage MCP servers.") } + return + } + val payload = app.rxlab.rxcode.proto.MCPConfigRequestPayload() + pendingMCPConfigRequestID = payload.clientRequestID + _state.update { it.copy(mcpConfigLoading = true, mcpConfigError = null) } + viewModelScope.launch { + val sent = client.send(Payload.MCPConfigRequest(payload), hex) + if (!sent) { + if (pendingMCPConfigRequestID == payload.clientRequestID) pendingMCPConfigRequestID = null + _state.update { it.copy(mcpConfigLoading = false, mcpConfigError = "Failed to request MCP servers.") } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (pendingMCPConfigRequestID == payload.clientRequestID) { + pendingMCPConfigRequestID = null + _state.update { it.copy(mcpConfigLoading = false, mcpConfigError = REMOTE_TIMEOUT_MESSAGE) } + } + } + } + + fun addMCPServer(server: app.rxlab.rxcode.proto.MobileMCPServer) = + mutateMCP(app.rxlab.rxcode.proto.MCPMutationRequestPayload.Operation.ADD, server.name, server = server) + + fun removeMCPServer(serverName: String) = + mutateMCP(app.rxlab.rxcode.proto.MCPMutationRequestPayload.Operation.REMOVE, serverName) + + fun setMCPServerEnabled(serverName: String, enabled: Boolean) = + mutateMCP( + app.rxlab.rxcode.proto.MCPMutationRequestPayload.Operation.SET_ENABLED, + serverName, + enabled = enabled, + ) + + private fun mutateMCP( + operation: app.rxlab.rxcode.proto.MCPMutationRequestPayload.Operation, + serverName: String, + server: app.rxlab.rxcode.proto.MobileMCPServer? = null, + enabled: Boolean? = null, + ) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(lastMCPError = "Connect a Mac first.") } + return + } + if (_state.value.inFlightMCPMutations.contains(serverName)) return + val payload = app.rxlab.rxcode.proto.MCPMutationRequestPayload( + operation = operation, + serverName = serverName, + server = server, + enabled = enabled, + ) + _state.update { + it.copy(inFlightMCPMutations = it.inFlightMCPMutations + serverName, lastMCPError = null) + } + viewModelScope.launch { + val sent = client.send(Payload.MCPMutationRequest(payload), hex) + if (!sent) { + _state.update { + it.copy( + inFlightMCPMutations = it.inFlightMCPMutations - serverName, + lastMCPError = "Failed to send request.", + ) + } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (_state.value.inFlightMCPMutations.contains(serverName)) { + _state.update { + it.copy( + inFlightMCPMutations = it.inFlightMCPMutations - serverName, + lastMCPError = REMOTE_TIMEOUT_MESSAGE, + ) + } + } + } + } + + private fun applyMCPConfigResult(result: app.rxlab.rxcode.proto.MCPConfigResultPayload) { + if (pendingMCPConfigRequestID != result.clientRequestID) return + pendingMCPConfigRequestID = null + _state.update { + it.copy( + mcpConfigLoading = false, + mcpServers = if (result.ok) result.servers else it.mcpServers, + mcpConfigError = if (result.ok) null else (result.errorMessage ?: "Failed to load MCP servers."), + ) + } + } + + private fun applyMCPMutationResult(result: app.rxlab.rxcode.proto.MCPMutationResultPayload) { + _state.update { + it.copy( + inFlightMCPMutations = it.inFlightMCPMutations - result.serverName, + mcpServers = result.servers, + lastMCPError = if (result.ok) null else (result.errorMessage ?: "MCP operation failed."), + ) + } + } + + // ---- ACP agent clients ---- + + fun requestACPRegistry(forceRefresh: Boolean = false) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(acpRegistryError = "Connect a Mac to manage agents.") } + return + } + val payload = app.rxlab.rxcode.proto.ACPRegistryRequestPayload(forceRefresh = forceRefresh) + pendingACPRegistryRequestID = payload.clientRequestID + _state.update { it.copy(acpRegistryLoading = true, acpRegistryError = null) } + viewModelScope.launch { + val sent = client.send(Payload.ACPRegistryRequest(payload), hex) + if (!sent) { + if (pendingACPRegistryRequestID == payload.clientRequestID) pendingACPRegistryRequestID = null + _state.update { it.copy(acpRegistryLoading = false, acpRegistryError = "Failed to request agents.") } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (pendingACPRegistryRequestID == payload.clientRequestID) { + pendingACPRegistryRequestID = null + _state.update { it.copy(acpRegistryLoading = false, acpRegistryError = REMOTE_TIMEOUT_MESSAGE) } + } + } + } + + fun installACPAgent(registryAgentID: String) = + mutateACP( + app.rxlab.rxcode.proto.ACPMutationRequestPayload.Operation.INSTALL, + key = registryAgentID, + registryAgentID = registryAgentID, + ) + + fun uninstallACPClient(clientID: String) = + mutateACP( + app.rxlab.rxcode.proto.ACPMutationRequestPayload.Operation.UNINSTALL, + key = clientID, + clientID = clientID, + ) + + fun setACPClientEnabled(clientID: String, enabled: Boolean) = + mutateACP( + app.rxlab.rxcode.proto.ACPMutationRequestPayload.Operation.SET_ENABLED, + key = clientID, + clientID = clientID, + enabled = enabled, + ) + + private fun mutateACP( + operation: app.rxlab.rxcode.proto.ACPMutationRequestPayload.Operation, + key: String, + registryAgentID: String? = null, + clientID: String? = null, + enabled: Boolean? = null, + ) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(lastACPError = "Connect a Mac first.") } + return + } + if (_state.value.inFlightACPMutations.contains(key)) return + val payload = app.rxlab.rxcode.proto.ACPMutationRequestPayload( + operation = operation, + registryAgentID = registryAgentID, + clientID = clientID, + enabled = enabled, + ) + acpMutationKeys[payload.clientRequestID] = key + _state.update { it.copy(inFlightACPMutations = it.inFlightACPMutations + key, lastACPError = null) } + val timeout = if (operation == app.rxlab.rxcode.proto.ACPMutationRequestPayload.Operation.INSTALL) { + ACP_INSTALL_TIMEOUT_MS + } else { + REMOTE_CONFIG_TIMEOUT_MS + } + viewModelScope.launch { + val sent = client.send(Payload.ACPMutationRequest(payload), hex) + if (!sent) { + acpMutationKeys.remove(payload.clientRequestID) + _state.update { + it.copy( + inFlightACPMutations = it.inFlightACPMutations - key, + lastACPError = "Failed to send request.", + ) + } + return@launch + } + delay(timeout) + if (acpMutationKeys.remove(payload.clientRequestID) != null) { + _state.update { + it.copy( + inFlightACPMutations = it.inFlightACPMutations - key, + lastACPError = REMOTE_TIMEOUT_MESSAGE, + ) + } + } + } + } + + private fun applyACPRegistryResult(result: app.rxlab.rxcode.proto.ACPRegistryResultPayload) { + if (pendingACPRegistryRequestID != result.clientRequestID) return + pendingACPRegistryRequestID = null + _state.update { + it.copy( + acpRegistryLoading = false, + acpRegistryAgents = if (result.ok) result.registryAgents else it.acpRegistryAgents, + acpInstalledClients = if (result.ok) result.installedClients else it.acpInstalledClients, + acpRegistryError = if (result.ok) null else (result.errorMessage ?: "Failed to load the agent registry."), + ) + } + } + + private fun applyACPMutationResult(result: app.rxlab.rxcode.proto.ACPMutationResultPayload) { + val key = acpMutationKeys.remove(result.clientRequestID) + _state.update { + it.copy( + inFlightACPMutations = if (key != null) it.inFlightACPMutations - key else it.inFlightACPMutations, + acpRegistryAgents = result.registryAgents, + acpInstalledClients = result.installedClients, + lastACPError = if (result.ok) null else (result.errorMessage ?: "Agent operation failed."), + ) + } + } + + // ---- Skills marketplace ---- + + fun requestSkillCatalog(forceRefresh: Boolean = false) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(skillCatalogError = "Connect a Mac to browse skills.") } + return + } + val payload = app.rxlab.rxcode.proto.SkillCatalogRequestPayload(forceRefresh = forceRefresh) + pendingSkillCatalogRequestID = payload.clientRequestID + _state.update { it.copy(skillCatalogLoading = true, skillCatalogError = null) } + viewModelScope.launch { + val sent = client.send(Payload.SkillCatalogRequest(payload), hex) + if (!sent) { + if (pendingSkillCatalogRequestID == payload.clientRequestID) pendingSkillCatalogRequestID = null + _state.update { it.copy(skillCatalogLoading = false, skillCatalogError = "Failed to request skills.") } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (pendingSkillCatalogRequestID == payload.clientRequestID) { + pendingSkillCatalogRequestID = null + _state.update { it.copy(skillCatalogLoading = false, skillCatalogError = REMOTE_TIMEOUT_MESSAGE) } + } + } + } + + fun installSkill(pluginID: String) = + mutateSkill(pluginID, app.rxlab.rxcode.proto.SkillMutationRequestPayload.Operation.INSTALL) + + fun uninstallSkill(pluginID: String) = + mutateSkill(pluginID, app.rxlab.rxcode.proto.SkillMutationRequestPayload.Operation.UNINSTALL) + + private fun mutateSkill( + pluginID: String, + operation: app.rxlab.rxcode.proto.SkillMutationRequestPayload.Operation, + ) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(lastSkillError = "Connect a Mac first.") } + return + } + if (_state.value.inFlightSkillMutations.contains(pluginID)) return + val payload = app.rxlab.rxcode.proto.SkillMutationRequestPayload(operation = operation, pluginID = pluginID) + _state.update { + it.copy(inFlightSkillMutations = it.inFlightSkillMutations + pluginID, lastSkillError = null) + } + viewModelScope.launch { + val sent = client.send(Payload.SkillMutationRequest(payload), hex) + if (!sent) { + _state.update { + it.copy( + inFlightSkillMutations = it.inFlightSkillMutations - pluginID, + lastSkillError = "Failed to send request.", + ) + } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (_state.value.inFlightSkillMutations.contains(pluginID)) { + _state.update { + it.copy( + inFlightSkillMutations = it.inFlightSkillMutations - pluginID, + lastSkillError = REMOTE_TIMEOUT_MESSAGE, + ) + } + } + } + } + + /** Add a custom marketplace Git source; [ref] is an optional branch/tag. */ + fun addSkillGitSource(url: String, ref: String?) { + val trimmedURL = url.trim() + if (trimmedURL.isEmpty()) { + _state.update { it.copy(lastSkillError = "Enter a GitHub repository URL.") } + return + } + val trimmedRef = ref?.trim()?.takeIf { it.isNotEmpty() } + mutateSkillSource( + key = "add:$trimmedURL", + operation = app.rxlab.rxcode.proto.SkillSourceMutationRequestPayload.Operation.ADD, + gitURL = trimmedURL, + ref = trimmedRef, + ) + } + + fun removeSkillGitSource(sourceID: String) = + mutateSkillSource( + key = sourceID, + operation = app.rxlab.rxcode.proto.SkillSourceMutationRequestPayload.Operation.REMOVE, + sourceID = sourceID, + ) + + private fun mutateSkillSource( + key: String, + operation: app.rxlab.rxcode.proto.SkillSourceMutationRequestPayload.Operation, + sourceID: String? = null, + gitURL: String? = null, + ref: String? = null, + ) { + val hex = _state.value.activeDesktopPubkey + if (hex.isEmpty()) { + _state.update { it.copy(lastSkillError = "Connect a Mac first.") } + return + } + if (_state.value.inFlightSkillSourceMutations.contains(key)) return + val payload = app.rxlab.rxcode.proto.SkillSourceMutationRequestPayload( + operation = operation, + sourceID = sourceID, + gitURL = gitURL, + ref = ref, + ) + skillSourceMutationKeys[payload.clientRequestID] = key + _state.update { + it.copy(inFlightSkillSourceMutations = it.inFlightSkillSourceMutations + key, lastSkillError = null) + } + viewModelScope.launch { + val sent = client.send(Payload.SkillSourceMutationRequest(payload), hex) + if (!sent) { + skillSourceMutationKeys.remove(payload.clientRequestID) + _state.update { + it.copy( + inFlightSkillSourceMutations = it.inFlightSkillSourceMutations - key, + lastSkillError = "Failed to send request.", + ) + } + return@launch + } + delay(REMOTE_CONFIG_TIMEOUT_MS) + if (skillSourceMutationKeys.remove(payload.clientRequestID) != null) { + _state.update { + it.copy( + inFlightSkillSourceMutations = it.inFlightSkillSourceMutations - key, + lastSkillError = REMOTE_TIMEOUT_MESSAGE, + ) + } + } + } + } + + private fun applySkillCatalogResult(result: app.rxlab.rxcode.proto.SkillCatalogResultPayload) { + if (pendingSkillCatalogRequestID != result.clientRequestID) return + pendingSkillCatalogRequestID = null + _state.update { + it.copy( + skillCatalogLoading = false, + skillCatalog = if (result.ok) result.plugins else it.skillCatalog, + skillSources = if (result.ok) result.sources else it.skillSources, + skillCatalogError = if (result.ok) null else (result.errorMessage ?: "Failed to load skills."), + ) + } + } + + private fun applySkillMutationResult(result: app.rxlab.rxcode.proto.SkillMutationResultPayload) { + _state.update { + it.copy( + inFlightSkillMutations = it.inFlightSkillMutations - result.pluginID, + skillCatalog = result.plugins, + skillSources = result.sources, + lastSkillError = if (result.ok) null else (result.errorMessage ?: "Skill operation failed."), + ) + } + } + + private fun applySkillSourceMutationResult(result: app.rxlab.rxcode.proto.SkillSourceMutationResultPayload) { + val key = skillSourceMutationKeys.remove(result.clientRequestID) + _state.update { + var next = it.inFlightSkillSourceMutations + if (key != null) next = next - key + if (result.sourceID != null) next = next - result.sourceID + it.copy( + inFlightSkillSourceMutations = next, + skillCatalog = result.plugins, + skillSources = result.sources, + lastSkillError = if (result.ok) null else (result.errorMessage ?: "Skill source operation failed."), + ) + } + } + // MARK: - Pairing fun beginPairing(token: PairingToken, displayName: String = defaultDisplayName()) { @@ -993,6 +1446,12 @@ class MobileAppState @Inject constructor( private const val SEARCH_DEBOUNCE_MS = 200L private const val SEARCH_LIMIT = 25 + // Remote-config (MCP/ACP/Skills) timeouts, mirroring iOS: ACP installs + // download a binary, so they get a much longer ceiling. + private const val REMOTE_CONFIG_TIMEOUT_MS = 20_000L + private const val ACP_INSTALL_TIMEOUT_MS = 90_000L + private const val REMOTE_TIMEOUT_MESSAGE = "Request timed out. Check your Mac and try again." + fun defaultDisplayName(): String = Build.MODEL.ifBlank { "Android" } fun defaultRelayUrl(): String = diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt index d94baa0d..9c57a7a8 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt @@ -2,14 +2,20 @@ package app.rxlab.rxcode.state import app.rxlab.rxcode.proto.ChatMessage import app.rxlab.rxcode.proto.DocsSearchHit +import app.rxlab.rxcode.proto.MobileACPClient +import app.rxlab.rxcode.proto.MobileACPRegistryAgent import app.rxlab.rxcode.proto.MobileBranchBriefing +import app.rxlab.rxcode.proto.MobileMCPServer import app.rxlab.rxcode.proto.MobileRunTaskSnapshot +import app.rxlab.rxcode.proto.MobileSkillPlugin +import app.rxlab.rxcode.proto.MobileSkillSource import app.rxlab.rxcode.proto.MobileThreadSummary import app.rxlab.rxcode.proto.MobileWebProxyInfo import app.rxlab.rxcode.proto.PendingQuestionPayload import app.rxlab.rxcode.proto.PermissionRequestPayload import app.rxlab.rxcode.proto.Project import app.rxlab.rxcode.proto.ProjectBranchInfo +import app.rxlab.rxcode.proto.ProjectCIStatus import app.rxlab.rxcode.proto.RunProfile import app.rxlab.rxcode.proto.SearchHit import app.rxlab.rxcode.proto.SessionSummary @@ -46,6 +52,8 @@ data class MobileState( val threadSummaries: List = emptyList(), /** Current + available branches per project, indexed by project id. */ val projectBranches: Map = emptyMap(), + /** CI / pull-request status per project (current branch), indexed by project id. */ + val ciStatusByProject: Map = emptyMap(), val pendingPermission: PermissionRequestPayload? = null, val pendingQuestions: List = emptyList(), @@ -93,6 +101,34 @@ data class MobileState( val searchDocHits: List = emptyList(), val searchProjectIDs: List = emptyList(), val isSearching: Boolean = false, + + // MARK: - MCP servers (Settings → MCP Servers), mirroring iOS MobileAppState. + val mcpServers: List = emptyList(), + val mcpConfigLoading: Boolean = false, + val mcpConfigError: String? = null, + /** Server names with an in-flight add/remove/toggle mutation. */ + val inFlightMCPMutations: Set = emptySet(), + val lastMCPError: String? = null, + + // MARK: - ACP agent clients (Settings → Agent Clients). + val acpRegistryAgents: List = emptyList(), + val acpInstalledClients: List = emptyList(), + val acpRegistryLoading: Boolean = false, + val acpRegistryError: String? = null, + /** Registry-agent / client ids with an in-flight install/uninstall/toggle. */ + val inFlightACPMutations: Set = emptySet(), + val lastACPError: String? = null, + + // MARK: - Skills marketplace (Settings → Skills). + val skillCatalog: List = emptyList(), + val skillSources: List = emptyList(), + val skillCatalogLoading: Boolean = false, + val skillCatalogError: String? = null, + /** Plugin ids with an in-flight install/uninstall. */ + val inFlightSkillMutations: Set = emptySet(), + /** Source keys ("add:" or source id) with an in-flight add/remove. */ + val inFlightSkillSourceMutations: Set = emptySet(), + val lastSkillError: String? = null, ) { val isPaired: Boolean get() = activeDesktopPubkey.isNotEmpty() diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/acp/AcpClientsScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/acp/AcpClientsScreen.kt new file mode 100644 index 00000000..673ec0fa --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/acp/AcpClientsScreen.kt @@ -0,0 +1,269 @@ +package app.rxlab.rxcode.ui.acp + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Memory +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.rxlab.rxcode.proto.MobileACPClient +import app.rxlab.rxcode.proto.MobileACPRegistryAgent +import app.rxlab.rxcode.state.MobileAppState +import app.rxlab.rxcode.ui.autopilot.AutopilotEmptyState +import app.rxlab.rxcode.ui.autopilot.AutopilotErrorRow +import app.rxlab.rxcode.ui.autopilot.AutopilotScaffold +import app.rxlab.rxcode.ui.autopilot.AutopilotSectionLabel +import coil.compose.AsyncImage + +/** + * Manage ACP agent clients on the paired desktop. 1:1 with iOS + * `MobileACPClientsView`: toggle/remove installed clients and install new ones + * from the agentclientprotocol.com registry. + */ +@Composable +fun AcpClientsScreen( + app: MobileAppState, + online: Boolean, + onExit: () -> Unit, +) { + val state by app.state.collectAsState() + var pendingUninstall by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + if (state.acpRegistryAgents.isEmpty() && state.acpInstalledClients.isEmpty()) { + app.requestACPRegistry() + } + } + + AutopilotScaffold( + title = "Agent Clients", + onBack = onExit, + actions = { + if (state.acpRegistryLoading) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + IconButton(onClick = { app.requestACPRegistry(forceRefresh = true) }, enabled = online) { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh") + } + } + }, + ) { modifier -> + val available = state.acpRegistryAgents.filter { !it.isInstalled } + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + state.acpRegistryError?.let { item { AutopilotErrorRow(it) } } + state.lastACPError?.let { item { AutopilotErrorRow(it) } } + + if (state.acpInstalledClients.isNotEmpty()) { + item { AutopilotSectionLabel("Installed") } + items(state.acpInstalledClients, key = { it.id }) { client -> + InstalledClientRow( + client = client, + inFlight = state.inFlightACPMutations.contains(client.id), + online = online, + onToggle = { app.setACPClientEnabled(client.id, it) }, + onRemove = { pendingUninstall = client }, + ) + } + } + + if (available.isNotEmpty()) { + item { AutopilotSectionLabel("Available in Registry") } + items(available, key = { it.id }) { agent -> + RegistryAgentRow( + agent = agent, + inFlight = state.inFlightACPMutations.contains(agent.id), + online = online, + onInstall = { app.installACPAgent(agent.id) }, + ) + } + } else if (state.acpInstalledClients.isEmpty() && + !state.acpRegistryLoading && + state.acpRegistryError == null + ) { + item { AutopilotEmptyState("No agents found.") } + } + } + } + + pendingUninstall?.let { client -> + AlertDialog( + onDismissRequest = { pendingUninstall = null }, + title = { Text("Remove agent?") }, + text = { Text("This removes ${client.displayName} and its downloaded binary from your Mac.") }, + confirmButton = { + TextButton(onClick = { + app.uninstallACPClient(client.id) + pendingUninstall = null + }) { Text("Remove", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { pendingUninstall = null }) { Text("Cancel") } + }, + ) + } +} + +@Composable +private fun AgentIcon(iconURL: String?) { + if (iconURL.isNullOrEmpty()) { + Icon( + Icons.Outlined.Memory, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(28.dp), + ) + } else { + AsyncImage( + model = iconURL, + contentDescription = null, + modifier = Modifier + .size(28.dp) + .clip(RoundedCornerShape(6.dp)), + ) + } +} + +@Composable +private fun InstalledClientRow( + client: MobileACPClient, + inFlight: Boolean, + online: Boolean, + onToggle: (Boolean) -> Unit, + onRemove: () -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + AgentIcon(client.iconURL) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text(client.displayName, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + "${client.launchKind} · ${client.modelCount} model${if (client.modelCount == 1) "" else "s"}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (inFlight) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Switch(checked = client.enabled, onCheckedChange = onToggle, enabled = online) + } + IconButton(onClick = onRemove) { + Icon(Icons.Outlined.Delete, contentDescription = "Remove", tint = MaterialTheme.colorScheme.error) + } + } + } +} + +@Composable +private fun RegistryAgentRow( + agent: MobileACPRegistryAgent, + inFlight: Boolean, + online: Boolean, + onInstall: () -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + AgentIcon(agent.iconURL) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text(agent.name, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + agent.version, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + if (agent.summary.isNotEmpty()) { + Text( + agent.summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + distributionLabel(agent)?.let { + Text(it, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } + if (inFlight) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + Text("Installing…", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + } else { + Button(onClick = onInstall, enabled = online, contentPadding = ButtonDefaults.ContentPadding) { + Text("Install") + } + } + } + } +} + +private fun distributionLabel(agent: MobileACPRegistryAgent): String? { + val kinds = buildList { + if (agent.hasBinary) add("binary") + if (agent.hasNpx) add("npx") + if (agent.hasUvx) add("uvx") + } + return kinds.takeIf { it.isNotEmpty() }?.joinToString(" · ") +} diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/autopilot/ProjectActionsMenu.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/autopilot/ProjectActionsMenu.kt index 43acd0fb..d034c201 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/autopilot/ProjectActionsMenu.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/autopilot/ProjectActionsMenu.kt @@ -60,6 +60,9 @@ private const val RELEASE_SETUP_PROMPT = * * @param branch current branch — enables "Create Pull Request" (hidden when null * or `"unknown"`) and seeds the release form's branch picker. + * @param prNumber the open PR number for [branch], or null when none exists. + * Mirrors the desktop briefing gate: "Create Pull Request" is only offered + * when the branch has no PR linked yet. * @param onOpenSession navigate to a chat after a setup action creates one. * @param onProjectDeleted called after confirming "Delete Project" so the host * can pop back (only reachable when [includeDeleteProject] is true). @@ -70,6 +73,7 @@ fun ProjectActionsMenu( branch: String?, branchInfo: ProjectBranchInfo?, viewModel: MobileAppState, + prNumber: Int? = null, includeDeleteProject: Boolean = false, onOpenSession: (String) -> Unit = {}, onProjectDeleted: () -> Unit = {}, @@ -77,7 +81,9 @@ fun ProjectActionsMenu( val scope = rememberCoroutineScope() val uriHandler = LocalUriHandler.current val hasRepo = !project.gitHubRepo.isNullOrEmpty() - val canCreatePR = branch != null && !branch.equals("unknown", ignoreCase = true) + // Offer "Create PR" for a real branch with no open PR yet (mirrors the + // desktop briefing PR button); once a PR exists "Open on GitHub" covers it. + val canCreatePR = branch != null && !branch.equals("unknown", ignoreCase = true) && prNumber == null var menuOpen by remember { mutableStateOf(false) } var status by remember(project.id) { mutableStateOf(null) } diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt index 32e4a1e7..5c2c55d0 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt @@ -126,6 +126,11 @@ fun BriefingDetailScreen( branch = groupKey.branch, branchInfo = branchInfo, viewModel = viewModel, + // Only this branch's PR counts — the synced CI status + // tracks the project's current branch (matches iOS). + prNumber = state.ciStatusByProject[groupKey.projectId] + ?.takeIf { branchInfo?.currentBranch == groupKey.branch } + ?.prNumber, onOpenSession = onOpenSession, ) } diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/mcp/McpServersScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/mcp/McpServersScreen.kt new file mode 100644 index 00000000..f23abb61 --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/mcp/McpServersScreen.kt @@ -0,0 +1,425 @@ +package app.rxlab.rxcode.ui.mcp + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.AlertDialog +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.rxlab.rxcode.proto.MobileMCPKeyValue +import app.rxlab.rxcode.proto.MobileMCPServer +import app.rxlab.rxcode.state.MobileAppState +import app.rxlab.rxcode.ui.autopilot.AutopilotEmptyState +import app.rxlab.rxcode.ui.autopilot.AutopilotErrorRow +import app.rxlab.rxcode.ui.autopilot.AutopilotScaffold +import app.rxlab.rxcode.ui.autopilot.AutopilotSectionLabel + +/** Local edit target: either adding a new server or editing an existing one. */ +private sealed interface McpForm { + data object Add : McpForm + data class Edit(val server: MobileMCPServer) : McpForm +} + +/** + * Manage the paired desktop's global MCP servers. 1:1 with iOS + * `MobileMCPServersView`: list with enable toggles + remove, plus an add/edit + * form that the desktop upserts by name. + */ +@Composable +fun McpServersScreen( + app: MobileAppState, + online: Boolean, + onExit: () -> Unit, +) { + val state by app.state.collectAsState() + var form by remember { mutableStateOf(null) } + var pendingRemoval by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + if (state.mcpServers.isEmpty()) app.requestMCPConfig() + } + + form?.let { target -> + BackHandler { form = null } + McpServerFormScreen( + existing = (target as? McpForm.Edit)?.server, + online = online, + onCancel = { form = null }, + onSave = { server -> + app.addMCPServer(server) + form = null + }, + ) + return + } + + AutopilotScaffold( + title = "MCP Servers", + onBack = onExit, + actions = { + IconButton(onClick = { app.requestMCPConfig() }, enabled = online) { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh") + } + IconButton(onClick = { form = McpForm.Add }, enabled = online) { + Icon(Icons.Outlined.Add, contentDescription = "Add MCP Server") + } + }, + ) { modifier -> + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + state.mcpConfigError?.let { item { AutopilotErrorRow(it) } } + state.lastMCPError?.let { item { AutopilotErrorRow(it) } } + + if (state.mcpServers.isEmpty()) { + item { + if (state.mcpConfigLoading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + Text("Loading…", style = MaterialTheme.typography.bodyMedium) + } + } else if (state.mcpConfigError == null) { + AutopilotEmptyState("No MCP servers configured. Tap + to add one.") + } + } + } else { + items(state.mcpServers, key = { it.name }) { server -> + McpServerRow( + server = server, + inFlight = state.inFlightMCPMutations.contains(server.name), + online = online, + onEdit = { form = McpForm.Edit(server) }, + onToggle = { app.setMCPServerEnabled(server.name, it) }, + onRemove = { pendingRemoval = server }, + ) + } + } + } + } + + pendingRemoval?.let { server -> + AlertDialog( + onDismissRequest = { pendingRemoval = null }, + title = { Text("Remove MCP server?") }, + text = { Text("This removes ${server.name} from your Mac's global MCP configuration.") }, + confirmButton = { + TextButton(onClick = { + app.removeMCPServer(server.name) + pendingRemoval = null + }) { Text("Remove", color = MaterialTheme.colorScheme.error) } + }, + dismissButton = { + TextButton(onClick = { pendingRemoval = null }) { Text("Cancel") } + }, + ) + } +} + +@Composable +private fun McpServerRow( + server: MobileMCPServer, + inFlight: Boolean, + online: Boolean, + onEdit: () -> Unit, + onToggle: (Boolean) -> Unit, + onRemove: () -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + onClick = { if (online) onEdit() }, + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + server.name, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + AssistChip( + onClick = {}, + enabled = false, + label = { Text(server.transport.uppercase(), style = MaterialTheme.typography.labelSmall) }, + colors = AssistChipDefaults.assistChipColors( + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) + } + if (server.endpoint.isNotEmpty()) { + Text( + server.endpoint, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + if (inFlight) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + Switch( + checked = server.isGloballyEnabled, + onCheckedChange = onToggle, + enabled = online, + ) + } + IconButton(onClick = onRemove) { + Icon( + Icons.Outlined.Delete, + contentDescription = "Remove", + tint = MaterialTheme.colorScheme.error, + ) + } + } + } +} + +/** Mutable holder for a key/value form row so edits recompose in place. */ +private class KeyValueDraft(key: String, value: String) { + var key by mutableStateOf(key) + var value by mutableStateOf(value) +} + +@Composable +private fun McpServerFormScreen( + existing: MobileMCPServer?, + online: Boolean, + onCancel: () -> Unit, + onSave: (MobileMCPServer) -> Unit, +) { + var name by remember { mutableStateOf(existing?.name ?: "") } + var transport by remember { mutableStateOf(existing?.transport ?: "stdio") } + var command by remember { mutableStateOf(existing?.command ?: "") } + var url by remember { mutableStateOf(existing?.url ?: "") } + val args = remember { mutableStateListOf().apply { existing?.args?.let { addAll(it) } } } + val env = remember { + mutableStateListOf().apply { + existing?.env?.forEach { add(KeyValueDraft(it.key, it.value)) } + } + } + val headers = remember { + mutableStateListOf().apply { + existing?.headers?.forEach { add(KeyValueDraft(it.key, it.value)) } + } + } + + val isStdio = transport == "stdio" + val canSave = name.trim().isNotEmpty() && + (if (isStdio) command.trim().isNotEmpty() else url.trim().isNotEmpty()) && + online + + AutopilotScaffold( + title = if (existing == null) "Add MCP Server" else "Edit MCP Server", + onBack = onCancel, + actions = { + TextButton( + enabled = canSave, + onClick = { + val cleanArgs = args.map { it.trim() }.filter { it.isNotEmpty() } + val cleanEnv = env.mapNotNull { kv -> + kv.key.trim().takeIf { it.isNotEmpty() }?.let { MobileMCPKeyValue(it, kv.value) } + } + val cleanHeaders = headers.mapNotNull { kv -> + kv.key.trim().takeIf { it.isNotEmpty() }?.let { MobileMCPKeyValue(it, kv.value) } + } + val trimmedCommand = command.trim() + val trimmedURL = url.trim() + val endpoint = if (isStdio) { + (listOf(trimmedCommand) + cleanArgs).filter { it.isNotEmpty() }.joinToString(" ") + } else { + trimmedURL + } + onSave( + MobileMCPServer( + name = name.trim(), + transport = transport, + url = if (isStdio) null else trimmedURL, + command = if (isStdio) trimmedCommand else null, + args = if (isStdio) cleanArgs else emptyList(), + env = if (isStdio) cleanEnv else emptyList(), + headers = if (isStdio) emptyList() else cleanHeaders, + isGloballyEnabled = existing?.isGloballyEnabled ?: true, + endpoint = endpoint, + ) + ) + }, + ) { Text("Save") } + }, + ) { modifier -> + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + AutopilotSectionLabel("Server") + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + singleLine = true, + enabled = existing == null, + modifier = Modifier.fillMaxWidth(), + ) + } + item { + AutopilotSectionLabel("Transport") + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf("stdio" to "stdio", "http" to "HTTP", "sse" to "SSE").forEach { (value, label) -> + FilterChip( + selected = transport == value, + onClick = { transport = value }, + label = { Text(label) }, + ) + } + } + } + + if (isStdio) { + item { + AutopilotSectionLabel("Command") + OutlinedTextField( + value = command, + onValueChange = { command = it }, + label = { Text("Command") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + item { AutopilotSectionLabel("Arguments") } + items(args.size) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = args[index], + onValueChange = { args[index] = it }, + label = { Text("Argument") }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = { args.removeAt(index) }) { + Icon(Icons.Outlined.Delete, contentDescription = "Remove argument") + } + } + } + item { + TextButton(onClick = { args.add("") }) { + Icon(Icons.Outlined.Add, contentDescription = null) + Text("Add Argument") + } + } + keyValueEditor("Environment Variables", "Add Variable", env) + } else { + item { + AutopilotSectionLabel("Endpoint") + OutlinedTextField( + value = url, + onValueChange = { url = it }, + label = { Text("URL") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + keyValueEditor("Headers", "Add Header", headers) + } + } + } +} + +/** Renders a titled list of editable key/value rows with add/remove. */ +private fun androidx.compose.foundation.lazy.LazyListScope.keyValueEditor( + title: String, + addLabel: String, + items: androidx.compose.runtime.snapshots.SnapshotStateList, +) { + item { AutopilotSectionLabel(title) } + items(items.size) { index -> + val draft = items[index] + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = draft.key, + onValueChange = { draft.key = it }, + label = { Text("Key") }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + OutlinedTextField( + value = draft.value, + onValueChange = { draft.value = it }, + label = { Text("Value") }, + singleLine = true, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = { items.removeAt(index) }) { + Icon(Icons.Outlined.Delete, contentDescription = "Remove") + } + } + } + item { + TextButton(onClick = { items.add(KeyValueDraft("", "")) }) { + Icon(Icons.Outlined.Add, contentDescription = null) + Text(addLabel) + } + } +} diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt index 79066402..9394abd7 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt @@ -125,6 +125,7 @@ fun SessionsScreen( branch = branchInfo?.currentBranch, branchInfo = branchInfo, viewModel = viewModel, + prNumber = state.ciStatusByProject[projectId]?.prNumber, includeDeleteProject = true, onOpenSession = onNewThread, onProjectDeleted = onBack, diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/settings/SettingsScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/settings/SettingsScreen.kt index 379446fb..be6a8878 100644 --- a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/settings/SettingsScreen.kt +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/settings/SettingsScreen.kt @@ -22,10 +22,14 @@ import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos import androidx.compose.material.icons.outlined.DesktopMac +import androidx.compose.material.icons.outlined.Dns +import androidx.compose.material.icons.outlined.Extension import androidx.compose.material.icons.outlined.Flight import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Memory import androidx.compose.material.icons.outlined.Router import androidx.compose.material.icons.outlined.Search +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.material3.AlertDialog import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChipDefaults @@ -87,16 +91,45 @@ fun SettingsScreen( val haptics = rememberHaptics() var unpairTarget by remember { mutableStateOf(null) } var showAutopilot by rememberSaveable { mutableStateOf(false) } + var showSkills by rememberSaveable { mutableStateOf(false) } + var showAcpClients by rememberSaveable { mutableStateOf(false) } + var showMcpServers by rememberSaveable { mutableStateOf(false) } + + val online = state.isPaired && + state.connectionState == RelayClient.ConnectionState.CONNECTED if (showAutopilot) { app.rxlab.rxcode.ui.autopilot.AutopilotNavHost( app = viewModel, - online = state.isPaired && - state.connectionState == RelayClient.ConnectionState.CONNECTED, + online = online, onExit = { showAutopilot = false }, ) return } + if (showSkills) { + app.rxlab.rxcode.ui.skills.SkillsMarketScreen( + app = viewModel, + online = online, + onExit = { showSkills = false }, + ) + return + } + if (showAcpClients) { + app.rxlab.rxcode.ui.acp.AcpClientsScreen( + app = viewModel, + online = online, + onExit = { showAcpClients = false }, + ) + return + } + if (showMcpServers) { + app.rxlab.rxcode.ui.mcp.McpServersScreen( + app = viewModel, + online = online, + onExit = { showMcpServers = false }, + ) + return + } Scaffold( containerColor = MaterialTheme.colorScheme.background, @@ -222,6 +255,41 @@ fun SettingsScreen( } } } + // Match the iOS MobileSettingsView ordering: Skills, Agent + // Clients, then MCP Servers. + item { + ConfigNavCard( + icon = Icons.Outlined.Extension, + title = "Skills", + subtitle = "Browse and install marketplace skills", + onClick = { + haptics.play(HapticEvent.LightTap) + showSkills = true + }, + ) + } + item { + ConfigNavCard( + icon = Icons.Outlined.Memory, + title = "Agent Clients", + subtitle = "Install and manage ACP agents", + onClick = { + haptics.play(HapticEvent.LightTap) + showAcpClients = true + }, + ) + } + item { + ConfigNavCard( + icon = Icons.Outlined.Dns, + title = "MCP Servers", + subtitle = "Configure global MCP servers", + onClick = { + haptics.play(HapticEvent.LightTap) + showMcpServers = true + }, + ) + } } // MARK: - About @@ -301,6 +369,63 @@ fun SettingsScreen( } } +/** + * A tappable Desktop-Configuration row (icon + title + subtitle + chevron), + * matching the Autopilot card. Used for the Skills / Agent Clients / MCP + * Servers entries that take over the screen with their own nav host. + */ +@Composable +private fun ConfigNavCard( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + onClick = onClick, + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(44.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon(icon, contentDescription = null) + } + } + Column(Modifier.weight(1f)) { + Text( + title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } + Icon( + Icons.AutoMirrored.Outlined.ArrowForwardIos, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(14.dp), + ) + } + } +} + @Composable private fun SectionLabel(text: String) { Text( diff --git a/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/skills/SkillsMarketScreen.kt b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/skills/SkillsMarketScreen.kt new file mode 100644 index 00000000..efb5123f --- /dev/null +++ b/RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/skills/SkillsMarketScreen.kt @@ -0,0 +1,351 @@ +package app.rxlab.rxcode.ui.skills + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.Button +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.rxlab.rxcode.proto.MobileSkillPlugin +import app.rxlab.rxcode.state.MobileAppState +import app.rxlab.rxcode.ui.autopilot.AutopilotEmptyState +import app.rxlab.rxcode.ui.autopilot.AutopilotErrorRow +import app.rxlab.rxcode.ui.autopilot.AutopilotScaffold +import app.rxlab.rxcode.ui.autopilot.AutopilotSectionLabel + +private const val FILTER_ALL = "All" +private const val FILTER_INSTALLED = "Installed" + +/** + * Browse the paired desktop's skill marketplace and install / remove skills + * remotely. 1:1 with iOS `MobileSkillMarketView`: search + filter, plugins + * grouped by category, and a custom Git-source sheet. + */ +@Composable +fun SkillsMarketScreen( + app: MobileAppState, + online: Boolean, + onExit: () -> Unit, +) { + val state by app.state.collectAsState() + var searchText by remember { mutableStateOf("") } + var selectedFilter by remember { mutableStateOf(FILTER_ALL) } + var filterMenuOpen by remember { mutableStateOf(false) } + var showGitSourceSheet by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + if (state.skillCatalog.isEmpty()) app.requestSkillCatalog() + } + + val availableMarketplaces = remember(state.skillCatalog) { + state.skillCatalog + .groupingBy { it.marketplaceLabel } + .eachCount() + .entries + .sortedByDescending { it.value } + .map { it.key } + } + + val filtered = remember(state.skillCatalog, searchText, selectedFilter) { + filterPlugins(state.skillCatalog, searchText, selectedFilter) + } + val categories = remember(filtered) { filtered.map { it.categoryLabel }.toSortedSet().toList() } + + AutopilotScaffold( + title = "Skills", + onBack = onExit, + actions = { + IconButton(onClick = { filterMenuOpen = true }) { + Icon(Icons.Outlined.FilterList, contentDescription = "Filter Skills") + } + DropdownMenu(expanded = filterMenuOpen, onDismissRequest = { filterMenuOpen = false }) { + FilterRow(FILTER_ALL, selectedFilter) { selectedFilter = FILTER_ALL; filterMenuOpen = false } + FilterRow(FILTER_INSTALLED, selectedFilter) { selectedFilter = FILTER_INSTALLED; filterMenuOpen = false } + availableMarketplaces.forEach { label -> + FilterRow(label, selectedFilter) { selectedFilter = label; filterMenuOpen = false } + } + } + IconButton(onClick = { showGitSourceSheet = true }, enabled = online) { + Icon(Icons.Outlined.Add, contentDescription = "Add Git Source") + } + if (state.skillCatalogLoading) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + } else { + IconButton(onClick = { app.requestSkillCatalog(forceRefresh = true) }, enabled = online) { + Icon(Icons.Outlined.Refresh, contentDescription = "Refresh") + } + } + }, + ) { modifier -> + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + OutlinedTextField( + value = searchText, + onValueChange = { searchText = it }, + label = { Text("Search skills") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + } + + state.skillCatalogError?.let { item { AutopilotErrorRow(it) } } + state.lastSkillError?.let { item { AutopilotErrorRow(it) } } + + if (state.skillCatalog.isEmpty()) { + item { + if (state.skillCatalogLoading) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + Text("Loading skills…", style = MaterialTheme.typography.bodyMedium) + } + } else if (state.skillCatalogError == null) { + AutopilotEmptyState("No skills found.") + } + } + } else if (filtered.isEmpty()) { + item { AutopilotEmptyState("No skills found.") } + } else { + categories.forEach { category -> + item(key = "section-$category") { AutopilotSectionLabel(category) } + val plugins = filtered + .filter { it.categoryLabel == category } + .sortedBy { it.name.lowercase() } + items(plugins, key = { it.id }) { plugin -> + SkillRow( + plugin = plugin, + inFlight = state.inFlightSkillMutations.contains(plugin.id), + online = online, + onInstall = { app.installSkill(plugin.id) }, + onRemove = { app.uninstallSkill(plugin.id) }, + ) + } + } + } + } + } + + if (showGitSourceSheet) { + SkillGitSourceSheet(app = app, online = online, onDismiss = { showGitSourceSheet = false }) + } +} + +@Composable +private fun FilterRow(label: String, selected: String, onClick: () -> Unit) { + DropdownMenuItem( + text = { Text(label) }, + onClick = onClick, + trailingIcon = { + if (selected == label) Icon(Icons.Outlined.Check, contentDescription = null) + }, + ) +} + +@Composable +private fun SkillRow( + plugin: MobileSkillPlugin, + inFlight: Boolean, + online: Boolean, + onInstall: () -> Unit, + onRemove: () -> Unit, +) { + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + ) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(plugin.name, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis) + if (plugin.summary.isNotEmpty()) { + Text( + plugin.summary, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + plugin.marketplaceLabel, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + when { + inFlight -> CircularProgressIndicator(modifier = Modifier.size(20.dp)) + plugin.isInstalled -> OutlinedButton(onClick = onRemove, enabled = online) { Text("Remove") } + else -> Button(onClick = onInstall, enabled = online) { Text("Install") } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SkillGitSourceSheet( + app: MobileAppState, + online: Boolean, + onDismiss: () -> Unit, +) { + val state by app.state.collectAsState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var gitUrl by remember { mutableStateOf("") } + var ref by remember { mutableStateOf("") } + + val addKey = "add:${gitUrl.trim()}" + val canAdd = gitUrl.trim().isNotEmpty() && + !state.inFlightSkillSourceMutations.contains(addKey) && + online + + ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) { + Column( + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Git Sources", style = MaterialTheme.typography.titleLarge) + OutlinedTextField( + value = gitUrl, + onValueChange = { gitUrl = it }, + label = { Text("https://github.com/owner/repo") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = ref, + onValueChange = { ref = it }, + label = { Text("main") }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + Text( + "Use a GitHub repository that exposes .claude-plugin/marketplace.json.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + state.lastSkillError?.let { AutopilotErrorRow(it) } + + Button( + onClick = { + app.addSkillGitSource(gitUrl, ref) + gitUrl = "" + ref = "" + }, + enabled = canAdd, + modifier = Modifier.fillMaxWidth(), + ) { + if (state.inFlightSkillSourceMutations.contains(addKey)) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + } else { + Text("Add Source") + } + } + + AutopilotSectionLabel("Custom Sources") + if (state.skillSources.isEmpty()) { + AutopilotEmptyState("No custom Git sources added.") + } else { + state.skillSources.forEach { source -> + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + source.displayName, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + if (state.inFlightSkillSourceMutations.contains(source.id)) { + CircularProgressIndicator(modifier = Modifier.size(18.dp)) + } else { + IconButton(onClick = { app.removeSkillGitSource(source.id) }) { + Icon( + Icons.Outlined.Delete, + contentDescription = "Remove ${source.displayName}", + tint = MaterialTheme.colorScheme.error, + ) + } + } + } + } + } + } + } +} + +/** Apply the marketplace/installed/search filters, mirroring the iOS logic. */ +private fun filterPlugins( + catalog: List, + searchText: String, + selectedFilter: String, +): List { + var plugins = catalog + when (selectedFilter) { + FILTER_ALL -> Unit + FILTER_INSTALLED -> plugins = plugins.filter { it.isInstalled } + else -> plugins = plugins.filter { it.marketplaceLabel == selectedFilter } + } + val query = searchText.trim().lowercase() + if (query.isEmpty()) return plugins + return plugins.filter { + it.name.lowercase().contains(query) || + it.summary.lowercase().contains(query) || + it.categoryLabel.lowercase().contains(query) || + it.marketplaceLabel.lowercase().contains(query) + } +} diff --git a/RxCodeMobile/Resources/Localizable.xcstrings b/RxCodeMobile/Resources/Localizable.xcstrings index 0730b4c5..a44116d6 100644 --- a/RxCodeMobile/Resources/Localizable.xcstrings +++ b/RxCodeMobile/Resources/Localizable.xcstrings @@ -5,13 +5,52 @@ }, "-- --port 3000" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "-- --port 3000" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "-- --port 3000" + } + } + } }, ".env" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : ".env" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : ".env" + } + } + } }, "#%lld" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "#%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "#%lld" + } + } + } }, "%@ · %lld model%@" : { "localizations" : { @@ -20,6 +59,18 @@ "state" : "new", "value" : "%1$@ · %2$lld model%3$@" } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ · 모델 %lld개" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ · %lld 个模型" + } } } }, @@ -31,6 +82,12 @@ "value" : "%@, Active" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@, 활성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -41,6 +98,12 @@ }, "%@. Tap to answer." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@. 답하려면 탭하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -51,6 +114,12 @@ }, "%@. Tap to review." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@. 검토하려면 탭하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -60,10 +129,29 @@ } }, "%lld" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld" + } + } + } }, "%lld active" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld개 활성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -74,6 +162,12 @@ }, "%lld active jobs" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활성 작업 %lld개" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -84,6 +178,12 @@ }, "%lld document(s)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 %lld개" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -100,6 +200,12 @@ "value" : "%1$lld environment(s), %2$lld file(s)" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "환경 %lld개, 파일 %lld개" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -110,6 +216,12 @@ }, "%lld outdated action(s)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "오래된 액션 %lld개" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -120,6 +232,12 @@ }, "%lld queued" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld개 대기 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -136,6 +254,12 @@ "value" : "%lld queued messages. Tap to view all." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 메시지 %lld개. 모두 보려면 탭하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -151,14 +275,52 @@ "state" : "new", "value" : "%1$lld/%2$lld" } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld/%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$lld/%2$lld" + } } } }, "•" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "•" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "•" + } + } + } }, "+%lld" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "+%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "+%lld" + } + } + } }, "+%lld more pending" : { "localizations" : { @@ -168,6 +330,12 @@ "value" : "+%lld more pending" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld개 더 대기 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -177,7 +345,20 @@ } }, "−%lld" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "−%lld" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "−%lld" + } + } + } }, "5-hour limit" : { "localizations" : { @@ -187,6 +368,12 @@ "value" : "5-hour limit" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "5시간 한도" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -203,6 +390,12 @@ "value" : "7-day limit" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "7일 한도" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -219,6 +412,12 @@ "value" : "A summary will appear after threads complete" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드가 완료되면 요약이 표시됩니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -229,6 +428,12 @@ }, "Account" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계정" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -245,6 +450,12 @@ "value" : "Action" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "작업" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -261,6 +472,12 @@ "value" : "Active" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -277,6 +494,12 @@ "value" : "Add" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -293,6 +516,12 @@ "value" : "Add a branch name after the prefix." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "접두사 뒤에 브랜치 이름을 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -309,6 +538,12 @@ "value" : "Add a project from your Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 프로젝트 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -325,6 +560,12 @@ "value" : "Add a project from your Mac to get started." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시작하려면 Mac에서 프로젝트를 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -335,6 +576,12 @@ }, "Add a QR code" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR 코드 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -351,6 +598,12 @@ "value" : "Add Argument" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인자 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -361,6 +614,12 @@ }, "Add Document" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -377,6 +636,12 @@ "value" : "Add Git Source" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git 소스 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -393,6 +658,12 @@ "value" : "Add MCP Server" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -409,6 +680,12 @@ "value" : "Add Project" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 추가" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -425,6 +702,12 @@ "value" : "Agent Clients" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트 클라이언트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -441,6 +724,12 @@ "value" : "Agent operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -457,6 +746,12 @@ "value" : "All" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전체" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -473,6 +768,12 @@ "value" : "All branches" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 브랜치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -489,6 +790,12 @@ "value" : "All projects" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 프로젝트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -505,6 +812,12 @@ "value" : "Allow" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "허용" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -521,6 +834,12 @@ "value" : "Answer" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "답변" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -537,6 +856,12 @@ "value" : "API keys stay on the Mac and are not synced to this device." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "API 키는 Mac에 저장되며 이 기기와 동기화되지 않습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -546,7 +871,20 @@ } }, "App.xcodeproj" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "App.xcodeproj" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "App.xcodeproj" + } + } + } }, "Archive" : { "localizations" : { @@ -556,6 +894,12 @@ "value" : "Archive" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보관" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -572,6 +916,12 @@ "value" : "Archive after %lld day" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld일 후 보관" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -588,6 +938,12 @@ "value" : "Archive after %lld days" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld일 후 보관" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -604,6 +960,12 @@ "value" : "Archive this thread?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 스레드를 보관할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -620,6 +982,12 @@ "value" : "Archived threads are hidden from the list. You can still find them on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보관된 스레드는 목록에서 숨겨집니다. Mac에서는 계속 찾을 수 있습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -636,6 +1004,12 @@ "value" : "Argument" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인자" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -652,6 +1026,12 @@ "value" : "Arguments" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인자" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -668,6 +1048,12 @@ "value" : "Assistant has a question" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "어시스턴트가 질문이 있습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -684,6 +1070,12 @@ "value" : "Attachment Auto-Preview" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "첨부 파일 자동 미리보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -700,6 +1092,12 @@ "value" : "Auto-archive" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "자동 보관" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -710,6 +1108,12 @@ }, "Automation" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "자동화" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -719,7 +1123,20 @@ } }, "Autopilot" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot" + } + } + } }, "Available in Registry" : { "localizations" : { @@ -729,6 +1146,12 @@ "value" : "Available in Registry" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "레지스트리에서 사용 가능" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -745,6 +1168,12 @@ "value" : "Bash" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bash" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -761,6 +1190,12 @@ "value" : "Bash Configuration" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bash 구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -771,6 +1206,12 @@ }, "Branch" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -780,7 +1221,20 @@ } }, "Branch + checkout" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 + 체크아웃" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "分支 + 检出" + } + } + } }, "Branch briefings collect recent thread summaries from your Mac into a quick mobile status view." : { "localizations" : { @@ -790,6 +1244,12 @@ "value" : "Branch briefings collect recent thread summaries from your Mac into a quick mobile status view." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 브리핑은 Mac의 최근 스레드 요약을 모바일용 빠른 상태 보기로 모읍니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -806,6 +1266,12 @@ "value" : "Branch name" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -822,6 +1288,12 @@ "value" : "Branch name cannot end with “/”." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 이름은 “/”로 끝날 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -838,6 +1310,12 @@ "value" : "Branch name is required." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 이름이 필요합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -854,6 +1332,12 @@ "value" : "Branch operation failed" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 작업에 실패했습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -870,6 +1354,12 @@ "value" : "Branch operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -880,6 +1370,12 @@ }, "Branch Ruleset (optional)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 규칙 집합 (선택 사항)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -896,6 +1392,12 @@ "value" : "Branches" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -912,6 +1414,12 @@ "value" : "Briefing" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브리핑" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -928,6 +1436,12 @@ "value" : "Briefings appear here after threads finish on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 스레드가 완료되면 브리핑이 여기에 표시됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -944,6 +1458,12 @@ "value" : "Browse" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "둘러보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -954,6 +1474,12 @@ }, "Browser Error" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브라우저 오류" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -970,6 +1496,12 @@ "value" : "Cancel" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "취소" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -986,6 +1518,12 @@ "value" : "Change the summarization provider from mobile; API keys and endpoint secrets stay on the Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모바일에서 요약 제공자를 변경하세요. API 키와 엔드포인트 시크릿은 Mac에 유지됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1002,6 +1540,12 @@ "value" : "Changes" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "변경사항" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1012,6 +1556,12 @@ }, "Checking account…" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계정 확인 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1028,6 +1578,12 @@ "value" : "Choose from Photos" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사진에서 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1044,6 +1600,12 @@ "value" : "Choose one" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "하나 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1060,6 +1622,12 @@ "value" : "Choose the thread agent" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 에이전트 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1069,10 +1637,29 @@ } }, "CI" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "CI" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "CI" + } + } + } }, "CI Auto-Update" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "CI 자동 업데이트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1089,6 +1676,12 @@ "value" : "Claude Code Usage" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Claude Code 사용량" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1105,6 +1698,12 @@ "value" : "Close" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닫기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1121,6 +1720,12 @@ "value" : "Close — answer later from the queue" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "닫기 — 나중에 대기열에서 답변" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1131,6 +1736,12 @@ }, "Close Browser" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브라우저 닫기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1141,6 +1752,12 @@ }, "Close PR" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "PR 닫기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1151,6 +1768,12 @@ }, "Close pull request?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트를 닫을까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1167,6 +1790,12 @@ "value" : "Codex Usage" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Codex 사용량" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1183,6 +1812,12 @@ "value" : "Command" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "커맨드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1199,6 +1834,12 @@ "value" : "Computer Status" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "컴퓨터 상태" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1215,6 +1856,12 @@ "value" : "Configuration" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1231,6 +1878,12 @@ "value" : "Connect a Mac first." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "먼저 Mac을 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1247,6 +1900,12 @@ "value" : "Connect a Mac to browse skills." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬을 둘러보려면 Mac을 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1263,6 +1922,12 @@ "value" : "Connect a Mac to manage agents." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트를 관리하려면 Mac을 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1279,6 +1944,12 @@ "value" : "Connect a Mac to manage MCP servers." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버를 관리하려면 Mac을 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1289,6 +1960,12 @@ }, "Connect an online Mac to use Autopilot." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot을 사용하려면 온라인 상태의 Mac을 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1305,6 +1982,12 @@ "value" : "Connect to" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결 대상" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1315,6 +1998,12 @@ }, "Connect to an online Mac to manage Autopilot." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot을 관리하려면 온라인 상태의 Mac에 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1331,6 +2020,12 @@ "value" : "Connect to your Mac and try again." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에 연결한 후 다시 시도하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1347,6 +2042,12 @@ "value" : "Connecting to %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에 연결 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1363,6 +2064,12 @@ "value" : "Connecting…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1379,6 +2086,12 @@ "value" : "Connection" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1395,6 +2108,12 @@ "value" : "Connection Timed Out" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결 시간 초과" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1411,6 +2130,12 @@ "value" : "Contents" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "내용" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1427,6 +2152,12 @@ "value" : "Control desktop summaries" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱 요약 제어" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1437,6 +2168,12 @@ }, "Control what autopilot does automatically and the templates applied to new repositories." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot이 자동으로 수행하는 작업과 새 저장소에 적용되는 템플릿을 제어합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1446,7 +2183,20 @@ } }, "Couldn’t Complete" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "완료할 수 없습니다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法完成" + } + } + } }, "Couldn't connect to the relay from the QR code. Check the relay address and try again." : { "localizations" : { @@ -1456,6 +2206,12 @@ "value" : "Couldn't connect to the relay from the QR code. Check the relay address and try again." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR 코드의 릴레이에 연결할 수 없습니다. 릴레이 주소를 확인하고 다시 시도하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1472,6 +2228,12 @@ "value" : "Couldn't Load Changes" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "변경사항을 불러올 수 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1481,10 +2243,29 @@ } }, "Couldn’t Load Environments" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "환경을 불러올 수 없습니다" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法加载环境" + } + } + } }, "Couldn't Load File" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일을 불러올 수 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1501,6 +2282,12 @@ "value" : "Couldn't load the selected image." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택한 이미지를 불러올 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1517,6 +2304,12 @@ "value" : "Couldn't reach the relay. Check your network and try again." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴레이에 도달할 수 없습니다. 네트워크를 확인하고 다시 시도하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1533,6 +2326,12 @@ "value" : "Couldn't read the selected image." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택한 이미지를 읽을 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1549,6 +2348,12 @@ "value" : "CPU" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "CPU" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1565,6 +2370,12 @@ "value" : "Create" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "생성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1581,6 +2392,12 @@ "value" : "Create a profile to run a command on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 명령을 실행할 프로필을 생성하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1597,6 +2414,12 @@ "value" : "Create branch" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 생성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1613,6 +2436,12 @@ "value" : "Create new branch…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 브랜치 생성…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1622,10 +2451,29 @@ } }, "Create Pull Request" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트 생성" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建拉取请求" + } + } + } }, "Create Release" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 생성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1642,38 +2490,89 @@ "value" : "Created from %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에서 생성됨" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从 %@ 创建" + } + } + } + }, + "Created from the project's current branch" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Created from the project's current branch" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트의 현재 브랜치에서 생성됨" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "从项目的当前分支创建" + } + } + } + }, + "Creates an isolated worktree so this thread works without touching the main checkout." : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "격리된 워크트리를 생성하여 이 스레드가 메인 체크아웃을 건드리지 않고 작동하도록 합니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建一个独立的工作树,让此对话无需改动主检出即可进行。" + } + } + } + }, + "Creates the branch and checks it out in the project root, changing the main checkout." : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 루트에서 브랜치를 생성하고 체크아웃하여 메인 체크아웃을 변경합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "从 %@ 创建" + "value" : "在项目根目录创建并检出该分支,会更改主检出。" } } } }, - "Created from the project's current branch" : { + "Creating Pull Request…" : { "localizations" : { - "en" : { + "ko" : { "stringUnit" : { - "state" : "new", - "value" : "Created from the project's current branch" + "state" : "translated", + "value" : "풀 리퀘스트 생성 중…" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "从项目的当前分支创建" + "value" : "正在创建拉取请求…" } } } - }, - "Creates an isolated worktree so this thread works without touching the main checkout." : { - - }, - "Creates the branch and checks it out in the project root, changing the main checkout." : { - - }, - "Creating Pull Request…" : { - }, "Critical" : { "localizations" : { @@ -1683,6 +2582,12 @@ "value" : "Critical" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "심각" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1699,6 +2604,12 @@ "value" : "Current branch" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 브랜치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1709,6 +2620,12 @@ }, "Current project" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 프로젝트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1725,6 +2642,12 @@ "value" : "Custom Scheme" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 지정 스킴" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1735,6 +2658,12 @@ }, "Custom Script" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 지정 스크립트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1751,6 +2680,12 @@ "value" : "Custom Sources" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 지정 소스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1767,6 +2702,12 @@ "value" : "Custom Target" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 지정 타깃" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1783,6 +2724,12 @@ "value" : "Custom…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용자 지정…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1799,6 +2746,12 @@ "value" : "Daily summary of your projects" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트의 일일 요약" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1809,6 +2762,12 @@ }, "Default" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "기본값" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1819,6 +2778,12 @@ }, "Default for new repositories" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 저장소의 기본값" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1835,6 +2800,12 @@ "value" : "Delete" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "삭제" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1845,6 +2816,12 @@ }, "Delete document?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서를 삭제할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1855,6 +2832,12 @@ }, "Delete environment?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "환경을 삭제할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1865,6 +2848,12 @@ }, "Delete file?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일을 삭제할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1874,13 +2863,45 @@ } }, "Delete Project" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 삭제" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除项目" + } + } + } }, "Delete Project?" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트를 삭제할까요?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除项目?" + } + } + } }, "Delete template?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "템플릿을 삭제할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1897,6 +2918,12 @@ "value" : "Delete this thread?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 스레드를 삭제할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1913,6 +2940,12 @@ "value" : "Deny" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "거부" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1929,6 +2962,12 @@ "value" : "Describe a task, paste a snippet, or ask a question." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "작업을 설명하거나 스니펫을 붙여넣거나 질문하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1945,6 +2984,12 @@ "value" : "Describe what to build…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "무엇을 만들지 설명하세요…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1954,7 +2999,20 @@ } }, "design/overview" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "design/overview" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "design/overview" + } + } + } }, "Desktop Behavior" : { "localizations" : { @@ -1964,6 +3022,12 @@ "value" : "Desktop Behavior" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱 동작" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1980,6 +3044,12 @@ "value" : "Desktop Configuration" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱 구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -1996,6 +3066,12 @@ "value" : "Desktop Settings" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱 설정" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2012,6 +3088,12 @@ "value" : "Destination" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대상" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2022,6 +3104,12 @@ }, "Detected · Make" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감지됨 · Make" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2032,6 +3120,12 @@ }, "Detected · Scripts" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감지됨 · 스크립트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2042,6 +3136,12 @@ }, "Detected · Xcode" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감지됨 · Xcode" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2058,6 +3158,12 @@ "value" : "Device name" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "기기 이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2074,6 +3180,12 @@ "value" : "Diff truncated — open this file on your Mac for the full diff." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "차이가 잘렸습니다 — 전체 차이를 보려면 Mac에서 이 파일을 여세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2084,6 +3196,12 @@ }, "Disabled" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "비활성화됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2100,6 +3218,12 @@ "value" : "Disconnected" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "연결 끊김" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2110,6 +3234,12 @@ }, "Dispatch" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "디스패치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2120,6 +3250,12 @@ }, "Document ID (slug)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 ID (슬러그)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2130,6 +3266,12 @@ }, "Documentation" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2140,6 +3282,12 @@ }, "Documents" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2156,6 +3304,12 @@ "value" : "Done" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "완료" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2165,10 +3319,36 @@ } }, "Download" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다운로드" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下载" + } + } + } }, "Download Secret" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시크릿 다운로드" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下载密钥" + } + } + } }, "Duplicate" : { "localizations" : { @@ -2178,6 +3358,12 @@ "value" : "Duplicate" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복제" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2194,6 +3380,12 @@ "value" : "Edit" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "편집" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2210,6 +3402,12 @@ "value" : "Edit MCP Server" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버 편집" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2220,6 +3418,12 @@ }, "Edit Template" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "템플릿 편집" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2236,6 +3440,12 @@ "value" : "Empty Folder" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "빈 폴더" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2252,6 +3462,12 @@ "value" : "Enable Plan before sending a new thread so the desktop agent drafts a read-only plan first." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 스레드를 보내기 전에 계획을 활성화하면 데스크톱 에이전트가 먼저 읽기 전용 계획을 작성합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2262,6 +3478,12 @@ }, "Enabled" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활성화됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2272,6 +3494,12 @@ }, "Encryption not set up" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "암호화가 설정되지 않음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2288,6 +3516,12 @@ "value" : "Endpoint" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "엔드포인트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2298,6 +3532,12 @@ }, "Enroll with your passkey on your Mac to manage secrets." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시크릿을 관리하려면 Mac에서 패스키로 등록하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2314,6 +3554,12 @@ "value" : "Enter a GitHub repository URL." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHub 저장소 URL을 입력하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2330,6 +3576,12 @@ "value" : "Enter a new name for this Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 Mac의 새 이름을 입력하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2339,10 +3591,29 @@ } }, "Environment" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "환경" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "环境" + } + } + } }, "Error" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "오류" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2359,6 +3630,12 @@ "value" : "Extra High" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "매우 높음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2375,6 +3652,12 @@ "value" : "Failed to add project." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 추가에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2384,7 +3667,20 @@ } }, "Failed to delete project." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 삭제에 실패했습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除项目失败。" + } + } + } }, "Failed to detect runnables." : { "localizations" : { @@ -2394,6 +3690,12 @@ "value" : "Failed to detect runnables." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 가능 항목 감지에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2410,6 +3712,12 @@ "value" : "Failed to load folders." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더를 불러오지 못했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2426,6 +3734,12 @@ "value" : "Failed to load MCP servers." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버를 불러오지 못했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2442,6 +3756,12 @@ "value" : "Failed to load skills." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬을 불러오지 못했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2458,6 +3778,12 @@ "value" : "Failed to load the agent registry." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트 레지스트리를 불러오지 못했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2474,6 +3800,12 @@ "value" : "Failed to request agents: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트 요청에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2490,6 +3822,12 @@ "value" : "Failed to request detection: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감지 요청에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2500,6 +3838,12 @@ }, "Failed to request file: %@" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 요청에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2516,6 +3860,12 @@ "value" : "Failed to request MCP servers: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버 요청에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2532,6 +3882,12 @@ "value" : "Failed to request skills: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 요청에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2548,6 +3904,12 @@ "value" : "Failed to send request: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요청 전송에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2564,6 +3926,12 @@ "value" : "Failed to send run profile delete: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필 삭제 전송에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2580,6 +3948,12 @@ "value" : "Failed to send run profile update: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필 업데이트 전송에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2596,6 +3970,12 @@ "value" : "Failed to send run request: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 요청 전송에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2612,6 +3992,12 @@ "value" : "Failed to send stop request: %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "중지 요청 전송에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2621,7 +4007,20 @@ } }, "Failed to trigger the release workflow." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 워크플로 트리거에 실패했습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "触发发布工作流失败。" + } + } + } }, "Failing" : { "localizations" : { @@ -2631,6 +4030,12 @@ "value" : "Failing" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실패 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2647,6 +4052,12 @@ "value" : "Fair" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보통" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2657,6 +4068,12 @@ }, "Favorites" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "즐겨찾기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2673,6 +4090,12 @@ "value" : "File paths" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 경로" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2683,6 +4106,12 @@ }, "File preview truncated." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 미리보기가 잘렸습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2693,6 +4122,12 @@ }, "Filename" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일 이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2703,6 +4138,12 @@ }, "Files" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2712,7 +4153,20 @@ } }, "Files are decrypted on this device with your passkey, then written into the project folder on your Mac." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "파일은 이 기기에서 패스키로 복호화된 후 Mac의 프로젝트 폴더에 기록됩니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "文件将使用你的通行密钥在本设备上解密,然后写入 Mac 上的项目文件夹。" + } + } + } }, "Filter Skills" : { "localizations" : { @@ -2722,6 +4176,12 @@ "value" : "Filter Skills" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 필터링" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2738,6 +4198,12 @@ "value" : "Focus mode" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "집중 모드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2754,6 +4220,12 @@ "value" : "Folders" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2770,6 +4242,12 @@ "value" : "Folders Unavailable" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더를 사용할 수 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2780,6 +4258,12 @@ }, "Form couldn't be rendered — edit raw JSON." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "양식을 렌더링할 수 없습니다 — 원본 JSON을 편집하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2790,6 +4274,12 @@ }, "Frequency" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "빈도" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2799,7 +4289,20 @@ } }, "ghp_… or github_pat_…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "ghp_… 또는 github_pat_…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "ghp_… 或 github_pat_…" + } + } + } }, "Git Source" : { "localizations" : { @@ -2809,6 +4312,12 @@ "value" : "Git Source" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git 소스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2825,6 +4334,12 @@ "value" : "Git Sources" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git 소스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2841,6 +4356,12 @@ "value" : "Global search" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전역 검색" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2851,6 +4372,12 @@ }, "Go" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이동" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2867,6 +4394,12 @@ "value" : "Go to **Settings → Mobile** and tap *Pair new device*." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "**설정 → 모바일**로 이동하여 *새 기기 페어링*을 탭하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2877,6 +4410,12 @@ }, "Hide values" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "값 숨기기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2893,6 +4432,12 @@ "value" : "High" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "높음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2902,13 +4447,52 @@ } }, "HTTP" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "HTTP" + } + } + } }, "https://…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://…" + } + } + } }, "https://github.com/owner/repo" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://github.com/owner/repo" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "https://github.com/owner/repo" + } + } + } }, "Images" : { "localizations" : { @@ -2918,6 +4502,12 @@ "value" : "Images" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이미지" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2928,6 +4518,12 @@ }, "Initialize Git" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Git 초기화" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2938,6 +4534,12 @@ }, "Initializing…" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "초기화 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2954,6 +4556,12 @@ "value" : "Input" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "입력" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2964,6 +4572,12 @@ }, "Inputs" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "입력" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2980,6 +4594,12 @@ "value" : "Install" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -2990,6 +4610,12 @@ }, "Install CI upload token" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "CI 업로드 토큰 설치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3000,6 +4626,12 @@ }, "Install RELEASE_TOKEN" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "RELEASE_TOKEN 설치" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3017,6 +4649,12 @@ "value" : "Install skills and agents, and configure MCP servers on the active Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활성 Mac에서 스킬과 에이전트를 설치하고 MCP 서버를 구성하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3027,6 +4665,12 @@ }, "Install skills and agents, configure MCP servers, and manage Autopilot on the active Mac." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활성 Mac에서 스킬과 에이전트를 설치하고 MCP 서버를 구성하며 Autopilot을 관리하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3043,6 +4687,12 @@ "value" : "Installed" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설치됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3053,6 +4703,12 @@ }, "Installed as the repo's GitHub Actions secret so the release workflow can publish." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 워크플로가 게시할 수 있도록 저장소의 GitHub Actions 시크릿으로 설치됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3069,6 +4725,12 @@ "value" : "Installing…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설치 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3085,6 +4747,12 @@ "value" : "Invalid or expired pairing code." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "잘못되었거나 만료된 페어링 코드입니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3101,6 +4769,12 @@ "value" : "Key" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "키" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3111,6 +4785,12 @@ }, "Latest: %@" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "최신: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3127,6 +4807,12 @@ "value" : "Live" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실시간" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3137,6 +4823,12 @@ }, "Load more" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "더 보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3146,7 +4838,20 @@ } }, "Loading Autopilot…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot 불러오는 중…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载 Autopilot…" + } + } + } }, "Loading changes…" : { "localizations" : { @@ -3156,6 +4861,12 @@ "value" : "Loading changes…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "변경사항 불러오는 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3165,7 +4876,20 @@ } }, "Loading environments…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "환경 불러오는 중…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载环境…" + } + } + } }, "Loading folders" : { "localizations" : { @@ -3175,6 +4899,12 @@ "value" : "Loading folders" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더 불러오는 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3185,6 +4915,12 @@ }, "Loading messages" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "메시지 불러오는 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3201,6 +4937,12 @@ "value" : "Loading skills…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 불러오는 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3211,6 +4953,12 @@ }, "Loading thread" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 불러오는 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3220,7 +4968,20 @@ } }, "Loading workflows…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "워크플로 불러오는 중…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "正在加载工作流…" + } + } + } }, "Loading…" : { "localizations" : { @@ -3230,6 +4991,12 @@ "value" : "Loading…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "불러오는 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3246,6 +5013,12 @@ "value" : "Long text" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "긴 텍스트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3262,6 +5035,12 @@ "value" : "Low" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "낮음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3278,6 +5057,12 @@ "value" : "Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3294,6 +5079,12 @@ "value" : "Mac name" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3304,6 +5095,12 @@ }, "Mac offline" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 오프라인" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3313,7 +5110,20 @@ } }, "main" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "main" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "main" + } + } + } }, "Make" : { "localizations" : { @@ -3323,6 +5133,12 @@ "value" : "Make" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Make" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3339,6 +5155,12 @@ "value" : "Make Configuration" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Make 구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3355,6 +5177,12 @@ "value" : "Make sure RxCode is open on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 RxCode가 열려 있는지 확인하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3371,6 +5199,12 @@ "value" : "Makefile" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Makefile" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3387,6 +5221,12 @@ "value" : "Malformed question payload" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "잘못된 형식의 질문 페이로드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3397,6 +5237,12 @@ }, "Manage end-to-end encrypted secrets, CI auto-update scans, indexed docs, and release workflows." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "종단 간 암호화된 시크릿, CI 자동 업데이트 스캔, 색인된 문서, 릴리스 워크플로를 관리합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3406,7 +5252,20 @@ } }, "Markdown" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markdown" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Markdown" + } + } + } }, "Marketplaces" : { "localizations" : { @@ -3416,6 +5275,12 @@ "value" : "Marketplaces" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "마켓플레이스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3432,6 +5297,12 @@ "value" : "Max" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "최대" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3448,6 +5319,12 @@ "value" : "MCP operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3464,6 +5341,12 @@ "value" : "MCP Servers" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3480,6 +5363,12 @@ "value" : "Medium" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "중간" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3496,6 +5385,12 @@ "value" : "Memory" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "메모리" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3506,6 +5401,12 @@ }, "Merge Settings" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "병합 설정" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3522,6 +5423,12 @@ "value" : "Message…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "메시지…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3532,26 +5439,51 @@ }, "Messages you queue while a response is streaming appear here." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "응답이 스트리밍되는 동안 대기열에 넣은 메시지가 여기에 표시됩니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在响应流式传输期间排队的消息会显示在这里。" + } + } + } + }, + "Mints a DOCS_UPLOAD_TOKEN and installs it as the repo's GitHub Actions secret so CI can publish docs automatically." : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "DOCS_UPLOAD_TOKEN을 발급하여 CI가 문서를 자동으로 게시할 수 있도록 저장소의 GitHub Actions 시크릿으로 설치합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "在响应流式传输期间排队的消息会显示在这里。" + "value" : "生成 DOCS_UPLOAD_TOKEN 并将其作为仓库的 GitHub Actions 密钥安装,以便 CI 可以自动发布文档。" } } } }, - "Mints a DOCS_UPLOAD_TOKEN and installs it as the repo's GitHub Actions secret so CI can publish docs automatically." : { + "Mode" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "生成 DOCS_UPLOAD_TOKEN 并将其作为仓库的 GitHub Actions 密钥安装,以便 CI 可以自动发布文档。" + "value" : "模式" } } } - }, - "Mode" : { - }, "Model" : { "localizations" : { @@ -3561,6 +5493,12 @@ "value" : "Model" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모델" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3577,6 +5515,12 @@ "value" : "Name" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3587,6 +5531,12 @@ }, "Name (e.g. production)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이름 (예: production)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3603,6 +5553,12 @@ "value" : "New" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "신규" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3613,6 +5569,12 @@ }, "New environment" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 환경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3623,6 +5585,12 @@ }, "New Template" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 템플릿" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3639,6 +5607,12 @@ "value" : "New Thread" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 스레드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3648,10 +5622,29 @@ } }, "New worktree" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 워크트리" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建工作树" + } + } + } }, "Next change" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 변경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3668,6 +5661,12 @@ "value" : "No agents found." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트를 찾을 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3684,6 +5683,12 @@ "value" : "No briefings for the current branch." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 브랜치에 대한 브리핑이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3700,6 +5705,12 @@ "value" : "No briefings for the current branch. Choose All branches to see other branches." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "현재 브랜치에 대한 브리핑이 없습니다. 다른 브랜치를 보려면 모든 브랜치를 선택하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3716,6 +5727,12 @@ "value" : "No briefings match the selected projects." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택한 프로젝트와 일치하는 브리핑이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3732,6 +5749,12 @@ "value" : "No Briefings Yet" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 브리핑 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3748,6 +5771,12 @@ "value" : "No Changes" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "변경사항 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3764,6 +5793,12 @@ "value" : "No custom Git sources added." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "추가된 사용자 지정 Git 소스가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3780,6 +5815,12 @@ "value" : "No diff content available." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용 가능한 차이 내용이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3789,10 +5830,29 @@ } }, "No dispatchable release workflow found for %@. Add a workflow_dispatch release workflow and rescan, then try again." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에 대해 디스패치 가능한 릴리스 워크플로를 찾을 수 없습니다. workflow_dispatch 릴리스 워크플로를 추가하고 다시 스캔한 후 시도하세요." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到 %@ 可触发的发布工作流。请添加一个 workflow_dispatch 发布工作流并重新扫描,然后重试。" + } + } + } }, "No documentation repositories. Tap + to add one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 저장소가 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3803,6 +5863,12 @@ }, "No documents indexed yet." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 색인된 문서가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3813,6 +5879,12 @@ }, "No environments yet. Tap + to add one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 환경이 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3823,6 +5895,12 @@ }, "No files yet. Tap + to add one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 파일이 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3839,6 +5917,12 @@ "value" : "No Folders" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3855,6 +5939,12 @@ "value" : "No MCP servers configured. Tap + to add one." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "구성된 MCP 서버가 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3871,6 +5961,12 @@ "value" : "No models available" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용 가능한 모델 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3881,6 +5977,12 @@ }, "No open auto-update pull requests." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "열려 있는 자동 업데이트 풀 리퀘스트가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3897,6 +5999,12 @@ "value" : "No Projects" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3913,6 +6021,12 @@ "value" : "No QR code found in that image." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "해당 이미지에서 QR 코드를 찾을 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3923,6 +6037,12 @@ }, "No Queued Messages" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 메시지 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3933,6 +6053,12 @@ }, "No release repositories. Tap + to add one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 저장소가 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3943,6 +6069,12 @@ }, "No repositories available to add." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "추가할 수 있는 저장소가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3953,6 +6085,12 @@ }, "No repositories available." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용 가능한 저장소가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3969,6 +6107,12 @@ "value" : "No Run Profiles" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3979,6 +6123,12 @@ }, "No scans yet." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 스캔이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -3988,10 +6138,29 @@ } }, "No Secrets" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시크릿 없음" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "暂无密钥" + } + } + } }, "No Selection" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택 항목 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4008,6 +6177,12 @@ "value" : "No skills found." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬을 찾을 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4024,6 +6199,12 @@ "value" : "No status" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "상태 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4040,6 +6221,12 @@ "value" : "No summary available yet" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 사용 가능한 요약이 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4056,6 +6243,12 @@ "value" : "No summary yet" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 요약 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4066,6 +6259,12 @@ }, "No summary yet. A summary is generated once the thread finishes a turn on your Mac." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 요약이 없습니다. Mac에서 스레드가 한 턴을 마치면 요약이 생성됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4076,6 +6275,12 @@ }, "No templates yet. Tap + to create one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 템플릿이 없습니다. +를 탭하여 생성하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4092,6 +6297,12 @@ "value" : "No Threads" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4108,6 +6319,12 @@ "value" : "No threads yet" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 스레드 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4118,6 +6335,12 @@ }, "No todos for this thread." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 스레드에 대한 할 일이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4128,6 +6351,12 @@ }, "No watched repositories. Tap + to add one." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감시 중인 저장소가 없습니다. +를 탭하여 추가하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4138,6 +6367,12 @@ }, "No workflows scanned yet." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "아직 스캔된 워크플로가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4154,6 +6389,12 @@ "value" : "Normal" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "보통" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4164,6 +6405,12 @@ }, "Not connected to your Mac." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에 연결되어 있지 않습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4174,6 +6421,12 @@ }, "Not signed in" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "로그인되지 않음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4190,6 +6443,12 @@ "value" : "Nothing to Show" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "표시할 항목 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4206,6 +6465,12 @@ "value" : "of %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4222,6 +6487,12 @@ "value" : "Off" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "끔" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4238,6 +6509,12 @@ "value" : "OK" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4254,6 +6531,12 @@ "value" : "On" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "켬" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4264,6 +6547,12 @@ }, "Open" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4280,6 +6569,12 @@ "value" : "Open **RxCode** on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 **RxCode**를 여세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4296,6 +6591,12 @@ "value" : "Open failing CI run" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실패한 CI 실행 열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4312,6 +6613,12 @@ "value" : "Open in Browser" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브라우저에서 열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4322,6 +6629,12 @@ }, "Open on GitHub" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHub에서 열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4332,6 +6645,12 @@ }, "Open Pull Request" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pull Request 열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4348,6 +6667,12 @@ "value" : "Opens todos and thread summary" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "할 일과 스레드 요약 열기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4358,6 +6683,12 @@ }, "Optional xcodebuild destination" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "선택적 xcodebuild 대상" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4367,13 +6698,45 @@ } }, "Overwrite existing files" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "기존 파일 덮어쓰기" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "覆盖现有文件" + } + } + } }, "Package" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "패키지" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "软件包" + } + } + } }, "Package Configuration" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "패키지 구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4384,6 +6747,12 @@ }, "Package Manager" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "패키지 관리자" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4400,6 +6769,12 @@ "value" : "Pair New Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새 Mac 페어링" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4416,6 +6791,12 @@ "value" : "Pair with your Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac과 페어링" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4432,6 +6813,12 @@ "value" : "Paired %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 페어링됨" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4448,6 +6835,12 @@ "value" : "Paired Macs" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "페어링된 Mac" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4464,6 +6857,12 @@ "value" : "Pairing with your Mac…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac과 페어링 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4480,6 +6879,12 @@ "value" : "Passing" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "통과" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4490,6 +6895,12 @@ }, "Passkey authentication failed: %@" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "패스키 인증에 실패했습니다: %@" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4500,6 +6911,12 @@ }, "Passkey authentication was cancelled." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "패스키 인증이 취소되었습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4510,6 +6927,12 @@ }, "Paste a GitHub ruleset JSON with name, target, enforcement, and rules." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "name, target, enforcement, rules가 포함된 GitHub 규칙 집합 JSON을 붙여넣으세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4526,6 +6949,12 @@ "value" : "Permission needed" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "권한이 필요합니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4542,6 +6971,12 @@ "value" : "Pick a desktop folder remotely, then start or continue its threads from mobile." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱 폴더를 원격으로 선택한 다음 모바일에서 해당 스레드를 시작하거나 이어가세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4558,6 +6993,12 @@ "value" : "Plan mode" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획 모드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4568,6 +7009,12 @@ }, "Previous change" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이전 변경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4578,6 +7025,12 @@ }, "Privacy Report" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "개인정보 보고서" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4594,6 +7047,12 @@ "value" : "Profiles" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로필" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4610,6 +7069,12 @@ "value" : "Project / Workspace" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 / 작업 공간" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4620,6 +7085,12 @@ }, "Project actions" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 작업" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4630,6 +7101,12 @@ }, "Project root" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 루트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4646,6 +7123,12 @@ "value" : "Projects" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4662,6 +7145,12 @@ "value" : "Provider" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제공자" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4672,6 +7161,12 @@ }, "Pull Requests" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4688,6 +7183,12 @@ "value" : "Queue a message..." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "메시지 대기열에 추가..." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4698,6 +7199,12 @@ }, "Queued Messages" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대기 중인 메시지" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4714,6 +7221,12 @@ "value" : "Reconnecting in %llds" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld초 후 재연결" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4730,6 +7243,12 @@ "value" : "Refresh" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새로고침" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4740,6 +7259,12 @@ }, "Release" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4749,13 +7274,45 @@ } }, "Release workflow triggered" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 워크플로가 트리거됨" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已触发发布工作流" + } + } + } }, "RELEASE_TOKEN" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "RELEASE_TOKEN" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "RELEASE_TOKEN" + } + } + } }, "Releases" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4766,6 +7323,12 @@ }, "Reload" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "새로고침" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4782,6 +7345,12 @@ "value" : "Remove" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "제거" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4798,6 +7367,12 @@ "value" : "Remove %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 제거" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4808,6 +7383,12 @@ }, "Remove agent?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "에이전트를 제거할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4818,6 +7399,12 @@ }, "Remove MCP server?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "MCP 서버를 제거하시겠습니까?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4834,6 +7421,12 @@ "value" : "Remove pairing?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "페어링을 제거할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4844,6 +7437,12 @@ }, "Remove repository?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장소를 제거할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4860,6 +7459,12 @@ "value" : "Rename" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이름 변경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4876,6 +7481,12 @@ "value" : "Rename Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 이름 변경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4886,6 +7497,12 @@ }, "Rename Thread" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 이름 변경" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4896,6 +7513,12 @@ }, "Repo Setup" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장소 설정" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4906,6 +7529,28 @@ }, "Repositories" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장소" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "仓库" + } + } + } + }, + "Repository" : { + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장소" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4913,9 +7558,6 @@ } } } - }, - "Repository" : { - }, "Request timed out. Check your Mac and try again." : { "localizations" : { @@ -4925,6 +7567,12 @@ "value" : "Request timed out. Check your Mac and try again." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요청 시간이 초과되었습니다. Mac을 확인하고 다시 시도하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4941,6 +7589,12 @@ "value" : "Resets %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에 재설정" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4951,6 +7605,12 @@ }, "Response in progress" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "응답 진행 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4967,6 +7627,12 @@ "value" : "Response notifications" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "응답 알림" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4983,6 +7649,12 @@ "value" : "Retry" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "재시도" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -4999,6 +7671,12 @@ "value" : "Review" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "검토" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5015,6 +7693,12 @@ "value" : "Review project briefings" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 브리핑 검토" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5031,6 +7715,12 @@ "value" : "Run Profile Error" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필 오류" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5047,6 +7737,12 @@ "value" : "Run profile operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5063,6 +7759,12 @@ "value" : "Run Profiles" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 프로필" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5079,6 +7781,12 @@ "value" : "Running" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5095,6 +7803,12 @@ "value" : "Runs" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "실행" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5105,6 +7819,12 @@ }, "RxCode keeps this browser session separate from your regular Safari data." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "RxCode는 이 브라우저 세션을 일반 Safari 데이터와 분리해서 유지합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5114,7 +7834,20 @@ } }, "rxcode/feature-name" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "rxcode/feature-name" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "rxcode/feature-name" + } + } + } }, "Save" : { "localizations" : { @@ -5124,6 +7857,12 @@ "value" : "Save" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "저장" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5134,6 +7873,12 @@ }, "Scan" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스캔" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5144,6 +7889,12 @@ }, "Scan History" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스캔 기록" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5160,6 +7911,12 @@ "value" : "Scan QR" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR 스캔" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5176,6 +7933,12 @@ "value" : "Scan QR Code" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "QR 코드 스캔" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5192,6 +7955,12 @@ "value" : "Scan the QR code from RxCode Settings on your Mac to control threads and approvals from this device." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 기기에서 스레드와 승인을 제어하려면 Mac의 RxCode 설정에 있는 QR 코드를 스캔하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5208,6 +7977,12 @@ "value" : "Scan the QR code shown on your Mac with this app." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 앱으로 Mac에 표시된 QR 코드를 스캔하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5224,6 +7999,12 @@ "value" : "Scan the QR code shown on your Mac, or pick a screenshot from your library." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에 표시된 QR 코드를 스캔하거나 라이브러리에서 스크린샷을 선택하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5240,6 +8021,12 @@ "value" : "Scan with Camera" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "카메라로 스캔" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5250,6 +8037,12 @@ }, "Scanning…" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스캔 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5266,6 +8059,12 @@ "value" : "Scheme" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킴" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5275,10 +8074,29 @@ } }, "Scope" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "범위" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "范围" + } + } + } }, "Script" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스크립트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5295,6 +8113,12 @@ "value" : "Scroll to bottom" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "맨 아래로 스크롤" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5304,10 +8128,29 @@ } }, "Search" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "검색" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索" + } + } + } }, "Search or enter website name" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "웹사이트 이름 검색 또는 입력" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5324,6 +8167,12 @@ "value" : "Search projects and threads" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 및 스레드 검색" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5340,6 +8189,12 @@ "value" : "Search skills" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 검색" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5349,7 +8204,20 @@ } }, "Search threads and docs" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 및 문서 검색" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "搜索对话和文档" + } + } + } }, "Searching…" : { "localizations" : { @@ -5359,6 +8227,12 @@ "value" : "Searching…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "검색 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5369,6 +8243,12 @@ }, "Secrets" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시크릿" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5385,6 +8265,12 @@ "value" : "Securely link this device to RxCode in seconds." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 기기를 몇 초 만에 RxCode에 안전하게 연결하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5395,6 +8281,12 @@ }, "Select a briefing to view details" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "세부 정보를 보려면 브리핑을 선택하세요" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5405,6 +8297,12 @@ }, "Select a project" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5415,6 +8313,12 @@ }, "Select a thread" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5431,6 +8335,12 @@ "value" : "Select all that apply" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "해당하는 항목 모두 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5447,6 +8357,12 @@ "value" : "Select Folder" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더 선택" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5463,6 +8379,12 @@ "value" : "Select which Mac this device controls. Removing one pairing does not reset this device's identity." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 기기가 제어할 Mac을 선택하세요. 페어링 하나를 제거해도 이 기기의 ID는 초기화되지 않습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5479,6 +8401,12 @@ "value" : "Serious" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "심각" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5495,6 +8423,12 @@ "value" : "Server" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "서버" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5504,20 +8438,65 @@ } }, "Set Up Docs" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "문서 설정" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置文档" + } + } + } }, "Set Up Release Workflow" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "릴리스 워크플로 설정" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置发布工作流" + } + } + } }, "Set Up Secrets" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "시크릿 설정" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置密钥" + } + } + } }, "Settings" : { "localizations" : { "en" : { "stringUnit" : { - "state" : "new", - "value" : "Settings" + "state" : "new", + "value" : "Settings" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설정" } }, "zh-Hans" : { @@ -5536,6 +8515,12 @@ "value" : "Sign in to Claude Code or Codex on your Mac to see usage." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용량을 보려면 Mac에서 Claude Code 또는 Codex에 로그인하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5546,6 +8531,12 @@ }, "Sign in with rxlab on your Mac to enable Autopilot." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Autopilot을 사용하려면 Mac에서 rxlab으로 로그인하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5562,6 +8553,12 @@ "value" : "Skill operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5578,6 +8575,12 @@ "value" : "Skill source operation failed." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬 소스 작업에 실패했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5594,6 +8597,12 @@ "value" : "Skills" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스킬" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5610,6 +8619,12 @@ "value" : "Skip All Questions" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 질문 건너뛰기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5620,6 +8635,12 @@ }, "Source link (optional)" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "소스 링크 (선택 사항)" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5629,7 +8650,20 @@ } }, "SSE" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSE" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "SSE" + } + } + } }, "Start a conversation from your Mac" : { "localizations" : { @@ -5639,6 +8673,12 @@ "value" : "Start a conversation from your Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 대화 시작" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5655,6 +8695,12 @@ "value" : "Start a mobile-created thread with Claude Code, Codex, or an ACP agent synced from your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에서 동기화된 Claude Code, Codex 또는 ACP 에이전트로 모바일에서 생성한 스레드를 시작하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5671,6 +8717,12 @@ "value" : "Start a new conversation to get help with your project." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트에 대한 도움을 받으려면 새 대화를 시작하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5687,6 +8739,12 @@ "value" : "Start with a plan" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "계획으로 시작" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5696,7 +8754,20 @@ } }, "stdio" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "stdio" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "stdio" + } + } + } }, "Stop" : { "localizations" : { @@ -5706,6 +8777,12 @@ "value" : "Stop" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "중지" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5716,6 +8793,12 @@ }, "Stop watching?" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "감시를 중지할까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5726,6 +8809,12 @@ }, "Success" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "성공" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5742,6 +8831,12 @@ "value" : "Summarization" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요약" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5758,6 +8853,12 @@ "value" : "Summary" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요약" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5774,6 +8875,12 @@ "value" : "Switch branch" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "브랜치 전환" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5790,6 +8897,12 @@ "value" : "Switch to %@" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@(으)로 전환" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5800,6 +8913,12 @@ }, "Switch to horizontal scroll" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "가로 스크롤로 전환" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5810,6 +8929,12 @@ }, "Switch to wrap" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "줄 바꿈으로 전환" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5826,6 +8951,12 @@ "value" : "Syncing settings…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "설정 동기화 중…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5842,6 +8973,12 @@ "value" : "Syncing with Mac" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac과 동기화 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5858,6 +8995,12 @@ "value" : "Target" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "대상" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5868,6 +9011,12 @@ }, "Template" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "템플릿" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5878,6 +9027,12 @@ }, "Templates of GitHub merge settings and branch rulesets. The default template is applied to newly created repositories." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHub 병합 설정 및 브랜치 규칙 집합의 템플릿입니다. 기본 템플릿은 새로 생성된 저장소에 적용됩니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5894,6 +9049,12 @@ "value" : "The desktop sent an AskUserQuestion call this app could not parse." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "데스크톱이 이 앱에서 구문 분석할 수 없는 AskUserQuestion 호출을 보냈습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5904,6 +9065,12 @@ }, "The Mac could not complete the request." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 요청을 완료할 수 없었습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5913,10 +9080,29 @@ } }, "The Mac is pushing the branch and opening the PR." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 브랜치를 푸시하고 PR을 여는 중입니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 正在推送分支并创建 PR。" + } + } + } }, "The Mac returned an empty response." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 빈 응답을 반환했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5926,13 +9112,45 @@ } }, "The Mac returned an invalid pull-request URL." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 잘못된 풀 리퀘스트 URL을 반환했습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac 返回了无效的拉取请求 URL。" + } + } + } }, "The next version is computed by semantic-release from the commit history." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 버전은 semantic-release가 커밋 기록에서 계산합니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "下一个版本由 semantic-release 根据提交历史计算。" + } + } + } }, "The paired Mac changed before the request finished." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "요청이 완료되기 전에 페어링된 Mac이 변경되었습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5949,6 +9167,12 @@ "value" : "Thermal" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "발열" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5958,10 +9182,29 @@ } }, "These files already exist and were skipped: %@. Turn on overwrite to replace them." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다음 파일이 이미 존재하여 건너뛰었습니다: %@. 덮어쓰기를 켜면 교체됩니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "这些文件已存在并被跳过:%@。开启覆盖以替换它们。" + } + } + } }, "Thinking" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "생각 중" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5978,6 +9221,12 @@ "value" : "This location has no visible folders." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 위치에 표시할 폴더가 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -5994,6 +9243,12 @@ "value" : "This location has nothing to show." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 위치에 표시할 항목이 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6004,6 +9259,12 @@ }, "This passkey can't unlock secrets — it doesn't support the PRF extension. Use the passkey you enrolled with." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 패스키로는 시크릿을 잠금 해제할 수 없습니다 — PRF 확장을 지원하지 않습니다. 등록할 때 사용한 패스키를 사용하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6014,6 +9275,12 @@ }, "This permanently deletes the environment and its secret files." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 환경과 그 시크릿 파일을 영구적으로 삭제합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6030,6 +9297,12 @@ "value" : "This permanently deletes the thread and all of its messages. This cannot be undone." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 스레드와 모든 메시지를 영구적으로 삭제합니다. 실행 취소할 수 없습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6046,6 +9319,12 @@ "value" : "this project" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 프로젝트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6055,7 +9334,20 @@ } }, "This project is not linked to a GitHub repo." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 프로젝트는 GitHub 저장소에 연결되어 있지 않습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此项目未关联到 GitHub 仓库。" + } + } + } }, "This QR has expired. Generate a new one on your Mac." : { "localizations" : { @@ -6065,6 +9357,12 @@ "value" : "This QR has expired. Generate a new one on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 QR이 만료되었습니다. Mac에서 새 QR을 생성하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6074,14 +9372,33 @@ } }, "This removes “%@” and all its threads from RxCode. Your files on the Mac are not deleted." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 RxCode에서 “%@”과(와) 그 모든 스레드를 제거합니다. Mac의 파일은 삭제되지 않습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "这将从 RxCode 中移除“%@”及其所有对话。Mac 上的文件不会被删除。" + } + } + } }, "This removes %@ and its downloaded binary from your Mac." : { "localizations" : { "en" : { "stringUnit" : { - "state" : "new", - "value" : "This removes %@ and its downloaded binary from your Mac." + "state" : "new", + "value" : "This removes %@ and its downloaded binary from your Mac." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 Mac에서 %@과(와) 그 다운로드된 바이너리를 제거합니다." } }, "zh-Hans" : { @@ -6100,6 +9417,12 @@ "value" : "This removes %@ from this device. Other paired Macs stay available." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 이 기기에서 %@을(를) 제거합니다. 다른 페어링된 Mac은 계속 사용할 수 있습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6116,6 +9439,12 @@ "value" : "This removes %@ from your Mac's global MCP configuration." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 Mac의 전역 MCP 구성에서 %@을(를) 제거합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6126,6 +9455,12 @@ }, "This removes the repository and its indexed documents from docs search." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 작업은 문서 검색에서 저장소와 그 색인된 문서를 제거합니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6135,10 +9470,29 @@ } }, "This repository has no secret environments to download." : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 저장소에는 다운로드할 시크릿 환경이 없습니다." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "此仓库没有可下载的密钥环境。" + } + } + } }, "This settings form couldn't be rendered. Edit the raw JSON instead." : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이 설정 양식을 렌더링할 수 없습니다. 대신 원시 JSON을 편집하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6155,6 +9509,12 @@ "value" : "This Turn" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "이번 턴" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6171,6 +9531,12 @@ "value" : "Thread actions" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 작업" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6181,6 +9547,12 @@ }, "Thread name" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 이름" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6191,6 +9563,12 @@ }, "Thread Summary" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드 요약" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6207,6 +9585,12 @@ "value" : "Threads" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "스레드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6217,6 +9601,12 @@ }, "Todos" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "할 일" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6227,6 +9617,12 @@ }, "Todos & Summary" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "할 일 및 요약" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6243,6 +9639,12 @@ "value" : "Tool" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "도구" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6259,6 +9661,12 @@ "value" : "Transport" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "전송" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6269,6 +9677,12 @@ }, "Trigger scan now" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "지금 스캔 트리거" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6285,6 +9699,12 @@ "value" : "Try Again" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "다시 시도" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6301,6 +9721,12 @@ "value" : "Type" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "유형" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6317,6 +9743,12 @@ "value" : "Type your answer…" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "답변을 입력하세요…" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6333,6 +9765,12 @@ "value" : "Unable to Add Project" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "프로젝트를 추가할 수 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6349,6 +9787,12 @@ "value" : "Unable to connect to your Mac. Make sure RxCode is running on your desktop and both devices are on the same network." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac에 연결할 수 없습니다. 데스크톱에서 RxCode가 실행 중이고 두 기기가 같은 네트워크에 있는지 확인하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6365,6 +9809,12 @@ "value" : "Unable to Load Folder" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "폴더를 불러올 수 없습니다" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6381,6 +9831,12 @@ "value" : "Uncommitted" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "커밋되지 않음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6397,6 +9853,12 @@ "value" : "Unknown" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "알 수 없음" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6413,6 +9875,12 @@ "value" : "Unknown error." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "알 수 없는 오류입니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6429,6 +9897,12 @@ "value" : "Unrecognized pairing link. Generate a new QR code on your Mac." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인식할 수 없는 페어링 링크입니다. Mac에서 새 QR 코드를 생성하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6445,6 +9919,12 @@ "value" : "Unrecognized QR. Try scanning the one from the Mac app." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "인식할 수 없는 QR입니다. Mac 앱의 QR을 스캔해 보세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6461,6 +9941,12 @@ "value" : "Updated %@. Pull to refresh." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ 업데이트됨. 당겨서 새로고침하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6471,6 +9957,12 @@ }, "Upload" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "업로드" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6480,7 +9972,20 @@ } }, "URL" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL" + } + } + } }, "URLs" : { "localizations" : { @@ -6490,6 +9995,12 @@ "value" : "URLs" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "URL" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6506,6 +10017,12 @@ "value" : "Usage" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "사용량" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6522,6 +10039,12 @@ "value" : "Use a GitHub repository that exposes .claude-plugin/marketplace.json." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : ".claude-plugin/marketplace.json을 노출하는 GitHub 저장소를 사용하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6538,6 +10061,12 @@ "value" : "Use mobile search to find synced projects, thread titles, and desktop search hits." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모바일 검색을 사용하여 동기화된 프로젝트, 스레드 제목, 데스크톱 검색 결과를 찾으세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6548,6 +10077,12 @@ }, "Use Workspace" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "작업 공간 사용" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6564,6 +10099,12 @@ "value" : "Value" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "값" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6580,6 +10121,12 @@ "value" : "View Changes" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "변경사항 보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6590,6 +10137,12 @@ }, "View pull request" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "풀 리퀘스트 보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6600,6 +10153,12 @@ }, "View values" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "값 보기" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6609,7 +10168,20 @@ } }, "View workflow run on GitHub" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "GitHub에서 워크플로 실행 보기" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "在 GitHub 上查看工作流运行" + } + } + } }, "What should we build in %@?" : { "localizations" : { @@ -6619,6 +10191,12 @@ "value" : "What should we build in %@?" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@에서 무엇을 만들까요?" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6628,13 +10206,45 @@ } }, "Workflow" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "워크플로" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作流" + } + } + } }, "Workflow inputs" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "워크플로 입력" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "工作流输入" + } + } + } }, "Workflows" : { "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "워크플로" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6651,6 +10261,12 @@ "value" : "Working Directory" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "작업 디렉터리" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6660,7 +10276,20 @@ } }, "Working…" : { - + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "작업 중…" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "处理中…" + } + } + } }, "Xcode" : { "localizations" : { @@ -6670,6 +10299,12 @@ "value" : "Xcode" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xcode" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6686,6 +10321,12 @@ "value" : "Xcode Configuration" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xcode 구성" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6702,6 +10343,12 @@ "value" : "Xcode Project" } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xcode 프로젝트" + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6718,6 +10365,12 @@ "value" : "Your Mac declined the pairing request." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 페어링 요청을 거부했습니다." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", @@ -6734,6 +10387,12 @@ "value" : "Your Mac didn't respond. Make sure RxCode is open and connected, then try again." } }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mac이 응답하지 않았습니다. RxCode가 열려 있고 연결되어 있는지 확인한 후 다시 시도하세요." + } + }, "zh-Hans" : { "stringUnit" : { "state" : "translated", diff --git a/RxCodeMobile/Views/Autopilot/ProjectAutopilotMenu.swift b/RxCodeMobile/Views/Autopilot/ProjectAutopilotMenu.swift index 881a2298..4b9acff9 100644 --- a/RxCodeMobile/Views/Autopilot/ProjectAutopilotMenu.swift +++ b/RxCodeMobile/Views/Autopilot/ProjectAutopilotMenu.swift @@ -44,6 +44,13 @@ struct ProjectAutopilotMenuItems: View { @Binding var showReleaseCreate: Bool @Binding var setupChat: AutopilotSetupChat? @Binding var info: AutopilotMenuInfo? + /// Create-PR support (mirrors the desktop briefing "Create PR" button). The + /// in-flight state and the actual request live with the host so the loading + /// dialog can attach there; this view only renders the gated menu item. + var branch: String? = nil + var prNumber: Int? = nil + var isCreatingPR: Bool = false + var onCreatePR: () -> Void = {} var body: some View { // Mirror the desktop guard: no repo → no autopilot items. @@ -53,6 +60,7 @@ struct ProjectAutopilotMenuItems: View { secretsItem(status) docsItem(status) releaseItem(status) + createPRItem() } else { Button {} label: { Label("Loading Autopilot…", systemImage: "hourglass") @@ -117,6 +125,21 @@ struct ProjectAutopilotMenuItems: View { } } } + + @ViewBuilder + private func createPRItem() -> some View { + // Offer "Create PR" for a real branch with no open PR yet (mirrors the + // desktop briefing PR button); once a PR exists the host's "Open Pull + // Request" link covers it. + if let branch, branch.lowercased() != "unknown", prNumber == nil { + Button { + onCreatePR() + } label: { + Label("Create Pull Request", systemImage: "arrow.triangle.pull.request") + } + .disabled(isCreatingPR) + } + } } // MARK: - Host modifier diff --git a/RxCodeMobile/Views/MobileBriefingDetailView.swift b/RxCodeMobile/Views/MobileBriefingDetailView.swift index 1b324cfb..ddadfa14 100644 --- a/RxCodeMobile/Views/MobileBriefingDetailView.swift +++ b/RxCodeMobile/Views/MobileBriefingDetailView.swift @@ -59,19 +59,15 @@ struct MobileBriefingDetailView: View { showDownloadSheet: $showingSecretsDownload, showReleaseCreate: $showingReleaseCreate, setupChat: $autopilotSetupChat, - info: $autopilotInfo + info: $autopilotInfo, + // Offer "Create PR" for a real branch with no open + // PR yet; once a PR exists the "Open Pull Request" + // link below covers it. + branch: isUnknownBranch ? nil : groupKey.branch, + prNumber: ciStatus?.prNumber, + isCreatingPR: isCreatingPR, + onCreatePR: { createPullRequest(project: project) } ) - // Offer "Create PR" for a real branch with no open PR - // yet (mirrors the desktop briefing PR button); once a - // PR exists the "Open Pull Request" link below covers it. - if !isUnknownBranch && !gitHubURLIsPullRequest { - Button { - createPullRequest(project: project) - } label: { - Label("Create Pull Request", systemImage: "arrow.triangle.pull.request") - } - .disabled(isCreatingPR) - } if gitHubURL != nil { Divider() } } if let gitHubURL { diff --git a/RxCodeMobile/Views/MobileRunProfilesView.swift b/RxCodeMobile/Views/MobileRunProfilesView.swift new file mode 100644 index 00000000..0fb7793d --- /dev/null +++ b/RxCodeMobile/Views/MobileRunProfilesView.swift @@ -0,0 +1,365 @@ +import RxCodeCore +import RxCodeSync +import SwiftUI + +struct MobileRunProfilesView: View { + @EnvironmentObject private var state: MobileAppState + @Environment(\.dismiss) private var dismiss + let projectID: UUID + @State private var editingProfile: RunProfile? + + private var project: Project? { + state.projects.first { $0.id == projectID } + } + + private var profiles: [RunProfile] { + state.runProfiles(for: projectID).sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } + } + + private var tasks: [MobileRunTaskSnapshot] { + state.runTasks(for: projectID) + } + + /// Desktop-detected runnables for this project, or an empty set until the + /// detection request resolves. + private var detected: DetectedRunnables { + state.detectedRunnablesByProject[projectID] ?? DetectedRunnables() + } + + var body: some View { + List { + if !tasks.isEmpty { + Section("Runs") { + ForEach(tasks) { task in + runTaskRow(task) + } + } + } + + Section("Profiles") { + if profiles.isEmpty { + ContentUnavailableView( + "No Run Profiles", + systemImage: "play.rectangle", + description: Text("Create a profile to run a command on your Mac.") + ) + } else { + ForEach(profiles) { profile in + profileRow(profile) + } + } + } + } + .navigationTitle(project?.name ?? "Run Profiles") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { dismiss() } + } + ToolbarItem(placement: .primaryAction) { + addMenu + } + } + .task { + await state.requestRunnableDetection(projectID: projectID) + } + .refreshable { + await state.refreshSnapshot() + await state.requestRunnableDetection(projectID: projectID) + } + .sheet(item: $editingProfile) { profile in + NavigationStack { + MobileRunProfileEditorView(profile: profile, projectID: projectID) + .environmentObject(state) + } + .mobileSheetPresentation() + } + .alert("Run Profile Error", isPresented: Binding( + get: { state.lastRunProfileError != nil }, + set: { if !$0 { state.lastRunProfileError = nil } } + )) { + Button("OK", role: .cancel) { state.lastRunProfileError = nil } + } message: { + Text(state.lastRunProfileError ?? "") + } + } + + private func profileRow(_ profile: RunProfile) -> some View { + let task = state.runningTask(projectID: projectID, profileID: profile.id) + return HStack(spacing: 12) { + Image(systemName: iconName(for: profile.type)) + .foregroundStyle(.secondary) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 3) { + Text(profile.name.isEmpty ? "Untitled" : profile.name) + .lineLimit(1) + Text(profileSubtitle(profile)) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + + Spacer() + + if let task { + Button { + Task { await state.stopRunTask(task) } + } label: { + Image(systemName: "stop.fill") + } + .buttonStyle(.bordered) + .controlSize(.small) + .tint(.red) + } else { + Button { + Task { await state.runProfile(projectID: projectID, profileID: profile.id) } + } label: { + Image(systemName: "play.fill") + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + } + } + .contentShape(Rectangle()) + .onTapGesture { + editingProfile = profile + } + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + Task { await state.deleteRunProfile(projectID: projectID, profileID: profile.id) } + } label: { + Label("Delete", systemImage: "trash") + } + } + .contextMenu { + Button("Edit") { editingProfile = profile } + Button("Duplicate") { + var copy = profile + copy.id = UUID() + copy.name = profile.name + " (copy)" + copy.createdAt = Date() + copy.updatedAt = Date() + Task { await state.saveRunProfile(copy, projectID: projectID) } + } + Button("Delete", role: .destructive) { + Task { await state.deleteRunProfile(projectID: projectID, profileID: profile.id) } + } + } + } + + private func runTaskRow(_ task: MobileRunTaskSnapshot) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Label(task.profileName, systemImage: task.isRunning ? "play.circle.fill" : "checkmark.circle") + .foregroundStyle(task.isRunning ? Color.accentColor : .secondary) + Spacer() + Text(task.statusLabel) + .font(.caption) + .foregroundStyle(.secondary) + } + + if !task.commandPreview.isEmpty { + Text(task.commandPreview) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.secondary) + .lineLimit(4) + } + + if let output = task.terminalOutputTail, !output.isEmpty { + ScrollView([.horizontal, .vertical]) { + Text(output) + .font(.system(.caption2, design: .monospaced)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + .frame(maxHeight: 180) + } + + if task.isRunning { + Button(role: .destructive) { + Task { await state.stopRunTask(task) } + } label: { + Label("Stop", systemImage: "stop.fill") + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + .padding(.vertical, 4) + } + + private func iconName(for type: RunProfileType) -> String { + switch type { + case .bash: return "terminal" + case .xcode: return "hammer.fill" + case .make: return "wrench.and.screwdriver.fill" + case .packageScript: return "shippingbox.fill" + } + } + + private func profileSubtitle(_ profile: RunProfile) -> String { + switch profile.type { + case .bash: + return profile.bash.command.isEmpty ? "Bash command" : profile.bash.command + case .xcode: + let xcode = profile.xcode ?? XcodeRunConfig() + return [xcode.container, xcode.scheme, xcode.action.rawValue].filter { !$0.isEmpty }.joined(separator: " · ") + case .make: + let make = profile.make ?? MakeRunConfig() + return ["make", make.target, make.arguments].filter { !$0.isEmpty }.joined(separator: " ") + case .packageScript: + let pkg = profile.package ?? PackageRunConfig() + return [pkg.packageManager.runPrefix, pkg.script, pkg.arguments].filter { !$0.isEmpty }.joined(separator: " ") + } + } + + /// "+" menu: blank profiles plus everything the desktop auto-detected for + /// this project. Picking a detected entry materializes a pre-filled profile. + private var addMenu: some View { + Menu { + Section("New") { + Button { + editingProfile = Self.newProfile(projectID: projectID, type: .bash) + } label: { + Label("Bash Configuration", systemImage: "terminal") + } + Button { + editingProfile = Self.newProfile(projectID: projectID, type: .xcode) + } label: { + Label("Xcode Configuration", systemImage: "hammer.fill") + } + Button { + editingProfile = Self.newProfile(projectID: projectID, type: .make) + } label: { + Label("Make Configuration", systemImage: "wrench.and.screwdriver.fill") + } + Button { + editingProfile = Self.newProfile(projectID: projectID, type: .packageScript) + } label: { + Label("Package Configuration", systemImage: "shippingbox.fill") + } + } + + if !detected.xcode.isEmpty { + Section("Detected · Xcode") { + ForEach(detected.xcode) { runnable in + Button { + editingProfile = Self.profile(from: runnable, projectID: projectID) + } label: { + Label(runnable.displayName, systemImage: "hammer.fill") + } + } + } + } + if !detected.make.isEmpty { + Section("Detected · Make") { + ForEach(detected.make) { runnable in + Button { + editingProfile = Self.profile(from: runnable, projectID: projectID) + } label: { + Label(runnable.displayName, systemImage: "wrench.and.screwdriver.fill") + } + } + } + } + if !detected.npm.isEmpty { + Section("Detected · Scripts") { + ForEach(detected.npm) { runnable in + Button { + editingProfile = Self.profile(from: runnable, projectID: projectID) + } label: { + Label(runnable.displayName, systemImage: "shippingbox.fill") + } + } + } + } + } label: { + Image(systemName: "plus") + } + } + + private static func newProfile(projectID: UUID, type: RunProfileType) -> RunProfile { + let now = Date() + switch type { + case .bash: + return RunProfile( + projectId: projectID, + name: "New Bash Configuration", + type: .bash, + bash: BashRunConfig(), + createdAt: now, + updatedAt: now + ) + case .xcode: + return RunProfile( + projectId: projectID, + name: "New Xcode Configuration", + type: .xcode, + xcode: XcodeRunConfig(), + createdAt: now, + updatedAt: now + ) + case .make: + return RunProfile( + projectId: projectID, + name: "New Make Configuration", + type: .make, + make: MakeRunConfig(), + createdAt: now, + updatedAt: now + ) + case .packageScript: + return RunProfile( + projectId: projectID, + name: "New Package Configuration", + type: .packageScript, + package: PackageRunConfig(), + createdAt: now, + updatedAt: now + ) + } + } + + /// Materialize a desktop-detected runnable into an editable `RunProfile`, + /// mirroring the desktop's `RunConfigurationsView.addProfile(from:)`. + private static func profile(from runnable: DetectedRunnable, projectID: UUID) -> RunProfile { + let now = Date() + if let xcode = runnable.xcode { + return RunProfile( + projectId: projectID, + name: runnable.displayName, + type: .xcode, + xcode: xcode, + createdAt: now, + updatedAt: now + ) + } + if let make = runnable.make { + return RunProfile( + projectId: projectID, + name: runnable.displayName, + type: .make, + make: make, + createdAt: now, + updatedAt: now + ) + } + if let package = runnable.package { + return RunProfile( + projectId: projectID, + name: runnable.displayName, + type: .packageScript, + package: package, + createdAt: now, + updatedAt: now + ) + } + return RunProfile( + projectId: projectID, + name: runnable.displayName, + bash: BashRunConfig(command: runnable.command), + createdAt: now, + updatedAt: now + ) + } +} diff --git a/RxCodeMobile/Views/SessionsList.swift b/RxCodeMobile/Views/SessionsList.swift index c9381dd6..6aa0e469 100644 --- a/RxCodeMobile/Views/SessionsList.swift +++ b/RxCodeMobile/Views/SessionsList.swift @@ -23,6 +23,7 @@ enum MobileDraftSessionID { struct SessionsList: View { @EnvironmentObject private var state: MobileAppState @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) private var openURL let projectID: UUID @Binding var selected: String? var usesSelection = true @@ -30,6 +31,7 @@ struct SessionsList: View { @State private var showingNewThread = false @State private var showingDeleteProjectConfirm = false @State private var showingSearch = false + @State private var isCreatingPR = false @Namespace private var glassNamespace // Autopilot actions (1:1 with the desktop project menu), moved here from the @@ -44,6 +46,16 @@ struct SessionsList: View { state.projects.first { $0.id == projectID } } + /// Current branch resolved on the desktop for this project. + private var currentBranch: String? { + state.projectBranches[projectID] + } + + /// CI/PR status for the project's current branch, used to gate "Create PR". + private var ciStatus: ProjectCIStatus? { + state.ciStatusByProject[projectID] + } + /// Number of rows currently materialized. The list grows in `pageSize` /// increments as the user scrolls so we never render every thread at once. @State private var displayLimit = SessionsList.pageSize @@ -118,6 +130,31 @@ struct SessionsList: View { } message: { Text("This removes “\(project?.name ?? "this project")” and all its threads from RxCode. Your files on the Mac are not deleted.") } + .mobileAutopilotLoadingDialog( + isCreatingPR, + title: "Creating Pull Request…", + message: "The Mac is pushing the branch and opening the PR." + ) + } + + /// Ask the Mac to open a PR for the project's current branch, then open it + /// in the browser (mirrors the desktop briefing "Create PR" button). + private func createPullRequest(project: Project, branch: String) { + guard !isCreatingPR else { return } + isCreatingPR = true + Task { + defer { isCreatingPR = false } + do { + let url = try await state.requestProjectCreatePullRequest( + projectId: project.id, + branch: branch + ) + await state.refreshSnapshot() + openURL(url) + } catch { + autopilotInfo = AutopilotMenuInfo(text: error.localizedDescription, isError: true) + } + } } // MARK: - Toolbar @@ -135,7 +172,15 @@ struct SessionsList: View { showDownloadSheet: $showingSecretsDownload, showReleaseCreate: $showingReleaseCreate, setupChat: $autopilotSetupChat, - info: $autopilotInfo + info: $autopilotInfo, + branch: currentBranch, + prNumber: ciStatus?.prNumber, + isCreatingPR: isCreatingPR, + onCreatePR: { + if let branch = currentBranch { + createPullRequest(project: project, branch: branch) + } + } ) Divider() } @@ -322,368 +367,6 @@ struct SessionsList: View { } } -struct MobileRunProfilesView: View { - @EnvironmentObject private var state: MobileAppState - @Environment(\.dismiss) private var dismiss - let projectID: UUID - @State private var editingProfile: RunProfile? - - private var project: Project? { - state.projects.first { $0.id == projectID } - } - - private var profiles: [RunProfile] { - state.runProfiles(for: projectID).sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } - } - - private var tasks: [MobileRunTaskSnapshot] { - state.runTasks(for: projectID) - } - - /// Desktop-detected runnables for this project, or an empty set until the - /// detection request resolves. - private var detected: DetectedRunnables { - state.detectedRunnablesByProject[projectID] ?? DetectedRunnables() - } - - var body: some View { - List { - if !tasks.isEmpty { - Section("Runs") { - ForEach(tasks) { task in - runTaskRow(task) - } - } - } - - Section("Profiles") { - if profiles.isEmpty { - ContentUnavailableView( - "No Run Profiles", - systemImage: "play.rectangle", - description: Text("Create a profile to run a command on your Mac.") - ) - } else { - ForEach(profiles) { profile in - profileRow(profile) - } - } - } - } - .navigationTitle(project?.name ?? "Run Profiles") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Done") { dismiss() } - } - ToolbarItem(placement: .primaryAction) { - addMenu - } - } - .task { - await state.requestRunnableDetection(projectID: projectID) - } - .refreshable { - await state.refreshSnapshot() - await state.requestRunnableDetection(projectID: projectID) - } - .sheet(item: $editingProfile) { profile in - NavigationStack { - MobileRunProfileEditorView(profile: profile, projectID: projectID) - .environmentObject(state) - } - .mobileSheetPresentation() - } - .alert("Run Profile Error", isPresented: Binding( - get: { state.lastRunProfileError != nil }, - set: { if !$0 { state.lastRunProfileError = nil } } - )) { - Button("OK", role: .cancel) { state.lastRunProfileError = nil } - } message: { - Text(state.lastRunProfileError ?? "") - } - } - - private func profileRow(_ profile: RunProfile) -> some View { - let task = state.runningTask(projectID: projectID, profileID: profile.id) - return HStack(spacing: 12) { - Image(systemName: iconName(for: profile.type)) - .foregroundStyle(.secondary) - .frame(width: 24) - - VStack(alignment: .leading, spacing: 3) { - Text(profile.name.isEmpty ? "Untitled" : profile.name) - .lineLimit(1) - Text(profileSubtitle(profile)) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } - - Spacer() - - if let task { - Button { - Task { await state.stopRunTask(task) } - } label: { - Image(systemName: "stop.fill") - } - .buttonStyle(.bordered) - .controlSize(.small) - .tint(.red) - } else { - Button { - Task { await state.runProfile(projectID: projectID, profileID: profile.id) } - } label: { - Image(systemName: "play.fill") - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - } - .contentShape(Rectangle()) - .onTapGesture { - editingProfile = profile - } - .swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button(role: .destructive) { - Task { await state.deleteRunProfile(projectID: projectID, profileID: profile.id) } - } label: { - Label("Delete", systemImage: "trash") - } - } - .contextMenu { - Button("Edit") { editingProfile = profile } - Button("Duplicate") { - var copy = profile - copy.id = UUID() - copy.name = profile.name + " (copy)" - copy.createdAt = Date() - copy.updatedAt = Date() - Task { await state.saveRunProfile(copy, projectID: projectID) } - } - Button("Delete", role: .destructive) { - Task { await state.deleteRunProfile(projectID: projectID, profileID: profile.id) } - } - } - } - - private func runTaskRow(_ task: MobileRunTaskSnapshot) -> some View { - VStack(alignment: .leading, spacing: 6) { - HStack { - Label(task.profileName, systemImage: task.isRunning ? "play.circle.fill" : "checkmark.circle") - .foregroundStyle(task.isRunning ? Color.accentColor : .secondary) - Spacer() - Text(task.statusLabel) - .font(.caption) - .foregroundStyle(.secondary) - } - - if !task.commandPreview.isEmpty { - Text(task.commandPreview) - .font(.system(.caption, design: .monospaced)) - .foregroundStyle(.secondary) - .lineLimit(4) - } - - if let output = task.terminalOutputTail, !output.isEmpty { - ScrollView([.horizontal, .vertical]) { - Text(output) - .font(.system(.caption2, design: .monospaced)) - .foregroundStyle(.secondary) - .textSelection(.enabled) - } - .frame(maxHeight: 180) - } - - if task.isRunning { - Button(role: .destructive) { - Task { await state.stopRunTask(task) } - } label: { - Label("Stop", systemImage: "stop.fill") - } - .buttonStyle(.bordered) - .controlSize(.small) - } - } - .padding(.vertical, 4) - } - - private func iconName(for type: RunProfileType) -> String { - switch type { - case .bash: return "terminal" - case .xcode: return "hammer.fill" - case .make: return "wrench.and.screwdriver.fill" - case .packageScript: return "shippingbox.fill" - } - } - - private func profileSubtitle(_ profile: RunProfile) -> String { - switch profile.type { - case .bash: - return profile.bash.command.isEmpty ? "Bash command" : profile.bash.command - case .xcode: - let xcode = profile.xcode ?? XcodeRunConfig() - return [xcode.container, xcode.scheme, xcode.action.rawValue].filter { !$0.isEmpty }.joined(separator: " · ") - case .make: - let make = profile.make ?? MakeRunConfig() - return ["make", make.target, make.arguments].filter { !$0.isEmpty }.joined(separator: " ") - case .packageScript: - let pkg = profile.package ?? PackageRunConfig() - return [pkg.packageManager.runPrefix, pkg.script, pkg.arguments].filter { !$0.isEmpty }.joined(separator: " ") - } - } - - /// "+" menu: blank profiles plus everything the desktop auto-detected for - /// this project. Picking a detected entry materializes a pre-filled profile. - private var addMenu: some View { - Menu { - Section("New") { - Button { - editingProfile = Self.newProfile(projectID: projectID, type: .bash) - } label: { - Label("Bash Configuration", systemImage: "terminal") - } - Button { - editingProfile = Self.newProfile(projectID: projectID, type: .xcode) - } label: { - Label("Xcode Configuration", systemImage: "hammer.fill") - } - Button { - editingProfile = Self.newProfile(projectID: projectID, type: .make) - } label: { - Label("Make Configuration", systemImage: "wrench.and.screwdriver.fill") - } - Button { - editingProfile = Self.newProfile(projectID: projectID, type: .packageScript) - } label: { - Label("Package Configuration", systemImage: "shippingbox.fill") - } - } - - if !detected.xcode.isEmpty { - Section("Detected · Xcode") { - ForEach(detected.xcode) { runnable in - Button { - editingProfile = Self.profile(from: runnable, projectID: projectID) - } label: { - Label(runnable.displayName, systemImage: "hammer.fill") - } - } - } - } - if !detected.make.isEmpty { - Section("Detected · Make") { - ForEach(detected.make) { runnable in - Button { - editingProfile = Self.profile(from: runnable, projectID: projectID) - } label: { - Label(runnable.displayName, systemImage: "wrench.and.screwdriver.fill") - } - } - } - } - if !detected.npm.isEmpty { - Section("Detected · Scripts") { - ForEach(detected.npm) { runnable in - Button { - editingProfile = Self.profile(from: runnable, projectID: projectID) - } label: { - Label(runnable.displayName, systemImage: "shippingbox.fill") - } - } - } - } - } label: { - Image(systemName: "plus") - } - } - - private static func newProfile(projectID: UUID, type: RunProfileType) -> RunProfile { - let now = Date() - switch type { - case .bash: - return RunProfile( - projectId: projectID, - name: "New Bash Configuration", - type: .bash, - bash: BashRunConfig(), - createdAt: now, - updatedAt: now - ) - case .xcode: - return RunProfile( - projectId: projectID, - name: "New Xcode Configuration", - type: .xcode, - xcode: XcodeRunConfig(), - createdAt: now, - updatedAt: now - ) - case .make: - return RunProfile( - projectId: projectID, - name: "New Make Configuration", - type: .make, - make: MakeRunConfig(), - createdAt: now, - updatedAt: now - ) - case .packageScript: - return RunProfile( - projectId: projectID, - name: "New Package Configuration", - type: .packageScript, - package: PackageRunConfig(), - createdAt: now, - updatedAt: now - ) - } - } - - /// Materialize a desktop-detected runnable into an editable `RunProfile`, - /// mirroring the desktop's `RunConfigurationsView.addProfile(from:)`. - private static func profile(from runnable: DetectedRunnable, projectID: UUID) -> RunProfile { - let now = Date() - if let xcode = runnable.xcode { - return RunProfile( - projectId: projectID, - name: runnable.displayName, - type: .xcode, - xcode: xcode, - createdAt: now, - updatedAt: now - ) - } - if let make = runnable.make { - return RunProfile( - projectId: projectID, - name: runnable.displayName, - type: .make, - make: make, - createdAt: now, - updatedAt: now - ) - } - if let package = runnable.package { - return RunProfile( - projectId: projectID, - name: runnable.displayName, - type: .packageScript, - package: package, - createdAt: now, - updatedAt: now - ) - } - return RunProfile( - projectId: projectID, - name: runnable.displayName, - bash: BashRunConfig(command: runnable.command), - createdAt: now, - updatedAt: now - ) - } -} - // MARK: - Previews #if DEBUG