From f7047a8fc781e1a1c7cc7362f2b7cd00a384f46a Mon Sep 17 00:00:00 2001 From: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:17:10 +0000 Subject: [PATCH] feat(mobile): redesigned Changes tab with file-first rows and swipe to discard (#37214) Rows lead with the file name and put its folder under it, so names stop truncating. The whole row opens the diff, one quiet button stages or unstages, and discard moves to a left swipe that still asks first. A status line, a Commits row and a conflict banner with Abort sit above Uncommitted and Staged sections that carry Stage all and Unstage all. The rarer git actions move into the header's grouped menu. --- ...-git.changes-load-git.branchcompare-1.json | 228 ++++--- .../matrix-git.changes-load-git.status-1.json | 628 +++++++++--------- .../matrix-git.changes-load-repo.list-1.json | 228 ++++--- ...trix-git.changes-load-worktree.show-1.json | 228 ++++--- .../goldens/sc-changes-loaded.json | 188 +++--- mobile/rpc-foundation/pilot-scenarios.json | 3 +- .../src/components/nav-bar/PullDownMenu.tsx | 101 +-- mobile/src/i18n/en-runtime-required.json | 10 +- mobile/src/i18n/locales/de.json | 40 +- mobile/src/i18n/locales/en.json | 72 +- mobile/src/i18n/locales/es.json | 43 +- mobile/src/i18n/locales/fr.json | 43 +- mobile/src/i18n/locales/ja.json | 40 +- mobile/src/i18n/locales/ko.json | 40 +- mobile/src/i18n/locales/pt.json | 43 +- mobile/src/i18n/locales/ru.json | 46 +- mobile/src/i18n/locales/tr.json | 40 +- mobile/src/i18n/locales/zh-Hant.json | 40 +- mobile/src/i18n/locales/zh.json | 40 +- .../MobileSourceControlBranchCard.tsx | 101 --- .../MobileSourceControlChangesHeader.tsx | 192 ++++++ .../MobileSourceControlContent.test.tsx | 64 +- .../MobileSourceControlContent.tsx | 231 ++++--- .../MobileSourceControlFileList.tsx | 173 +++-- .../MobileSourceControlFileList.web.test.tsx | 32 +- .../MobileSourceControlFileRows.tsx | 544 ++++++++------- .../MobileSourceControlModals.tsx | 15 +- .../MobileSourceControlPanel.tsx | 92 ++- .../MobileSourceControlPrChip.tsx | 147 ---- .../mobile-branch-compare.test.ts | 19 +- .../source-control/mobile-branch-compare.ts | 32 - .../mobile-branch-entry-format.ts | 12 - .../source-control/mobile-git-status.test.ts | 4 +- .../src/source-control/mobile-git-status.ts | 4 +- .../mobile-pr-chip-summary.test.ts | 166 ----- .../source-control/mobile-pr-chip-summary.ts | 95 --- .../mobile-source-control-actions.ts | 25 +- .../mobile-source-control-list-styles.ts | 54 +- .../mobile-source-control-styles.ts | 111 ++-- .../source-control-status-retry.test.tsx | 4 +- .../use-mobile-source-control-loaders.ts | 5 +- .../use-mobile-source-control-state.ts | 14 +- 42 files changed, 1837 insertions(+), 2400 deletions(-) delete mode 100644 mobile/src/source-control/MobileSourceControlBranchCard.tsx create mode 100644 mobile/src/source-control/MobileSourceControlChangesHeader.tsx delete mode 100644 mobile/src/source-control/MobileSourceControlPrChip.tsx delete mode 100644 mobile/src/source-control/mobile-branch-entry-format.ts delete mode 100644 mobile/src/source-control/mobile-pr-chip-summary.test.ts delete mode 100644 mobile/src/source-control/mobile-pr-chip-summary.ts diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json index ecd62d820e2..1cf8b9ecc75 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.branchcompare-1.json @@ -163,11 +163,6 @@ } } }, - "2bf960db8adf": { - "name": "git.status#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\",\"includeLineStats\":false}}" - }, "2cb17031e16a": { "branchCompareState": { "kind": "error", @@ -381,6 +376,62 @@ "startedAt": 0 } }, + "6a5580a606dc": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "branch": "feature", + "entries": [ + { + "added": 4, + "area": "unstaged", + "path": "src/app.ts", + "removed": 1, + "status": "modified" + }, + { + "area": "staged", + "oldPath": "src/older.ts", + "path": "src/old.ts", + "status": "renamed" + } + ], + "head": "head-oid", + "upstreamStatus": { + "ahead": 1, + "behind": 0, + "hasUpstream": true, + "upstreamName": "origin/feature" + } + } + } + } + }, "6d220bb7b42a": { "name": "worktree.show#1", "ordinal": 5, @@ -407,6 +458,32 @@ "startedAt": 0 } }, + "72e251ae531b": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, "830aa65db9d0": { "branchCompareState": { "kind": "idle" @@ -645,6 +722,11 @@ } } }, + "973ac78137f2": { + "name": "git.status#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" + }, "9b643a151075": { "name": "git.branchCompare#1", "ordinal": 11, @@ -729,95 +811,11 @@ } } }, - "aa8c5813ba8a": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "branch": "feature", - "entries": [ - { - "added": 4, - "area": "unstaged", - "path": "src/app.ts", - "removed": 1, - "status": "modified" - }, - { - "area": "staged", - "oldPath": "src/older.ts", - "path": "src/old.ts", - "status": "renamed" - } - ], - "head": "head-oid", - "upstreamStatus": { - "ahead": 1, - "behind": 0, - "hasUpstream": true, - "upstreamName": "origin/feature" - } - } - } - } - }, "ad2a2a1df35b": { "name": "worktree.show#1", "ordinal": 9, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" }, - "ae602a492660": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, "c2212473f0c1": { "name": "repo.list#1", "ordinal": 10, @@ -987,8 +985,8 @@ { "id": "sc-changes-loaded.prelude:status-pending", "observation": { - "sender": ["e3973bb12da1", "ae602a492660"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "72e251ae531b"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -999,8 +997,8 @@ { "id": "sc-changes-loaded.prelude:compare-pending", "observation": { - "sender": ["e3973bb12da1", "aa8c5813ba8a", "6d220bb7b42a", "587de4c7f7fc"], - "payloads": ["f648b042d810", "2bf960db8adf", "ad2a2a1df35b", "c2212473f0c1"], + "sender": ["e3973bb12da1", "6a5580a606dc", "6d220bb7b42a", "587de4c7f7fc"], + "payloads": ["f648b042d810", "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1013,14 +1011,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1037,14 +1035,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "893813584d8b" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1061,14 +1059,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "55d62e9e07b4" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1085,14 +1083,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "2b6257fd1172" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1109,14 +1107,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "1650cbe07807" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1133,14 +1131,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "a76331b97c21" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1157,14 +1155,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "967f1b227050" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1181,14 +1179,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "00fe64c7bc78" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1205,14 +1203,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "c75999ea5a6c" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1229,14 +1227,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "47205e8686ad" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1253,14 +1251,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "9b643a151075" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json index aade53b10dc..21a989a692d 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-git.status-1.json @@ -4,6 +4,75 @@ "family": "git.changes-load", "namedDeltas": [], "values": { + "04f6ea51fb83": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "0a645f4241a6": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, "19bb6c737352": { "branchCompareState": { "kind": "idle" @@ -65,6 +134,40 @@ } } }, + "200efb648720": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, "21ab22282843": { "branchCompareState": { "kind": "idle" @@ -83,11 +186,6 @@ "message": "Update Orca desktop to use Source Control on mobile." } }, - "2bf960db8adf": { - "name": "git.status#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\",\"includeLineStats\":false}}" - }, "35cc9e325d08": { "name": "git.branchCompare#1", "ordinal": 11, @@ -148,6 +246,41 @@ "message": "" } }, + "4d11fcc4af02": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-2", + "ok": false + } + } + }, "587de4c7f7fc": { "name": "repo.list#1", "ordinal": 6, @@ -183,7 +316,7 @@ "message": "outer refused" } }, - "60f6e2d48ef7": { + "6a5580a606dc": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -194,7 +327,6 @@ { "name": "params", "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -206,13 +338,37 @@ } ], "settlement": { - "status": "rejected", + "status": "fulfilled", "startedAt": 0, "settledAt": 0, - "error": { - "category": "Error", - "message": "transport failure", - "isRpcDeliveryUnknown": true + "value": { + "id": "frame-2", + "ok": true, + "result": { + "branch": "feature", + "entries": [ + { + "added": 4, + "area": "unstaged", + "path": "src/app.ts", + "removed": 1, + "status": "modified" + }, + { + "area": "staged", + "oldPath": "src/older.ts", + "path": "src/old.ts", + "status": "renamed" + } + ], + "head": "head-oid", + "upstreamStatus": { + "ahead": 1, + "behind": 0, + "hasUpstream": true, + "upstreamName": "origin/feature" + } + } } } }, @@ -242,16 +398,7 @@ "startedAt": 0 } }, - "74b7ff5e1818": { - "branchCompareState": { - "kind": "idle" - }, - "screenState": { - "kind": "error", - "message": "transport failure" - } - }, - "885a150a2ecd": { + "72e251ae531b": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -262,7 +409,6 @@ { "name": "params", "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -274,20 +420,20 @@ } ], "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "method_not_found", - "message": "Unknown method" - }, - "id": "frame-2", - "ok": false - } + "status": "pending", + "startedAt": 0 } }, - "8af3432e9742": { + "74b7ff5e1818": { + "branchCompareState": { + "kind": "idle" + }, + "screenState": { + "kind": "error", + "message": "transport failure" + } + }, + "7bb6d5b22b62": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -298,7 +444,6 @@ { "name": "params", "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -317,7 +462,9 @@ "id": "frame-2", "ok": true, "result": { - "error": "inner refused", + "error": { + "message": "inner refused" + }, "ok": false } } @@ -392,6 +539,37 @@ } } }, + "91448c30aa24": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true + } + } + }, "9536c968b88a": { "name": "repo.list#1", "ordinal": 6, @@ -426,7 +604,12 @@ } } }, - "9cedf6bac284": { + "973ac78137f2": { + "name": "git.status#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" + }, + "9809f1b5d628": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -437,7 +620,6 @@ { "name": "params", "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -449,16 +631,13 @@ } ], "settlement": { - "status": "fulfilled", + "status": "rejected", "startedAt": 0, "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "" - }, - "id": "frame-2", - "ok": false + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true } } }, @@ -470,47 +649,12 @@ "kind": "loading" } }, - "a0e97f94a377": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "$rpc": "null" - } - } - } - }, "a6482f604002": { "name": "source-control.status-load-success", "ordinal": 8, "value": {} }, - "aa8c5813ba8a": { + "aae82d534914": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -521,7 +665,6 @@ { "name": "params", "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -540,29 +683,8 @@ "id": "frame-2", "ok": true, "result": { - "branch": "feature", - "entries": [ - { - "added": 4, - "area": "unstaged", - "path": "src/app.ts", - "removed": 1, - "status": "modified" - }, - { - "area": "staged", - "oldPath": "src/older.ts", - "path": "src/old.ts", - "status": "renamed" - } - ], - "head": "head-oid", - "upstreamStatus": { - "ahead": 1, - "behind": 0, - "hasUpstream": true, - "upstreamName": "origin/feature" - } + "error": "inner refused", + "ok": false } } } @@ -572,7 +694,7 @@ "ordinal": 9, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" }, - "ae602a492660": { + "c212210ceca6": { "name": "git.status#1", "ordinal": 2, "args": [ @@ -583,139 +705,6 @@ { "name": "params", "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, - "b0d4cb96a6f3": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true - } - } - }, - "b660ecdc7911": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": "refused" - } - } - } - }, - "bbdf2a03af16": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": { - "message": "inner refused" - }, - "ok": false - } - } - } - }, - "c1923dfd79bf": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, "worktree": "id:repo42::/p" } }, @@ -754,6 +743,38 @@ } } }, + "d858ce00f4f0": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, "e2d7409ece1c": { "name": "git.branchCompare#1", "ordinal": 12, @@ -793,39 +814,6 @@ "$rpc": "undefined" } }, - "f5e9af0a8858": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "", - "isRpcDeliveryUnknown": true - } - } - }, "f648b042d810": { "name": "settings.get#1", "ordinal": 3, @@ -838,8 +826,8 @@ { "id": "sc-changes-loaded.prelude:status-pending", "observation": { - "sender": ["e3973bb12da1", "ae602a492660"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "72e251ae531b"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -850,8 +838,8 @@ { "id": "sc-changes-loaded.normal:compare-pending", "observation": { - "sender": ["e3973bb12da1", "aa8c5813ba8a", "6d220bb7b42a", "587de4c7f7fc"], - "payloads": ["f648b042d810", "2bf960db8adf", "ad2a2a1df35b", "c2212473f0c1"], + "sender": ["e3973bb12da1", "6a5580a606dc", "6d220bb7b42a", "587de4c7f7fc"], + "payloads": ["f648b042d810", "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1"], "settlements": { "mount": "eb79a9b3682a" }, @@ -864,14 +852,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -886,8 +874,8 @@ { "id": "sc-changes-loaded.result-absent:compare-pending", "observation": { - "sender": ["e3973bb12da1", "b0d4cb96a6f3"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "91448c30aa24"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -898,8 +886,8 @@ { "id": "sc-changes-loaded.result-absent:settled", "observation": { - "sender": ["e3973bb12da1", "b0d4cb96a6f3"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "91448c30aa24"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -910,8 +898,8 @@ { "id": "sc-changes-loaded.result-null:compare-pending", "observation": { - "sender": ["e3973bb12da1", "a0e97f94a377"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "200efb648720"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -922,8 +910,8 @@ { "id": "sc-changes-loaded.result-null:settled", "observation": { - "sender": ["e3973bb12da1", "a0e97f94a377"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "200efb648720"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -934,8 +922,8 @@ { "id": "sc-changes-loaded.inner-ok-missing:compare-pending", "observation": { - "sender": ["e3973bb12da1", "b660ecdc7911"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "04f6ea51fb83"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -946,8 +934,8 @@ { "id": "sc-changes-loaded.inner-ok-missing:settled", "observation": { - "sender": ["e3973bb12da1", "b660ecdc7911"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "04f6ea51fb83"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -958,8 +946,8 @@ { "id": "sc-changes-loaded.inner-false-string-error:compare-pending", "observation": { - "sender": ["e3973bb12da1", "8af3432e9742"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "aae82d534914"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -970,8 +958,8 @@ { "id": "sc-changes-loaded.inner-false-string-error:settled", "observation": { - "sender": ["e3973bb12da1", "8af3432e9742"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "aae82d534914"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -982,8 +970,8 @@ { "id": "sc-changes-loaded.inner-false-object-error:compare-pending", "observation": { - "sender": ["e3973bb12da1", "bbdf2a03af16"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "7bb6d5b22b62"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -994,8 +982,8 @@ { "id": "sc-changes-loaded.inner-false-object-error:settled", "observation": { - "sender": ["e3973bb12da1", "bbdf2a03af16"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "7bb6d5b22b62"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1006,8 +994,8 @@ { "id": "sc-changes-loaded.outer-refused:compare-pending", "observation": { - "sender": ["e3973bb12da1", "c1923dfd79bf"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "c212210ceca6"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1018,8 +1006,8 @@ { "id": "sc-changes-loaded.outer-refused:settled", "observation": { - "sender": ["e3973bb12da1", "c1923dfd79bf"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "c212210ceca6"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1030,8 +1018,8 @@ { "id": "sc-changes-loaded.outer-refused-no-message:compare-pending", "observation": { - "sender": ["e3973bb12da1", "9cedf6bac284"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "4d11fcc4af02"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1042,8 +1030,8 @@ { "id": "sc-changes-loaded.outer-refused-no-message:settled", "observation": { - "sender": ["e3973bb12da1", "9cedf6bac284"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "4d11fcc4af02"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1054,8 +1042,8 @@ { "id": "sc-changes-loaded.method-not-found:compare-pending", "observation": { - "sender": ["e3973bb12da1", "885a150a2ecd"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "0a645f4241a6"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1066,8 +1054,8 @@ { "id": "sc-changes-loaded.method-not-found:settled", "observation": { - "sender": ["e3973bb12da1", "885a150a2ecd"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "0a645f4241a6"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1078,8 +1066,8 @@ { "id": "sc-changes-loaded.transport-rejection:compare-pending", "observation": { - "sender": ["e3973bb12da1", "60f6e2d48ef7"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "d858ce00f4f0"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1090,8 +1078,8 @@ { "id": "sc-changes-loaded.transport-rejection:settled", "observation": { - "sender": ["e3973bb12da1", "60f6e2d48ef7"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "d858ce00f4f0"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1102,8 +1090,8 @@ { "id": "sc-changes-loaded.transport-rejection-no-message:compare-pending", "observation": { - "sender": ["e3973bb12da1", "f5e9af0a8858"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "9809f1b5d628"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -1114,8 +1102,8 @@ { "id": "sc-changes-loaded.transport-rejection-no-message:settled", "observation": { - "sender": ["e3973bb12da1", "f5e9af0a8858"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "9809f1b5d628"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json index 82cc3f5246d..7bc156d150a 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-repo.list-1.json @@ -159,11 +159,6 @@ } } }, - "2bf960db8adf": { - "name": "git.status#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\",\"includeLineStats\":false}}" - }, "35cc9e325d08": { "name": "git.branchCompare#1", "ordinal": 11, @@ -273,6 +268,62 @@ "startedAt": 0 } }, + "6a5580a606dc": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "branch": "feature", + "entries": [ + { + "added": 4, + "area": "unstaged", + "path": "src/app.ts", + "removed": 1, + "status": "modified" + }, + { + "area": "staged", + "oldPath": "src/older.ts", + "path": "src/old.ts", + "status": "renamed" + } + ], + "head": "head-oid", + "upstreamStatus": { + "ahead": 1, + "behind": 0, + "hasUpstream": true, + "upstreamName": "origin/feature" + } + } + } + } + }, "6b974d7f9507": { "name": "repo.list#1", "ordinal": 6, @@ -371,6 +422,32 @@ "startedAt": 0 } }, + "72e251ae531b": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, "84280af3ca36": { "name": "repo.list#1", "ordinal": 6, @@ -541,6 +618,11 @@ } } }, + "973ac78137f2": { + "name": "git.status#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" + }, "a0e527bdf205": { "branchCompareState": { "kind": "idle" @@ -554,95 +636,11 @@ "ordinal": 8, "value": {} }, - "aa8c5813ba8a": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "branch": "feature", - "entries": [ - { - "added": 4, - "area": "unstaged", - "path": "src/app.ts", - "removed": 1, - "status": "modified" - }, - { - "area": "staged", - "oldPath": "src/older.ts", - "path": "src/old.ts", - "status": "renamed" - } - ], - "head": "head-oid", - "upstreamStatus": { - "ahead": 1, - "behind": 0, - "hasUpstream": true, - "upstreamName": "origin/feature" - } - } - } - } - }, "ad2a2a1df35b": { "name": "worktree.show#1", "ordinal": 9, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" }, - "ae602a492660": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, "bc1d0b381f25": { "name": "repo.list#1", "ordinal": 6, @@ -774,8 +772,8 @@ { "id": "sc-changes-loaded.prelude:status-pending", "observation": { - "sender": ["e3973bb12da1", "ae602a492660"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "72e251ae531b"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -786,8 +784,8 @@ { "id": "sc-changes-loaded.prelude:compare-pending", "observation": { - "sender": ["e3973bb12da1", "aa8c5813ba8a", "6d220bb7b42a", "587de4c7f7fc"], - "payloads": ["f648b042d810", "2bf960db8adf", "ad2a2a1df35b", "c2212473f0c1"], + "sender": ["e3973bb12da1", "6a5580a606dc", "6d220bb7b42a", "587de4c7f7fc"], + "payloads": ["f648b042d810", "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1"], "settlements": { "mount": "eb79a9b3682a" }, @@ -800,14 +798,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -824,14 +822,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "bc1d0b381f25", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -848,14 +846,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "204b32e0c54e", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -872,14 +870,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "042d01f3263f", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -896,14 +894,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "26929519c12a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -920,14 +918,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "6b974d7f9507", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -944,14 +942,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "877dcd616e1f", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -968,14 +966,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "6b9dc51089ad", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -992,14 +990,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "c7a8ab38e974", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1016,14 +1014,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "84280af3ca36", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -1040,14 +1038,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "49197a7eac62", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" diff --git a/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json b/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json index 4d4f3b48e14..2157784b934 100644 --- a/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json +++ b/mobile/rpc-foundation/goldens/matrix-git.changes-load-worktree.show-1.json @@ -56,11 +56,6 @@ } } }, - "2bf960db8adf": { - "name": "git.status#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\",\"includeLineStats\":false}}" - }, "35cc9e325d08": { "name": "git.branchCompare#1", "ordinal": 11, @@ -209,6 +204,62 @@ "startedAt": 0 } }, + "6a5580a606dc": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "branch": "feature", + "entries": [ + { + "added": 4, + "area": "unstaged", + "path": "src/app.ts", + "removed": 1, + "status": "modified" + }, + { + "area": "staged", + "oldPath": "src/older.ts", + "path": "src/old.ts", + "status": "renamed" + } + ], + "head": "head-oid", + "upstreamStatus": { + "ahead": 1, + "behind": 0, + "hasUpstream": true, + "upstreamName": "origin/feature" + } + } + } + } + }, "6d220bb7b42a": { "name": "worktree.show#1", "ordinal": 5, @@ -235,6 +286,32 @@ "startedAt": 0 } }, + "72e251ae531b": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, "7440046c1348": { "name": "worktree.show#1", "ordinal": 5, @@ -441,6 +518,11 @@ } } }, + "973ac78137f2": { + "name": "git.status#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" + }, "a0e527bdf205": { "branchCompareState": { "kind": "idle" @@ -454,63 +536,6 @@ "ordinal": 8, "value": {} }, - "aa8c5813ba8a": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "branch": "feature", - "entries": [ - { - "added": 4, - "area": "unstaged", - "path": "src/app.ts", - "removed": 1, - "status": "modified" - }, - { - "area": "staged", - "oldPath": "src/older.ts", - "path": "src/old.ts", - "status": "renamed" - } - ], - "head": "head-oid", - "upstreamStatus": { - "ahead": 1, - "behind": 0, - "hasUpstream": true, - "upstreamName": "origin/feature" - } - } - } - } - }, "ad2a2a1df35b": { "name": "worktree.show#1", "ordinal": 9, @@ -551,33 +576,6 @@ } } }, - "ae602a492660": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, "b7a249bf3627": { "name": "worktree.show#1", "ordinal": 5, @@ -805,8 +803,8 @@ { "id": "sc-changes-loaded.prelude:status-pending", "observation": { - "sender": ["e3973bb12da1", "ae602a492660"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "72e251ae531b"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -817,8 +815,8 @@ { "id": "sc-changes-loaded.prelude:compare-pending", "observation": { - "sender": ["e3973bb12da1", "aa8c5813ba8a", "6d220bb7b42a", "587de4c7f7fc"], - "payloads": ["f648b042d810", "2bf960db8adf", "ad2a2a1df35b", "c2212473f0c1"], + "sender": ["e3973bb12da1", "6a5580a606dc", "6d220bb7b42a", "587de4c7f7fc"], + "payloads": ["f648b042d810", "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1"], "settlements": { "mount": "eb79a9b3682a" }, @@ -831,14 +829,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" @@ -855,14 +853,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "54cc20e9cfbc", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -879,14 +877,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8cf0ca1cc85d", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -903,14 +901,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "bb66129d9391", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -927,14 +925,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "ecfe4b0cfe19", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -951,14 +949,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "7440046c1348", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -975,14 +973,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "ae3822726c00", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -999,14 +997,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "4fadbf6b1661", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -1023,14 +1021,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "b7a249bf3627", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -1047,14 +1045,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "d65d39bc05d3", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" @@ -1071,14 +1069,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "7689d7b4e8e2", "9536c968b88a", "f1bc948c977d" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "3ca74b9dc048" diff --git a/mobile/rpc-foundation/goldens/sc-changes-loaded.json b/mobile/rpc-foundation/goldens/sc-changes-loaded.json index 0365526da70..0e95a43e419 100644 --- a/mobile/rpc-foundation/goldens/sc-changes-loaded.json +++ b/mobile/rpc-foundation/goldens/sc-changes-loaded.json @@ -56,11 +56,6 @@ } } }, - "2bf960db8adf": { - "name": "git.status#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\",\"includeLineStats\":false}}" - }, "35cc9e325d08": { "name": "git.branchCompare#1", "ordinal": 11, @@ -138,6 +133,62 @@ "startedAt": 0 } }, + "6a5580a606dc": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "branch": "feature", + "entries": [ + { + "added": 4, + "area": "unstaged", + "path": "src/app.ts", + "removed": 1, + "status": "modified" + }, + { + "area": "staged", + "oldPath": "src/older.ts", + "path": "src/old.ts", + "status": "renamed" + } + ], + "head": "head-oid", + "upstreamStatus": { + "ahead": 1, + "behind": 0, + "hasUpstream": true, + "upstreamName": "origin/feature" + } + } + } + } + }, "6d220bb7b42a": { "name": "worktree.show#1", "ordinal": 5, @@ -164,6 +215,32 @@ "startedAt": 0 } }, + "72e251ae531b": { + "name": "git.status#1", + "ordinal": 2, + "args": [ + { + "name": "method", + "value": "git.status" + }, + { + "name": "params", + "value": { + "worktree": "id:repo42::/p" + } + }, + { + "name": "options", + "value": { + "$rpc": "absent" + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, "8c8e528ca222": { "name": "worktree.show#1", "ordinal": 5, @@ -267,6 +344,11 @@ } } }, + "973ac78137f2": { + "name": "git.status#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"git.status\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" + }, "a0e527bdf205": { "branchCompareState": { "kind": "idle" @@ -280,95 +362,11 @@ "ordinal": 8, "value": {} }, - "aa8c5813ba8a": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "branch": "feature", - "entries": [ - { - "added": 4, - "area": "unstaged", - "path": "src/app.ts", - "removed": 1, - "status": "modified" - }, - { - "area": "staged", - "oldPath": "src/older.ts", - "path": "src/old.ts", - "status": "renamed" - } - ], - "head": "head-oid", - "upstreamStatus": { - "ahead": 1, - "behind": 0, - "hasUpstream": true, - "upstreamName": "origin/feature" - } - } - } - } - }, "ad2a2a1df35b": { "name": "worktree.show#1", "ordinal": 9, "json": "{\"id\":\"frame-3\",\"deviceToken\":\"recording-device\",\"method\":\"worktree.show\",\"params\":{\"worktree\":\"id:repo42::/p\"}}" }, - "ae602a492660": { - "name": "git.status#1", - "ordinal": 2, - "args": [ - { - "name": "method", - "value": "git.status" - }, - { - "name": "params", - "value": { - "includeLineStats": false, - "worktree": "id:repo42::/p" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, "c2212473f0c1": { "name": "repo.list#1", "ordinal": 10, @@ -434,8 +432,8 @@ { "id": "status-pending", "observation": { - "sender": ["e3973bb12da1", "ae602a492660"], - "payloads": ["f648b042d810", "2bf960db8adf"], + "sender": ["e3973bb12da1", "72e251ae531b"], + "payloads": ["f648b042d810", "973ac78137f2"], "settlements": { "mount": "eb79a9b3682a" }, @@ -446,8 +444,8 @@ { "id": "compare-pending", "observation": { - "sender": ["e3973bb12da1", "aa8c5813ba8a", "6d220bb7b42a", "587de4c7f7fc"], - "payloads": ["f648b042d810", "2bf960db8adf", "ad2a2a1df35b", "c2212473f0c1"], + "sender": ["e3973bb12da1", "6a5580a606dc", "6d220bb7b42a", "587de4c7f7fc"], + "payloads": ["f648b042d810", "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1"], "settlements": { "mount": "eb79a9b3682a" }, @@ -460,14 +458,14 @@ "observation": { "sender": [ "e3973bb12da1", - "aa8c5813ba8a", + "6a5580a606dc", "8c8e528ca222", "9536c968b88a", "35cc9e325d08" ], "payloads": [ "f648b042d810", - "2bf960db8adf", + "973ac78137f2", "ad2a2a1df35b", "c2212473f0c1", "e2d7409ece1c" diff --git a/mobile/rpc-foundation/pilot-scenarios.json b/mobile/rpc-foundation/pilot-scenarios.json index 2376579cee6..b429d9d6347 100644 --- a/mobile/rpc-foundation/pilot-scenarios.json +++ b/mobile/rpc-foundation/pilot-scenarios.json @@ -20348,8 +20348,7 @@ { "complete": "git.status#1", "params": { - "worktree": "id:repo42::/p", - "includeLineStats": false + "worktree": "id:repo42::/p" }, "reply": { "ok": true, diff --git a/mobile/src/components/nav-bar/PullDownMenu.tsx b/mobile/src/components/nav-bar/PullDownMenu.tsx index 7e8f95d77d2..029dc1aadc6 100644 --- a/mobile/src/components/nav-bar/PullDownMenu.tsx +++ b/mobile/src/components/nav-bar/PullDownMenu.tsx @@ -5,6 +5,7 @@ import { Modal, Platform, Pressable, + ScrollView, StyleSheet, Text, View, @@ -13,7 +14,7 @@ import { import { Check } from 'lucide-react-native' import { useReducedMotionEnabled } from '../../platform/use-reduced-motion' import { useBackClaim } from '../../navigation/use-back-claim' -import { colors, radii, spacing } from '../../theme/mobile-theme' +import { colors, radii, spacing, typography } from '../../theme/mobile-theme' import { themedStyleSheet } from '../../theme/themed-styles' import { useMobileColorScheme } from '../../theme/use-mobile-color-scheme' import { useMobileTranslation } from '../../i18n/use-mobile-translation' @@ -153,6 +154,10 @@ export function PullDownMenu({ visible, anchorRef, groups, onClose }: PullDownMe style={[ styles.menu, anchor, + // Long menus scroll instead of running off a short (landscape) screen. + { + maxHeight: window.height - ('top' in anchor ? anchor.top : anchor.bottom) - EDGE_INSET + }, { opacity: appear.interpolate({ inputRange: [0, 1], @@ -173,44 +178,54 @@ export function PullDownMenu({ visible, anchorRef, groups, onClose }: PullDownMe ]} accessibilityRole="menu" > - {shown.map((group, groupIndex) => ( - 0 && styles.groupGap}> - {group.map((action, index) => { - const Icon = action.icon - const tint = action.destructive ? colors.statusRed : colors.textPrimary - return ( - [ - styles.item, - index > 0 && styles.itemDivider, - pressed && styles.itemPressed, - action.disabled && styles.itemDisabled - ]} - disabled={action.disabled || action.loading} - onPress={() => press(action)} - accessibilityRole="menuitem" - accessibilityLabel={action.label} - accessibilityHint={action.hint} - accessibilityState={ - action.checked === undefined ? undefined : { checked: action.checked } - } - > - - {action.label} - - {action.checked ? ( - - ) : action.renderIcon ? ( - action.renderIcon() - ) : ( - Icon && - )} - - ) - })} - - ))} + + {shown.map((group, groupIndex) => ( + 0 && styles.groupGap}> + {group.map((action, index) => { + const Icon = action.icon + const tint = action.destructive ? colors.statusRed : colors.textPrimary + return ( + [ + styles.item, + index > 0 && styles.itemDivider, + pressed && styles.itemPressed, + action.disabled && styles.itemDisabled + ]} + disabled={action.disabled || action.loading} + onPress={() => press(action)} + accessibilityRole="menuitem" + accessibilityLabel={action.label} + accessibilityHint={action.hint} + accessibilityState={ + action.checked === undefined ? undefined : { checked: action.checked } + } + > + + + {action.label} + + {/* No tooltips on a phone: a disabled item says why, a destructive one what it replaces. */} + {(action.disabled || action.destructive) && action.hint ? ( + + {action.hint} + + ) : null} + + {action.checked ? ( + + ) : action.renderIcon ? ( + action.renderIcon() + ) : ( + Icon && + )} + + ) + })} + + ))} + ) : null} @@ -254,8 +269,16 @@ const styles = themedStyleSheet(() => ({ itemDisabled: { opacity: 0.4 }, + labelBlock: { + flexShrink: 1, + gap: 2 + }, label: { flexShrink: 1, fontSize: 15.5 + }, + hint: { + color: colors.textSecondary, + fontSize: typography.metaSize } })) diff --git a/mobile/src/i18n/en-runtime-required.json b/mobile/src/i18n/en-runtime-required.json index 7957760ec78..82edfb8b3c1 100644 --- a/mobile/src/i18n/en-runtime-required.json +++ b/mobile/src/i18n/en-runtime-required.json @@ -307,11 +307,11 @@ "pushCommits_one": "Push {{count}} commit.", "pushCommits_other": "Push {{count}} commits." }, - "branchCompare": { + "changesHeader": { "commits_one": "{{count}} commit", "commits_other": "{{count}} commits", - "files_one": "{{count}} file", - "files_other": "{{count}} files" + "filesNeedDecision_one": "{{count}} file needs a decision before you can finish.", + "filesNeedDecision_other": "{{count}} files need a decision before you can finish." }, "commitDetail": { "files_one": "{{count}} file", @@ -355,10 +355,6 @@ "checksRunning_one": "{{count}} check still running", "checksRunning_other": "{{count}} checks still running" }, - "prChip": { - "openWithComments_one": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request.", - "openWithComments_other": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request." - }, "primaryAction": { "commitFiles_one": "Commit {{count}} file", "commitFiles_other": "Commit {{count}} files", diff --git a/mobile/src/i18n/locales/de.json b/mobile/src/i18n/locales/de.json index 5886d85bd67..c4438e3b46d 100644 --- a/mobile/src/i18n/locales/de.json +++ b/mobile/src/i18n/locales/de.json @@ -2670,7 +2670,6 @@ "checkingReviewStatus": "Review-Status wird geprüft.", "checkoutBeforePublish": "Vor dem Veröffentlichen von Commits einen Branch auschecken.", "commitStagedChanges": "Vorgemerkte Änderungen committen.", - "commits": "Commits", "committing": "Commit läuft.", "createPr": "PR erstellen", "enterCommitMessage": "Zum Committen eine Commit-Nachricht eingeben.", @@ -2698,23 +2697,11 @@ "resolveFailed": "Branch-Basis konnte nicht ermittelt werden." }, "branchCard": { - "branchCount": "{{count}} auf Branch", - "changedCount": "{{count}} geändert", - "cherryPickState": "Cherry-pick", - "mergeState": "Merge", - "rebaseState": "Rebase", - "stagedCount": "{{count}} vorgemerkt", "completingMerge": "Wird abgeschlossen…", "completeMerge": "Merge abschließen" }, "branchCompare": { - "commits_one": "{{count}} Commit", - "commits_other": "{{count}} Commits", - "files_one": "{{count}} Datei", - "files_other": "{{count}} Dateien", - "sectionTitle": "Auf Branch committet", - "summary": "{{files}} – gegenüber {{baseRef}}", - "summaryWithCommits": "{{files}} – {{commits}} – gegenüber {{baseRef}}" + "sectionTitle": "Auf Branch committet" }, "branchCompareOutcome": { "baseBranchUnavailable": "Basis-Branch für den Vergleich konnte nicht ermittelt werden.", @@ -2773,11 +2760,8 @@ "emptyMessage": "Der Worktree ist sauber.", "emptyTitle": "Keine lokalen Änderungen", "generateMessage": "Commit-Nachricht mit KI generieren", - "openActions": "Aktionen der Quellcodeverwaltung öffnen", "reconnecting": "Verbindung zum Desktop wird wiederhergestellt…", - "regenerateHint": "Nachricht leeren, um sie neu zu generieren.", - "stageAll": "Alle vormerken", - "unstageAll": "Alle Vormerkungen aufheben" + "regenerateHint": "Nachricht leeren, um sie neu zu generieren." }, "createPrAction": { "checkingStatus": "Review-Status wird geprüft…", @@ -2806,20 +2790,15 @@ "titleUnpublishedForcePushCount_one": "Push von {{count}} Branch-Commit mit Lease erzwingen und bei Bedarf einen Upstream festlegen.", "titleUnpublishedForcePushCount_other": "Push von {{count}} Branch-Commits mit Lease erzwingen und bei Bedarf einen Upstream festlegen." }, - "fileList": { - "committedSection": "Auf Branch committet" - }, "fileRows": { "addedByThem": "Von ihnen hinzugefügt", "addedByUs": "Von uns hinzugefügt", "bothAdded": "Beidseitig hinzugefügt", "bothDeleted": "Beidseitig gelöscht", "bothModified": "Beidseitig geändert", - "committedSection": "Auf Branch committet", "committedUnavailable": "Committete Änderungen nicht verfügbar.", "deletedByThem": "Von ihnen gelöscht", "deletedByUs": "Von uns gelöscht", - "discardFile": "Änderungen an {{path}} verwerfen", "loadingCommitted": "Committete Änderungen werden geladen…", "openChangedFile": "Geänderte Datei {{path}} öffnen", "openCommittedChange": "Committete Änderung an {{path}} öffnen", @@ -2852,9 +2831,7 @@ "waitingForDesktop": "Warten auf Desktop…" }, "gitStatus": { - "changes": "Änderungen", - "conflicts": "Konflikte", - "stagedChanges": "Vorgemerkte Änderungen" + "conflicts": "Konflikte" }, "header": { "backToSession": "Zurück zur Sitzung", @@ -2922,7 +2899,6 @@ "waitingForDesktop": "Warten auf den Desktop…" }, "modals": { - "actionsTitle": "Quellcodeverwaltung", "currentBranch": "aktuell", "discardButton": "Verwerfen", "discardConfirmMessage": "Änderungen an „{{path}}“ verwerfen? Dies kann nicht rückgängig gemacht werden.", @@ -2935,15 +2911,6 @@ "panel": { "unavailableTitle": "Quellcodeverwaltung nicht verfügbar" }, - "prChip": { - "createPr": "Pull Request erstellen", - "loading": "Pull Request wird geladen…", - "loadingLabel": "Pull Request wird geladen", - "openLabel": "Pull Request #{{number}}, {{state}}, {{checks}}. Pull Request öffnen.", - "openWithComments_one": "Pull Request #{{number}}, {{state}}, {{checks}}, {{count}} ungelöster Kommentar. Pull Request öffnen.", - "openWithComments_other": "Pull Request #{{number}}, {{state}}, {{checks}}, {{count}} ungelöste Kommentare. Pull Request öffnen.", - "unavailableLabel": "Pull Request nicht verfügbar: {{message}}" - }, "prCreate": { "authRequired": "Vor dem Erstellen eines {{reviewLabel}}s ist eine Authentifizierung erforderlich.", "baseNotOnRemote": "Den Basis-Branch pushen, bevor ein {{reviewLabel}} erstellt wird.", @@ -3031,7 +2998,6 @@ "noBranch": "Kein Branch" }, "state": { - "noUpstream": "Kein Upstream", "syncStatus": "{{ahead}} voraus, {{behind}} zurück" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/en.json b/mobile/src/i18n/locales/en.json index f6b6ac5c333..3ecd1df2797 100644 --- a/mobile/src/i18n/locales/en.json +++ b/mobile/src/i18n/locales/en.json @@ -3219,7 +3219,6 @@ "createPr": "Create PR", "pushAndCreatePr": "Push & Create PR", "switchBranch": "Switch branch", - "commits": "Commits", "committing": "Commit in progress.", "forcePushing": "Force push in progress.", "remoteOperationInProgress": "Remote operation in progress.", @@ -3241,7 +3240,8 @@ "pullCommits_other": "Pull {{count}} commits.", "pushCommits_one": "Push {{count}} commit.", "pushCommits_other": "Push {{count}} commits.", - "upToDate": "Nothing to commit. Branch is up to date." + "upToDate": "Nothing to commit. Branch is up to date.", + "openCommits": "Open commits" }, "dropdownCounts": { "pushCount_one": "Push {{count}} commit", @@ -3299,20 +3299,11 @@ "loading": "Loading commits…" }, "branchCard": { - "mergeState": "merge", - "rebaseState": "rebase", - "cherryPickState": "cherry-pick", - "changedCount": "{{count}} changed", - "stagedCount": "{{count}} staged", - "branchCount": "{{count}} on branch", "completingMerge": "Completing…", "completeMerge": "Complete merge" }, "content": { "reconnecting": "Reconnecting to desktop...", - "stageAll": "Stage All", - "unstageAll": "Unstage All", - "openActions": "Open source control actions", "emptyTitle": "No local changes", "emptyMessage": "Working tree is clean.", "commitMessageDisabled": "Commit message disabled. No staged files.", @@ -3321,9 +3312,6 @@ "cancelGeneration": "Cancel commit message generation", "generateMessage": "Generate commit message with AI" }, - "fileList": { - "committedSection": "Committed on Branch" - }, "fileRows": { "bothModified": "Both modified", "bothAdded": "Both added", @@ -3338,10 +3326,10 @@ "unstageFile": "Unstage {{path}}", "stageFile": "Stage {{path}}", "discardFile": "Discard {{path}}", - "committedSection": "Committed on Branch", "loadingCommitted": "Loading committed changes...", "committedUnavailable": "Committed changes unavailable.", - "openCommittedChange": "Open committed change {{path}}" + "openCommittedChange": "Open committed change {{path}}", + "discard": "Discard" }, "header": { "close": "Close source control", @@ -3353,7 +3341,6 @@ "moreActions": "More source control actions" }, "modals": { - "actionsTitle": "Source Control", "discardTitle": "Discard Change", "discardConfirmMessage": "Discard changes to \"{{path}}\"? This cannot be undone.", "discardButton": "Discard", @@ -3367,15 +3354,6 @@ "unavailableTitle": "Source Control Unavailable", "loading": "Loading changes…" }, - "prChip": { - "loading": "Loading pull request…", - "createPr": "Create pull request", - "loadingLabel": "Loading pull request", - "unavailableLabel": "Pull request unavailable: {{message}}", - "openWithComments_one": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request.", - "openWithComments_other": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request.", - "openLabel": "Pull request #{{number}}, {{state}}, {{checks}}. Open pull request." - }, "hostedReviewCopy": { "updatingBranch": "Updating branch...", "stagingChanges": "Staging changes...", @@ -3394,13 +3372,7 @@ "loadFailed": "Unable to load committed changes" }, "branchCompare": { - "sectionTitle": "Committed on Branch", - "summary": "{{files}} - vs {{baseRef}}", - "summaryWithCommits": "{{files}} - {{commits}} - vs {{baseRef}}", - "files_one": "{{count}} file", - "files_other": "{{count}} files", - "commits_one": "{{count}} commit", - "commits_other": "{{count}} commits" + "sectionTitle": "Committed on Branch" }, "commitMessageAi": { "generateFailed": "Failed to generate commit message" @@ -3422,8 +3394,8 @@ }, "gitStatus": { "conflicts": "Conflicts", - "changes": "Changes", - "stagedChanges": "Staged Changes" + "uncommitted": "Uncommitted", + "staged": "Staged" }, "hostedReviewCreateIntent": { "resolveConflicts": "Resolve conflicts before creating a pull request.", @@ -3498,8 +3470,7 @@ "actionFailed": "Source control action failed" }, "state": { - "syncStatus": "{{ahead}} ahead, {{behind}} behind", - "noUpstream": "No upstream" + "syncStatus": "{{ahead}} ahead, {{behind}} behind" }, "textGenerationFailure": { "openCodeInvalidEvents": "OpenCode returned invalid JSON events.", @@ -3641,6 +3612,33 @@ }, "prCreateDraft": { "generateFailed": "Failed to write a description" + }, + "changesHeader": { + "merging": "Merging", + "rebasing": "Rebasing", + "cherryPicking": "Cherry-picking", + "notPublished": "Not published", + "notPushed": "{{count}} not pushed", + "behind": "{{count}} behind", + "upToDate": "Up to date", + "switchBranch": "Switch branch from {{branch}}", + "mergeInProgress": "Merge in progress", + "rebaseInProgress": "Rebase in progress", + "cherryPickInProgress": "Cherry-pick in progress", + "filesNeedDecision_one": "{{count}} file needs a decision before you can finish.", + "filesNeedDecision_other": "{{count}} files need a decision before you can finish.", + "conflictsResolved": "Every conflict is resolved. Finish from the bar below.", + "continueOnComputer": "Every conflict is resolved. Continue on your computer to finish.", + "commits_one": "{{count}} commit", + "commits_other": "{{count}} commits", + "conflictsMayRemain": "Not every file is listed, so conflicts may remain. Check on your computer." + }, + "fileList": { + "stageAll": "Stage all", + "mergedCleanly": "Merged cleanly", + "unstageAll": "Unstage all", + "onThisBranch": "On this branch {{count}}", + "versusBase": "vs {{base}}" } }, "newWorkspace": { diff --git a/mobile/src/i18n/locales/es.json b/mobile/src/i18n/locales/es.json index a1741bf8b36..e06f495a16b 100644 --- a/mobile/src/i18n/locales/es.json +++ b/mobile/src/i18n/locales/es.json @@ -2742,7 +2742,6 @@ "checkingReviewStatus": "Comprobando estado de revisión.", "checkoutBeforePublish": "Haz checkout de una rama antes de publicar commits.", "commitStagedChanges": "Haz commit de los cambios preparados.", - "commits": "Commits", "committing": "Commit en curso.", "createPr": "Crear PR", "enterCommitMessage": "Introduce un mensaje de commit para hacer commit.", @@ -2772,25 +2771,11 @@ "resolveFailed": "No se pudo determinar la base de la rama" }, "branchCard": { - "branchCount": "{{count}} en la rama", - "changedCount": "{{count}} con cambios", - "cherryPickState": "cherry-pick", - "mergeState": "merge", - "rebaseState": "rebase", - "stagedCount": "{{count}} en stage", "completingMerge": "Finalizando…", "completeMerge": "Completar fusión" }, "branchCompare": { - "commits_many": "{{count}} commits", - "commits_one": "{{count}} commit", - "commits_other": "{{count}} commits", - "files_many": "{{count}} archivos", - "files_one": "{{count}} archivo", - "files_other": "{{count}} archivos", - "sectionTitle": "Commits en la rama", - "summary": "{{files}} - frente a {{baseRef}}", - "summaryWithCommits": "{{files}} - {{commits}} - frente a {{baseRef}}" + "sectionTitle": "Commits en la rama" }, "branchCompareOutcome": { "baseBranchUnavailable": "No se pudo determinar la rama base para la comparación.", @@ -2849,11 +2834,8 @@ "emptyMessage": "El árbol de trabajo está limpio.", "emptyTitle": "No hay cambios locales", "generateMessage": "Generar mensaje de commit con IA", - "openActions": "Abrir acciones de control de código fuente", "reconnecting": "Reconectando con el escritorio…", - "regenerateHint": "Borre el mensaje para regenerar.", - "stageAll": "Hacer stage de todo", - "unstageAll": "Quitar todo del stage" + "regenerateHint": "Borre el mensaje para regenerar." }, "createPrAction": { "checkingStatus": "Comprobando estado de revisión…", @@ -2889,20 +2871,15 @@ "titleUnpublishedForcePushCount_one": "Haz force push con lease de {{count}} commit de la rama y configura un upstream si es necesario.", "titleUnpublishedForcePushCount_other": "Haz force push con lease de {{count}} commits de la rama y configura un upstream si es necesario." }, - "fileList": { - "committedSection": "Commits en la rama" - }, "fileRows": { "addedByThem": "Agregado por ellos", "addedByUs": "Agregado por nosotros", "bothAdded": "Agregado por ambos", "bothDeleted": "Eliminado por ambos", "bothModified": "Modificado por ambos", - "committedSection": "Commits en la rama", "committedUnavailable": "Los cambios confirmados no están disponibles.", "deletedByThem": "Eliminado por ellos", "deletedByUs": "Eliminado por nosotros", - "discardFile": "Descartar {{path}}", "loadingCommitted": "Cargando cambios confirmados…", "openChangedFile": "Abrir archivo modificado {{path}}", "openCommittedChange": "Abrir cambio confirmado en {{path}}", @@ -2940,9 +2917,7 @@ "waitingForDesktop": "Esperando al escritorio…" }, "gitStatus": { - "changes": "Cambios", - "conflicts": "Conflictos", - "stagedChanges": "Cambios preparados" + "conflicts": "Conflictos" }, "header": { "backToSession": "Volver a la sesión", @@ -3010,7 +2985,6 @@ "waitingForDesktop": "Esperando al escritorio…" }, "modals": { - "actionsTitle": "Control de código fuente", "currentBranch": "actual", "discardButton": "Descartar", "discardConfirmMessage": "¿Descartar los cambios de \"{{path}}\"? Esta acción no se puede deshacer.", @@ -3023,16 +2997,6 @@ "panel": { "unavailableTitle": "Control de código fuente no disponible" }, - "prChip": { - "createPr": "Crear pull request", - "loading": "Cargando pull request…", - "loadingLabel": "Cargando pull request", - "openLabel": "Pull request #{{number}}, {{state}}, {{checks}}. Abrir pull request.", - "openWithComments_many": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentarios sin resolver. Abrir pull request.", - "openWithComments_one": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentario sin resolver. Abrir pull request.", - "openWithComments_other": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentarios sin resolver. Abrir pull request.", - "unavailableLabel": "Pull request no disponible: {{message}}" - }, "prCreate": { "authRequired": "Autentícate antes de crear una {{reviewLabel}}.", "baseNotOnRemote": "Haz push de la rama base antes de crear una {{reviewLabel}}.", @@ -3120,7 +3084,6 @@ "noBranch": "Sin rama" }, "state": { - "noUpstream": "Sin upstream", "syncStatus": "{{ahead}} por delante, {{behind}} por detrás" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/fr.json b/mobile/src/i18n/locales/fr.json index 8a2fea5270f..40cb6184fef 100644 --- a/mobile/src/i18n/locales/fr.json +++ b/mobile/src/i18n/locales/fr.json @@ -2742,7 +2742,6 @@ "checkingReviewStatus": "Vérification du statut de revue.", "checkoutBeforePublish": "Effectuez un checkout d'une branche avant de publier des commits.", "commitStagedChanges": "Committer les modifications indexées.", - "commits": "Commits", "committing": "Commit en cours.", "createPr": "Créer une PR", "enterCommitMessage": "Saisissez un message de commit pour committer.", @@ -2772,25 +2771,11 @@ "resolveFailed": "Impossible de déterminer la base de la branche" }, "branchCard": { - "branchCount": "{{count}} sur la branche", - "changedCount": "Modifiés : {{count}}", - "cherryPickState": "cherry-pick", - "mergeState": "merge", - "rebaseState": "rebase", - "stagedCount": "Indexés : {{count}}", "completingMerge": "Finalisation…", "completeMerge": "Terminer la fusion" }, "branchCompare": { - "commits_many": "{{count}} commits", - "commits_one": "{{count}} commit", - "commits_other": "{{count}} commits", - "files_many": "{{count}} fichiers", - "files_one": "{{count}} fichier", - "files_other": "{{count}} fichiers", - "sectionTitle": "Committé sur la branche", - "summary": "{{files}} - par rapport à {{baseRef}}", - "summaryWithCommits": "{{files}} - {{commits}} - par rapport à {{baseRef}}" + "sectionTitle": "Committé sur la branche" }, "branchCompareOutcome": { "baseBranchUnavailable": "Impossible de déterminer la branche de base pour la comparaison.", @@ -2849,11 +2834,8 @@ "emptyMessage": "Le worktree est propre.", "emptyTitle": "Aucune modification locale", "generateMessage": "Générer le message de commit avec l'IA", - "openActions": "Ouvrir les actions de contrôle de code source", "reconnecting": "Reconnexion à l'ordinateur…", - "regenerateHint": "Effacez le message pour le régénérer.", - "stageAll": "Tout indexer", - "unstageAll": "Tout désindexer" + "regenerateHint": "Effacez le message pour le régénérer." }, "createPrAction": { "checkingStatus": "Vérification du statut de revue…", @@ -2889,20 +2871,15 @@ "titleUnpublishedForcePushCount_one": "Faites un force push avec lease de {{count}} commit de la branche et définissez un upstream si nécessaire.", "titleUnpublishedForcePushCount_other": "Faites un force push avec lease de {{count}} commits de la branche et définissez un upstream si nécessaire." }, - "fileList": { - "committedSection": "Committé sur la branche" - }, "fileRows": { "addedByThem": "Ajouté chez eux", "addedByUs": "Ajouté chez nous", "bothAdded": "Ajouté des deux côtés", "bothDeleted": "Supprimé des deux côtés", "bothModified": "Modifié des deux côtés", - "committedSection": "Committé sur la branche", "committedUnavailable": "Modifications committées indisponibles.", "deletedByThem": "Supprimé chez eux", "deletedByUs": "Supprimé chez nous", - "discardFile": "Abandonner les modifications de {{path}}", "loadingCommitted": "Chargement des modifications committées…", "openChangedFile": "Ouvrir le fichier modifié {{path}}", "openCommittedChange": "Ouvrir la modification committée de {{path}}", @@ -2940,9 +2917,7 @@ "waitingForDesktop": "En attente de l'ordinateur…" }, "gitStatus": { - "changes": "Modifications", - "conflicts": "Conflits", - "stagedChanges": "Modifications stagées" + "conflicts": "Conflits" }, "header": { "backToSession": "Retour à la session", @@ -3010,7 +2985,6 @@ "waitingForDesktop": "En attente de l'ordinateur…" }, "modals": { - "actionsTitle": "Contrôle de code source", "currentBranch": "actuelle", "discardButton": "Abandonner", "discardConfirmMessage": "Abandonner les modifications de « {{path}} » ? Cette action est irréversible.", @@ -3023,16 +2997,6 @@ "panel": { "unavailableTitle": "Contrôle de code source indisponible" }, - "prChip": { - "createPr": "Créer la pull request", - "loading": "Chargement de la pull request…", - "loadingLabel": "Chargement de la pull request", - "openLabel": "Pull request #{{number}}, {{state}}, {{checks}}. Ouvrir la pull request.", - "openWithComments_many": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} commentaires non résolus. Ouvrir la pull request.", - "openWithComments_one": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} commentaire non résolu. Ouvrir la pull request.", - "openWithComments_other": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} commentaires non résolus. Ouvrir la pull request.", - "unavailableLabel": "Pull request indisponible : {{message}}" - }, "prCreate": { "authRequired": "Authentifiez-vous avant de créer une {{reviewLabel}}.", "baseNotOnRemote": "Poussez la branche de base avant de créer une {{reviewLabel}}.", @@ -3120,7 +3084,6 @@ "noBranch": "Aucune branche" }, "state": { - "noUpstream": "Aucune branche de suivi", "syncStatus": "{{ahead}} en avance, {{behind}} en retard" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/ja.json b/mobile/src/i18n/locales/ja.json index 65262ea77e0..32b9b5941a2 100644 --- a/mobile/src/i18n/locales/ja.json +++ b/mobile/src/i18n/locales/ja.json @@ -2666,7 +2666,6 @@ "checkingReviewStatus": "レビュー状態を確認中です。", "checkoutBeforePublish": "コミットを公開する前にブランチをチェックアウトしてください。", "commitStagedChanges": "ステージ済みの変更をコミットします。", - "commits": "コミット", "committing": "コミット中です。", "createPr": "PR を作成", "enterCommitMessage": "コミットするには、コミットメッセージを入力してください。", @@ -2694,23 +2693,11 @@ "resolveFailed": "ベースブランチを特定できませんでした。" }, "branchCard": { - "branchCount": "ブランチ内 {{count}} 件", - "changedCount": "変更 {{count}} 件", - "cherryPickState": "チェリーピック", - "mergeState": "マージ", - "rebaseState": "リベース", - "stagedCount": "ステージ済み {{count}} 件", "completingMerge": "完了しています…", "completeMerge": "マージを完了" }, "branchCompare": { - "commits_one": "{{count}} 件のコミット", - "commits_other": "{{count}} 件のコミット", - "files_one": "{{count}} 個のファイル", - "files_other": "{{count}} 個のファイル", - "sectionTitle": "ブランチのコミット済み変更", - "summary": "{{files}} - {{baseRef}} との比較", - "summaryWithCommits": "{{files}} - {{commits}} - {{baseRef}} との比較" + "sectionTitle": "ブランチのコミット済み変更" }, "branchCompareOutcome": { "baseBranchUnavailable": "比較用のベースブランチを特定できませんでした。", @@ -2769,11 +2756,8 @@ "emptyMessage": "ワークツリーに変更はありません。", "emptyTitle": "ローカルの変更なし", "generateMessage": "AIでコミットメッセージを生成", - "openActions": "ソース管理の操作を開く", "reconnecting": "デスクトップに再接続中…", - "regenerateHint": "再生成するには、メッセージをクリアしてください。", - "stageAll": "ステージオール", - "unstageAll": "すべてステージ解除" + "regenerateHint": "再生成するには、メッセージをクリアしてください。" }, "createPrAction": { "checkingStatus": "レビュー状態を確認中…", @@ -2802,20 +2786,15 @@ "titleUnpublishedForcePushCount_one": "{{count}} 件のブランチコミットを lease 付きで強制プッシュし、必要に応じてアップストリームを設定します。", "titleUnpublishedForcePushCount_other": "{{count}} 件のブランチコミットを lease 付きで強制プッシュし、必要に応じてアップストリームを設定します。" }, - "fileList": { - "committedSection": "ブランチのコミット済み変更" - }, "fileRows": { "addedByThem": "相手側で追加", "addedByUs": "こちら側で追加", "bothAdded": "双方で追加", "bothDeleted": "双方で削除", "bothModified": "双方で変更", - "committedSection": "ブランチのコミット済み変更", "committedUnavailable": "コミット済みの変更を表示できません。", "deletedByThem": "相手側で削除", "deletedByUs": "こちら側で削除", - "discardFile": "{{path}} の変更を破棄", "loadingCommitted": "コミット済みの変更を読み込み中…", "openChangedFile": "変更されたファイル {{path}} を開く", "openCommittedChange": "{{path}} のコミット済みの変更を開く", @@ -2848,9 +2827,7 @@ "waitingForDesktop": "デスクトップからの応答を待機中…" }, "gitStatus": { - "changes": "変更点", - "conflicts": "競合", - "stagedChanges": "ステージ済みの変更" + "conflicts": "競合" }, "header": { "backToSession": "セッションに戻る", @@ -2918,7 +2895,6 @@ "waitingForDesktop": "デスクトップへの接続を待機中…" }, "modals": { - "actionsTitle": "ソース管理", "currentBranch": "現在", "discardButton": "破棄", "discardConfirmMessage": "「{{path}}」への変更を破棄しますか?この操作は取り消せません。", @@ -2931,15 +2907,6 @@ "panel": { "unavailableTitle": "ソース管理を利用できません" }, - "prChip": { - "createPr": "PR を作成", - "loading": "PR を読み込み中…", - "loadingLabel": "PR を読み込み中", - "openLabel": "PR #{{number}}、{{state}}、{{checks}}。PR を開く。", - "openWithComments_one": "PR #{{number}}、{{state}}、{{checks}}、未解決のコメント {{count}} 件。PR を開く。", - "openWithComments_other": "PR #{{number}}、{{state}}、{{checks}}、未解決のコメント {{count}} 件。PR を開く。", - "unavailableLabel": "PR を表示できません:{{message}}" - }, "prCreate": { "authRequired": "{{reviewLabel}} を作成する前に認証してください。", "baseNotOnRemote": "{{reviewLabel}} を作成する前にベースブランチをプッシュしてください。", @@ -3027,7 +2994,6 @@ "noBranch": "ブランチなし" }, "state": { - "noUpstream": "アップストリームなし", "syncStatus": "{{ahead}} コミット先行、{{behind}} コミット遅れ" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/ko.json b/mobile/src/i18n/locales/ko.json index 428d61d66d3..bcca2fca555 100644 --- a/mobile/src/i18n/locales/ko.json +++ b/mobile/src/i18n/locales/ko.json @@ -2666,7 +2666,6 @@ "checkingReviewStatus": "리뷰 상태를 확인 중입니다.", "checkoutBeforePublish": "커밋을 게시하기 전에 브랜치를 체크아웃하세요.", "commitStagedChanges": "스테이징된 변경 사항을 커밋하세요.", - "commits": "커밋", "committing": "커밋이 진행 중입니다.", "createPr": "PR 작성", "enterCommitMessage": "커밋하려면 커밋 메시지를 입력하세요.", @@ -2694,23 +2693,11 @@ "resolveFailed": "브랜치의 기준점을 확인할 수 없습니다." }, "branchCard": { - "branchCount": "브랜치 변경 {{count}}개", - "changedCount": "변경 {{count}}개", - "cherryPickState": "체리픽", - "mergeState": "병합", - "rebaseState": "리베이스", - "stagedCount": "{{count}}개 스테이징됨", "completingMerge": "완료 중…", "completeMerge": "병합 완료" }, "branchCompare": { - "commits_one": "커밋 {{count}}개", - "commits_other": "커밋 {{count}}개", - "files_one": "파일 {{count}}개", - "files_other": "파일 {{count}}개", - "sectionTitle": "브랜치에 커밋됨", - "summary": "{{files}} - {{baseRef}} 대비", - "summaryWithCommits": "{{files}} - {{commits}} - {{baseRef}} 대비" + "sectionTitle": "브랜치에 커밋됨" }, "branchCompareOutcome": { "baseBranchUnavailable": "비교할 기본 브랜치를 확인할 수 없습니다.", @@ -2769,11 +2756,8 @@ "emptyMessage": "워크트리에 변경 사항이 없습니다.", "emptyTitle": "로컬 변경 사항 없음", "generateMessage": "AI로 commit 메시지 생성", - "openActions": "소스 제어 작업 열기", "reconnecting": "데스크톱에 다시 연결 중…", - "regenerateHint": "다시 생성하려면 메시지를 지웁니다.", - "stageAll": "모두 스테이지", - "unstageAll": "모두 언스테이징" + "regenerateHint": "다시 생성하려면 메시지를 지웁니다." }, "createPrAction": { "checkingStatus": "리뷰 상태 확인 중…", @@ -2802,20 +2786,15 @@ "titleUnpublishedForcePushCount_one": "브랜치 커밋 {{count}}개를 lease로 강제 푸시하고 필요하면 업스트림을 설정합니다.", "titleUnpublishedForcePushCount_other": "브랜치 커밋 {{count}}개를 lease로 강제 푸시하고 필요하면 업스트림을 설정합니다." }, - "fileList": { - "committedSection": "브랜치에 커밋됨" - }, "fileRows": { "addedByThem": "상대 쪽에서 추가됨", "addedByUs": "우리 쪽에서 추가됨", "bothAdded": "양쪽에서 추가됨", "bothDeleted": "양쪽에서 삭제됨", "bothModified": "양쪽에서 수정됨", - "committedSection": "브랜치에 커밋됨", "committedUnavailable": "커밋된 변경 사항을 확인할 수 없습니다.", "deletedByThem": "상대 쪽에서 삭제됨", "deletedByUs": "우리 쪽에서 삭제됨", - "discardFile": "{{path}} 변경사항 취소", "loadingCommitted": "커밋된 변경 사항 로드 중…", "openChangedFile": "변경된 파일 {{path}} 열기", "openCommittedChange": "{{path}}의 커밋된 변경 사항 열기", @@ -2848,9 +2827,7 @@ "waitingForDesktop": "데스크톱 응답 대기 중…" }, "gitStatus": { - "changes": "변경 사항", - "conflicts": "충돌", - "stagedChanges": "스테이징된 변경 사항" + "conflicts": "충돌" }, "header": { "backToSession": "세션으로 돌아가기", @@ -2918,7 +2895,6 @@ "waitingForDesktop": "데스크톱 연결 대기 중…" }, "modals": { - "actionsTitle": "소스 제어", "currentBranch": "현재", "discardButton": "버리기", "discardConfirmMessage": "\"{{path}}\"의 변경 사항을 버리시겠습니까? 이 작업은 되돌릴 수 없습니다.", @@ -2931,15 +2907,6 @@ "panel": { "unavailableTitle": "소스 제어 사용 불가" }, - "prChip": { - "createPr": "PR 생성", - "loading": "PR 로드 중…", - "loadingLabel": "PR 로드 중", - "openLabel": "PR #{{number}}, {{state}}, {{checks}}. PR 열기.", - "openWithComments_one": "PR #{{number}}, {{state}}, {{checks}}, 해결되지 않은 댓글 {{count}}개. PR 열기.", - "openWithComments_other": "PR #{{number}}, {{state}}, {{checks}}, 해결되지 않은 댓글 {{count}}개. PR 열기.", - "unavailableLabel": "PR 확인 불가: {{message}}" - }, "prCreate": { "authRequired": "{{reviewLabel}} 생성 전에 인증하세요.", "baseNotOnRemote": "{{reviewLabel}}을(를) 생성하기 전에 기본 브랜치를 푸시하세요.", @@ -3027,7 +2994,6 @@ "noBranch": "브랜치 없음" }, "state": { - "noUpstream": "업스트림 없음", "syncStatus": "{{ahead}}개 앞섬, {{behind}}개 뒤처짐" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/pt.json b/mobile/src/i18n/locales/pt.json index 5343c605d4c..93ed011a2b8 100644 --- a/mobile/src/i18n/locales/pt.json +++ b/mobile/src/i18n/locales/pt.json @@ -2742,7 +2742,6 @@ "checkingReviewStatus": "Verificando status da revisão.", "checkoutBeforePublish": "Faça checkout de uma branch antes de publicar commits.", "commitStagedChanges": "Faça commit das alterações staged.", - "commits": "Commits", "committing": "Commit em andamento.", "createPr": "Criar PR", "enterCommitMessage": "Digite uma mensagem de commit para fazer commit.", @@ -2772,25 +2771,11 @@ "resolveFailed": "Não foi possível determinar a branch base" }, "branchCard": { - "branchCount": "{{count}} na branch", - "changedCount": "Alterações: {{count}}", - "cherryPickState": "cherry-pick", - "mergeState": "merge", - "rebaseState": "rebase", - "stagedCount": "No stage: {{count}}", "completingMerge": "Concluindo…", "completeMerge": "Concluir mesclagem" }, "branchCompare": { - "commits_many": "{{count}} commits", - "commits_one": "{{count}} commit", - "commits_other": "{{count}} commits", - "files_many": "{{count}} arquivos", - "files_one": "{{count}} arquivo", - "files_other": "{{count}} arquivos", - "sectionTitle": "Commitado na Branch", - "summary": "{{files}} - em relação a {{baseRef}}", - "summaryWithCommits": "{{files}} - {{commits}} - em relação a {{baseRef}}" + "sectionTitle": "Commitado na Branch" }, "branchCompareOutcome": { "baseBranchUnavailable": "Não foi possível determinar a branch base para comparação.", @@ -2849,11 +2834,8 @@ "emptyMessage": "A árvore de trabalho está limpa.", "emptyTitle": "Nenhuma alteração local", "generateMessage": "Gerar mensagem de commit com IA", - "openActions": "Abrir ações de controle de versão", "reconnecting": "Reconectando ao desktop…", - "regenerateHint": "Limpe a mensagem para gerar novamente.", - "stageAll": "Colocar Tudo em Stage", - "unstageAll": "Remover Tudo do Stage" + "regenerateHint": "Limpe a mensagem para gerar novamente." }, "createPrAction": { "checkingStatus": "Verificando status de review…", @@ -2889,20 +2871,15 @@ "titleUnpublishedForcePushCount_one": "Faça force push com lease de {{count}} commit da branch e defina um upstream se necessário.", "titleUnpublishedForcePushCount_other": "Faça force push com lease de {{count}} commits da branch e defina um upstream se necessário." }, - "fileList": { - "committedSection": "Commitado na Branch" - }, "fileRows": { "addedByThem": "Adicionado por eles", "addedByUs": "Adicionado por nós", "bothAdded": "Adicionado por ambos", "bothDeleted": "Excluído por ambos", "bothModified": "Modificado por ambos", - "committedSection": "Commitado na Branch", "committedUnavailable": "Alterações commitadas indisponíveis.", "deletedByThem": "Excluído por eles", "deletedByUs": "Excluído por nós", - "discardFile": "Descartar {{path}}", "loadingCommitted": "Carregando alterações commitadas…", "openChangedFile": "Abrir arquivo alterado {{path}}", "openCommittedChange": "Abrir alteração commitada em {{path}}", @@ -2940,9 +2917,7 @@ "waitingForDesktop": "Aguardando o desktop…" }, "gitStatus": { - "changes": "Alterações", - "conflicts": "Conflitos", - "stagedChanges": "Alterações no Stage" + "conflicts": "Conflitos" }, "header": { "backToSession": "Voltar à sessão", @@ -3010,7 +2985,6 @@ "waitingForDesktop": "Aguardando o desktop…" }, "modals": { - "actionsTitle": "Controle de Código-Fonte", "currentBranch": "atual", "discardButton": "Descartar", "discardConfirmMessage": "Descartar alterações em \"{{path}}\"? Esta ação não pode ser desfeita.", @@ -3023,16 +2997,6 @@ "panel": { "unavailableTitle": "Controle de Código-Fonte Indisponível" }, - "prChip": { - "createPr": "Criar pull request", - "loading": "Carregando pull request…", - "loadingLabel": "Carregando pull request", - "openLabel": "Pull request #{{number}}, {{state}}, {{checks}}. Abrir pull request.", - "openWithComments_many": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentários não resolvidos. Abrir pull request.", - "openWithComments_one": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentário não resolvido. Abrir pull request.", - "openWithComments_other": "Pull request #{{number}}, {{state}}, {{checks}}, {{count}} comentários não resolvidos. Abrir pull request.", - "unavailableLabel": "Pull request indisponível: {{message}}" - }, "prCreate": { "authRequired": "Autentique-se antes de criar um {{reviewLabel}}.", "baseNotOnRemote": "Faça push da branch base antes de criar um {{reviewLabel}}.", @@ -3120,7 +3084,6 @@ "noBranch": "Sem branch" }, "state": { - "noUpstream": "Sem upstream", "syncStatus": "{{ahead}} à frente, {{behind}} atrás" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/ru.json b/mobile/src/i18n/locales/ru.json index eeda9c28825..0ca130d1b95 100644 --- a/mobile/src/i18n/locales/ru.json +++ b/mobile/src/i18n/locales/ru.json @@ -2814,7 +2814,6 @@ "checkingReviewStatus": "Получение статуса PR/MR.", "checkoutBeforePublish": "Переключитесь на ветку перед публикацией коммитов.", "commitStagedChanges": "Создать коммит с изменениями в индексе.", - "commits": "Коммиты", "committing": "Создаётся коммит.", "createPr": "Создать PR", "enterCommitMessage": "Введите сообщение, чтобы создать коммит.", @@ -2846,27 +2845,11 @@ "resolveFailed": "Не удалось определить базовую ветку" }, "branchCard": { - "branchCount": "В ветке: {{count}}", - "changedCount": "Изменено: {{count}}", - "cherryPickState": "cherry-pick", - "mergeState": "слияние", - "rebaseState": "перебазирование", - "stagedCount": "В индексе: {{count}}", "completingMerge": "Завершение…", "completeMerge": "Завершить слияние" }, "branchCompare": { - "commits_few": "{{count}} коммита", - "commits_many": "{{count}} коммитов", - "commits_one": "{{count}} коммит", - "commits_other": "{{count}} коммита", - "files_few": "{{count}} файла", - "files_many": "{{count}} файлов", - "files_one": "{{count}} файл", - "files_other": "{{count}} файла", - "sectionTitle": "Закоммичено в ветке", - "summary": "{{files}} — по сравнению с {{baseRef}}", - "summaryWithCommits": "{{files}} — {{commits}} — по сравнению с {{baseRef}}" + "sectionTitle": "Закоммичено в ветке" }, "branchCompareOutcome": { "baseBranchUnavailable": "Не удалось определить базовую ветку для сравнения.", @@ -2925,11 +2908,8 @@ "emptyMessage": "В рабочем дереве нет изменений.", "emptyTitle": "Нет локальных изменений", "generateMessage": "Создать сообщение коммита с помощью ИИ", - "openActions": "Открыть меню управления версиями", "reconnecting": "Переподключение к компьютеру…", - "regenerateHint": "Очистите сообщение для повторного создания.", - "stageAll": "Добавить всё в индекс", - "unstageAll": "Убрать всё из индекса" + "regenerateHint": "Очистите сообщение для повторного создания." }, "createPrAction": { "checkingStatus": "Получение статуса PR/MR…", @@ -2972,20 +2952,15 @@ "titleUnpublishedForcePushCount_one": "Принудительно отправьте {{count}} коммит этой ветки с проверкой состояния удалённой ветки и при необходимости настройте отслеживаемую удалённую ветку.", "titleUnpublishedForcePushCount_other": "Принудительно отправьте {{count}} коммита этой ветки с проверкой состояния удалённой ветки и при необходимости настройте отслеживаемую удалённую ветку." }, - "fileList": { - "committedSection": "Закоммичено в ветке" - }, "fileRows": { "addedByThem": "Добавлен ими", "addedByUs": "Добавлен нами", "bothAdded": "Добавлен с обеих сторон", "bothDeleted": "Удалён с обеих сторон", "bothModified": "Изменён с обеих сторон", - "committedSection": "Закоммичено в ветке", "committedUnavailable": "Закоммиченные изменения недоступны.", "deletedByThem": "Удалён ими", "deletedByUs": "Удалён нами", - "discardFile": "Отменить изменения в «{{path}}»", "loadingCommitted": "Загрузка закоммиченных изменений…", "openChangedFile": "Открыть изменённый файл {{path}}", "openCommittedChange": "Открыть закоммиченные изменения в «{{path}}»", @@ -3028,9 +3003,7 @@ "waitingForDesktop": "Ожидание ответа от компьютера…" }, "gitStatus": { - "changes": "Изменения", - "conflicts": "Конфликты", - "stagedChanges": "Изменения в индексе" + "conflicts": "Конфликты" }, "header": { "backToSession": "Вернуться к сессии", @@ -3098,7 +3071,6 @@ "waitingForDesktop": "Ожидание подключения к компьютеру…" }, "modals": { - "actionsTitle": "Управление версиями", "currentBranch": "текущая", "discardButton": "Отменить изменения", "discardConfirmMessage": "Отменить изменения в «{{path}}»? Это действие необратимо.", @@ -3111,17 +3083,6 @@ "panel": { "unavailableTitle": "Управление версиями недоступно" }, - "prChip": { - "createPr": "Создать пул-реквест", - "loading": "Загрузка пул-реквеста…", - "loadingLabel": "Загрузка пул-реквеста", - "openLabel": "Пул-реквест #{{number}}, {{state}}, {{checks}}. Открыть пул-реквест.", - "openWithComments_few": "Пул-реквест #{{number}}, {{state}}, {{checks}}, {{count}} неразрешённых комментария. Открыть пул-реквест.", - "openWithComments_many": "Пул-реквест #{{number}}, {{state}}, {{checks}}, {{count}} неразрешённых комментариев. Открыть пул-реквест.", - "openWithComments_one": "Пул-реквест #{{number}}, {{state}}, {{checks}}, {{count}} неразрешённый комментарий. Открыть пул-реквест.", - "openWithComments_other": "Пул-реквест #{{number}}, {{state}}, {{checks}}, {{count}} неразрешённого комментария. Открыть пул-реквест.", - "unavailableLabel": "Пул-реквест недоступен: {{message}}" - }, "prCreate": { "authRequired": "Прежде чем создать {{reviewLabel}}, пройдите аутентификацию.", "baseNotOnRemote": "Прежде чем создать {{reviewLabel}}, отправьте базовую ветку.", @@ -3209,7 +3170,6 @@ "noBranch": "Нет ветки" }, "state": { - "noUpstream": "Нет отслеживаемой ветки", "syncStatus": "Опережает на {{ahead}}, отстаёт на {{behind}}" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/tr.json b/mobile/src/i18n/locales/tr.json index b7acbef6c26..4706bed8d0f 100644 --- a/mobile/src/i18n/locales/tr.json +++ b/mobile/src/i18n/locales/tr.json @@ -2670,7 +2670,6 @@ "checkingReviewStatus": "İnceleme durumu kontrol ediliyor.", "checkoutBeforePublish": "Commit'leri yayınlamadan önce bir branch'i checkout edin.", "commitStagedChanges": "Stage edilmiş değişiklikleri commit'leyin.", - "commits": "Commit'ler", "committing": "Commit işlemi sürüyor.", "createPr": "PR Oluştur", "enterCommitMessage": "Commit'lemek için bir commit mesajı girin.", @@ -2698,23 +2697,11 @@ "resolveFailed": "Temel branch belirlenemedi" }, "branchCard": { - "branchCount": "branch'te {{count}}", - "changedCount": "{{count}} değişiklik", - "cherryPickState": "cherry-pick", - "mergeState": "merge", - "rebaseState": "rebase", - "stagedCount": "{{count}} stage edildi", "completingMerge": "Tamamlanıyor…", "completeMerge": "Birleştirmeyi tamamla" }, "branchCompare": { - "commits_one": "{{count}} commit", - "commits_other": "{{count}} commit", - "files_one": "{{count}} dosya", - "files_other": "{{count}} dosya", - "sectionTitle": "Branch'e Commit Edildi", - "summary": "{{files}} - {{baseRef}} ile karşılaştırma", - "summaryWithCommits": "{{files}} - {{commits}} - {{baseRef}} ile karşılaştırma" + "sectionTitle": "Branch'e Commit Edildi" }, "branchCompareOutcome": { "baseBranchUnavailable": "Karşılaştırma için temel branch belirlenemedi.", @@ -2773,11 +2760,8 @@ "emptyMessage": "Çalışma ağacı temiz.", "emptyTitle": "Yerel değişiklik yok", "generateMessage": "Commit mesajını AI ile oluştur", - "openActions": "Kaynak kontrolü eylemlerini aç", "reconnecting": "Masaüstüne yeniden bağlanılıyor…", - "regenerateHint": "Yeniden oluşturmak için mesajı temizleyin.", - "stageAll": "Tümünü Stage Et", - "unstageAll": "Tümünü Stage'den Çıkar" + "regenerateHint": "Yeniden oluşturmak için mesajı temizleyin." }, "createPrAction": { "checkingStatus": "İnceleme durumu kontrol ediliyor…", @@ -2806,20 +2790,15 @@ "titleUnpublishedForcePushCount_one": "Branch'teki {{count}} commit'i lease korumasıyla force push edin ve gerekirse bir upstream ayarlayın.", "titleUnpublishedForcePushCount_other": "Branch'teki {{count}} commit'i lease korumasıyla force push edin ve gerekirse bir upstream ayarlayın." }, - "fileList": { - "committedSection": "Branch'e Commit'lendi" - }, "fileRows": { "addedByThem": "Karşı tarafça eklendi", "addedByUs": "Tarafımızdan eklendi", "bothAdded": "İki tarafta da eklendi", "bothDeleted": "İki tarafta da silindi", "bothModified": "İki tarafta da değiştirildi", - "committedSection": "Branch'e Commit'lendi", "committedUnavailable": "Commit'lenen değişiklikler kullanılamıyor.", "deletedByThem": "Karşı tarafça silindi", "deletedByUs": "Tarafımızdan silindi", - "discardFile": "{{path}} değişikliklerini at", "loadingCommitted": "Commit'lenen değişiklikler yükleniyor…", "openChangedFile": "Değiştirilen {{path}} dosyasını aç", "openCommittedChange": "{{path}} için commit'lenen değişikliği aç", @@ -2852,9 +2831,7 @@ "waitingForDesktop": "Masaüstü bekleniyor…" }, "gitStatus": { - "changes": "Değişiklikler", - "conflicts": "Conflict'ler", - "stagedChanges": "Stage Edilen Değişiklikler" + "conflicts": "Conflict'ler" }, "header": { "backToSession": "Oturuma dön", @@ -2922,7 +2899,6 @@ "waitingForDesktop": "Masaüstü bekleniyor…" }, "modals": { - "actionsTitle": "Kaynak Kontrolü", "currentBranch": "geçerli", "discardButton": "At", "discardConfirmMessage": "“{{path}}” dosyasındaki değişiklikler atılsın mı? Bu işlem geri alınamaz.", @@ -2935,15 +2911,6 @@ "panel": { "unavailableTitle": "Kaynak Kontrolü Kullanılamıyor" }, - "prChip": { - "createPr": "Pull request oluştur", - "loading": "Pull request yükleniyor…", - "loadingLabel": "Pull request yükleniyor", - "openLabel": "#{{number}} numaralı pull request, {{state}}, {{checks}}. Pull request'i aç.", - "openWithComments_one": "#{{number}} numaralı pull request, {{state}}, {{checks}}, {{count}} çözülmemiş yorum. Pull request'i aç.", - "openWithComments_other": "#{{number}} numaralı pull request, {{state}}, {{checks}}, {{count}} çözülmemiş yorum. Pull request'i aç.", - "unavailableLabel": "Pull request kullanılamıyor: {{message}}" - }, "prCreate": { "authRequired": "Bir {{reviewLabel}} oluşturmadan önce kimliğinizi doğrulayın.", "baseNotOnRemote": "Bir {{reviewLabel}} oluşturmadan önce temel branch'i push edin.", @@ -3031,7 +2998,6 @@ "noBranch": "Branch yok" }, "state": { - "noUpstream": "Upstream yok", "syncStatus": "{{ahead}} ileride, {{behind}} geride" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/zh-Hant.json b/mobile/src/i18n/locales/zh-Hant.json index 4f420e895c8..2377832a35d 100644 --- a/mobile/src/i18n/locales/zh-Hant.json +++ b/mobile/src/i18n/locales/zh-Hant.json @@ -2666,7 +2666,6 @@ "checkingReviewStatus": "正在檢查審查狀態。", "checkoutBeforePublish": "請先檢出分支再發布提交。", "commitStagedChanges": "提交已暫存的變更。", - "commits": "提交", "committing": "正在提交。", "createPr": "建立 PR", "enterCommitMessage": "請輸入提交訊息以進行提交。", @@ -2694,23 +2693,11 @@ "resolveFailed": "無法判定分支基準" }, "branchCard": { - "branchCount": "已提交 {{count}} 項", - "changedCount": "已變更 {{count}} 項", - "cherryPickState": "挑選", - "mergeState": "合併", - "rebaseState": "變基", - "stagedCount": "已暫存 {{count}} 項", "completingMerge": "正在完成…", "completeMerge": "完成合併" }, "branchCompare": { - "commits_one": "{{count}} 筆提交", - "commits_other": "{{count}} 筆提交", - "files_one": "{{count}} 個檔案", - "files_other": "{{count}} 個檔案", - "sectionTitle": "已在分支上提交", - "summary": "{{files}} - 與 {{baseRef}} 比較", - "summaryWithCommits": "{{files}} - {{commits}} - 與 {{baseRef}} 比較" + "sectionTitle": "已在分支上提交" }, "branchCompareOutcome": { "baseBranchUnavailable": "無法判定用於比較的基礎分支。", @@ -2769,11 +2756,8 @@ "emptyMessage": "工作樹沒有變更。", "emptyTitle": "沒有本機變更", "generateMessage": "使用 AI 產生提交訊息", - "openActions": "開啟原始碼控制操作", "reconnecting": "正在重新連線至電腦…", - "regenerateHint": "清除訊息以重新產生。", - "stageAll": "暫存全部", - "unstageAll": "全部取消暫存" + "regenerateHint": "清除訊息以重新產生。" }, "createPrAction": { "checkingStatus": "正在檢查審查狀態…", @@ -2802,20 +2786,15 @@ "titleUnpublishedForcePushCount_one": "使用租約強制推送此分支的 {{count}} 筆提交,並視需要設定上游分支。", "titleUnpublishedForcePushCount_other": "使用租約強制推送此分支的 {{count}} 筆提交,並視需要設定上游分支。" }, - "fileList": { - "committedSection": "已在分支上提交" - }, "fileRows": { "addedByThem": "對方已新增", "addedByUs": "我方已新增", "bothAdded": "雙方皆已新增", "bothDeleted": "雙方皆已刪除", "bothModified": "雙方皆已修改", - "committedSection": "已在分支上提交", "committedUnavailable": "無法取得已提交的變更。", "deletedByThem": "對方已刪除", "deletedByUs": "我方已刪除", - "discardFile": "放棄 {{path}} 的變更", "loadingCommitted": "正在載入已提交的變更…", "openChangedFile": "開啟已變更的檔案 {{path}}", "openCommittedChange": "開啟 {{path}} 的已提交變更", @@ -2848,9 +2827,7 @@ "waitingForDesktop": "正在等待電腦…" }, "gitStatus": { - "changes": "變更", - "conflicts": "衝突", - "stagedChanges": "已暫存的變更" + "conflicts": "衝突" }, "header": { "backToSession": "返回工作階段", @@ -2918,7 +2895,6 @@ "waitingForDesktop": "等待電腦…" }, "modals": { - "actionsTitle": "原始碼控制", "currentBranch": "目前分支", "discardButton": "放棄", "discardConfirmMessage": "要放棄「{{path}}」的變更嗎?此操作無法復原。", @@ -2931,15 +2907,6 @@ "panel": { "unavailableTitle": "無法使用原始碼控制" }, - "prChip": { - "createPr": "建立 PR", - "loading": "正在載入 PR…", - "loadingLabel": "正在載入 PR", - "openLabel": "PR #{{number}},{{state}},{{checks}}。開啟 PR。", - "openWithComments_one": "PR #{{number}},{{state}},{{checks}},有 {{count}} 則未解決的留言。開啟 PR。", - "openWithComments_other": "PR #{{number}},{{state}},{{checks}},有 {{count}} 則未解決的留言。開啟 PR。", - "unavailableLabel": "無法取得 PR:{{message}}" - }, "prCreate": { "authRequired": "請在建立 {{reviewLabel}} 之前完成身分驗證。", "baseNotOnRemote": "建立 {{reviewLabel}} 前,請先推送基準分支。", @@ -3027,7 +2994,6 @@ "noBranch": "無分支" }, "state": { - "noUpstream": "無上游分支", "syncStatus": "領先 {{ahead}} 個提交,落後 {{behind}} 個提交" }, "textGenerationFailure": { diff --git a/mobile/src/i18n/locales/zh.json b/mobile/src/i18n/locales/zh.json index 335d1c310fb..4c83f3760a4 100644 --- a/mobile/src/i18n/locales/zh.json +++ b/mobile/src/i18n/locales/zh.json @@ -2666,7 +2666,6 @@ "checkingReviewStatus": "正在检查评审状态。", "checkoutBeforePublish": "请先检出分支再发布提交。", "commitStagedChanges": "提交已暂存的更改。", - "commits": "提交", "committing": "正在提交。", "createPr": "创建PR", "enterCommitMessage": "请输入提交信息后再提交。", @@ -2694,23 +2693,11 @@ "resolveFailed": "无法解析基础分支" }, "branchCard": { - "branchCount": "已提交 {{count}} 个文件", - "changedCount": "已更改 {{count}} 个文件", - "cherryPickState": "cherry-pick", - "mergeState": "合并", - "rebaseState": "变基", - "stagedCount": "已暂存 {{count}} 个文件", "completingMerge": "正在完成…", "completeMerge": "完成合并" }, "branchCompare": { - "commits_one": "{{count}} 次提交", - "commits_other": "{{count}} 次提交", - "files_one": "{{count}} 个文件", - "files_other": "{{count}} 个文件", - "sectionTitle": "已提交的更改", - "summary": "{{files}} - 与 {{baseRef}} 比较", - "summaryWithCommits": "{{files}} - {{commits}} - 与 {{baseRef}} 比较" + "sectionTitle": "已提交的更改" }, "branchCompareOutcome": { "baseBranchUnavailable": "无法解析用于比较的基础分支。", @@ -2769,11 +2756,8 @@ "emptyMessage": "工作树无更改。", "emptyTitle": "无本地更改", "generateMessage": "使用 AI 生成提交信息", - "openActions": "打开源代码控制操作菜单", "reconnecting": "正在重新连接桌面…", - "regenerateHint": "清除消息以重新生成。", - "stageAll": "暂存全部", - "unstageAll": "取消全部暂存" + "regenerateHint": "清除消息以重新生成。" }, "createPrAction": { "checkingStatus": "正在检查评审状态…", @@ -2802,20 +2786,15 @@ "titleUnpublishedForcePushCount_one": "使用 lease 强制推送 {{count}} 个分支提交,并在需要时设置上游。", "titleUnpublishedForcePushCount_other": "使用 lease 强制推送 {{count}} 个分支提交,并在需要时设置上游。" }, - "fileList": { - "committedSection": "已提交的更改" - }, "fileRows": { "addedByThem": "对方已添加", "addedByUs": "我方已添加", "bothAdded": "双方均已添加", "bothDeleted": "双方均已删除", "bothModified": "双方均已修改", - "committedSection": "已提交的更改", "committedUnavailable": "已提交的更改不可用。", "deletedByThem": "对方已删除", "deletedByUs": "我方已删除", - "discardFile": "放弃 {{path}} 的更改", "loadingCommitted": "正在加载已提交的更改…", "openChangedFile": "打开已更改的文件 {{path}}", "openCommittedChange": "打开 {{path}} 中已提交的更改", @@ -2848,9 +2827,7 @@ "waitingForDesktop": "正在等待桌面…" }, "gitStatus": { - "changes": "更改", - "conflicts": "冲突", - "stagedChanges": "已暂存的更改" + "conflicts": "冲突" }, "header": { "backToSession": "返回会话", @@ -2918,7 +2895,6 @@ "waitingForDesktop": "正在等待桌面…" }, "modals": { - "actionsTitle": "源代码控制", "currentBranch": "当前", "discardButton": "放弃", "discardConfirmMessage": "放弃对“{{path}}”的更改?此操作无法撤销。", @@ -2931,15 +2907,6 @@ "panel": { "unavailableTitle": "源代码控制不可用" }, - "prChip": { - "createPr": "创建 PR", - "loading": "正在加载 PR…", - "loadingLabel": "正在加载 PR", - "openLabel": "PR #{{number}},{{state}},{{checks}}。打开 PR。", - "openWithComments_one": "PR #{{number}},{{state}},{{checks}},{{count}} 条未解决的评论。打开 PR。", - "openWithComments_other": "PR #{{number}},{{state}},{{checks}},{{count}} 条未解决的评论。打开 PR。", - "unavailableLabel": "PR 不可用:{{message}}" - }, "prCreate": { "authRequired": "请在创建 {{reviewLabel}} 之前完成身份验证。", "baseNotOnRemote": "请先推送基础分支,再创建 {{reviewLabel}}。", @@ -3027,7 +2994,6 @@ "noBranch": "无分支" }, "state": { - "noUpstream": "无上游分支", "syncStatus": "领先 {{ahead}},落后 {{behind}}" }, "textGenerationFailure": { diff --git a/mobile/src/source-control/MobileSourceControlBranchCard.tsx b/mobile/src/source-control/MobileSourceControlBranchCard.tsx deleted file mode 100644 index 44862bcddb9..00000000000 --- a/mobile/src/source-control/MobileSourceControlBranchCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Pressable, Text, View } from 'react-native' -import { GitBranch } from 'lucide-react-native' -import { colors } from '../theme/mobile-theme' -import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' -import { useMobileTranslation } from '../i18n/use-mobile-translation' -import { styles } from './mobile-source-control-styles' -import { MobileSourceControlPrChip } from './MobileSourceControlPrChip' -import type { MobilePrChipSummary } from './mobile-pr-chip-summary' -import { mobileConflictAbortLabel } from './mobile-source-control-conflict-abort' - -type Props = { - branchLabel: string - syncLabel: string | null - unstagedCount: number - stagedCount: number - branchCount: number - conflictOperation: string | null - conflictBusy: boolean - conflictAborting: boolean - onAbortConflict: (operation: string) => void - prChip: MobilePrChipSummary | null - onOpenPr: () => void -} - -export function MobileSourceControlBranchCard({ - branchLabel, - syncLabel, - unstagedCount, - stagedCount, - branchCount, - conflictOperation, - conflictBusy, - conflictAborting, - onAbortConflict, - prChip, - onOpenPr -}: Props) { - useMobileColorScheme() - const t = useMobileTranslation() - const showConflict = conflictOperation !== null && conflictOperation !== 'unknown' - const conflictLabel = - conflictOperation === 'merge' - ? t('sourceControl.branchCard.mergeState', 'merge') - : conflictOperation === 'rebase' - ? t('sourceControl.branchCard.rebaseState', 'rebase') - : conflictOperation === 'cherry-pick' - ? t('sourceControl.branchCard.cherryPickState', 'cherry-pick') - : conflictOperation - return ( - - - - - - {branchLabel} - - - {syncLabel ? {syncLabel} : null} - - - - {t('sourceControl.branchCard.changedCount', '{{count}} changed', { - count: unstagedCount - })} - - - {t('sourceControl.branchCard.stagedCount', '{{count}} staged', { count: stagedCount })} - - {branchCount > 0 ? ( - - {t('sourceControl.branchCard.branchCount', '{{count}} on branch', { - count: branchCount - })} - - ) : null} - - {/* Keep Abort on its own row so long counts cannot overflow the card. */} - {showConflict ? ( - - {conflictLabel} - {conflictOperation === 'merge' || conflictOperation === 'rebase' ? ( - [ - styles.abortButton, - conflictBusy && styles.abortButtonDisabled, - pressed && !conflictBusy && styles.abortPressed - ]} - disabled={conflictBusy} - onPress={() => onAbortConflict(conflictOperation)} - > - - {mobileConflictAbortLabel(conflictOperation, conflictAborting)} - - - ) : null} - - ) : null} - {prChip ? : null} - - ) -} diff --git a/mobile/src/source-control/MobileSourceControlChangesHeader.tsx b/mobile/src/source-control/MobileSourceControlChangesHeader.tsx new file mode 100644 index 00000000000..c7e8e4ccf8f --- /dev/null +++ b/mobile/src/source-control/MobileSourceControlChangesHeader.tsx @@ -0,0 +1,192 @@ +import { Pressable, Text, View } from 'react-native' +import { + ChevronDown, + ChevronRight, + GitBranch, + GitCommitHorizontal, + TriangleAlert +} from 'lucide-react-native' +import { colors } from '../theme/mobile-theme' +import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' +import type { Translate } from '../i18n/mobile-i18n' +import { useMobileTranslation } from '../i18n/use-mobile-translation' +import type { GitConflictOperation } from '../../../src/shared/git-status-types' +import { + isMobileConflictAborting, + mobileConflictAbortLabel +} from './mobile-source-control-conflict-abort' +import type { MobileSourceControlState } from './use-mobile-source-control-state' +import { styles } from './mobile-source-control-styles' + +type HeaderState = Pick< + MobileSourceControlState, + | 'status' + | 'branchLabel' + | 'branchCompareResult' + | 'busyAction' + | 'openBranchPicker' + | 'abortConflictOperation' +> + +function syncMeta( + status: MobileSourceControlState['status'], + operation: GitConflictOperation | null, + t: Translate +): string | null { + switch (operation) { + case 'merge': + return t('sourceControl.changesHeader.merging', 'Merging') + case 'rebase': + return t('sourceControl.changesHeader.rebasing', 'Rebasing') + case 'cherry-pick': + return t('sourceControl.changesHeader.cherryPicking', 'Cherry-picking') + case 'unknown': + case null: + break + } + const upstream = status?.upstreamStatus + if (!upstream) { + return null + } + if (!upstream.hasUpstream) { + return t('sourceControl.changesHeader.notPublished', 'Not published') + } + if (upstream.ahead > 0 && upstream.behind > 0) { + return t('sourceControl.state.syncStatus', '{{ahead}} ahead, {{behind}} behind', upstream) + } + if (upstream.ahead > 0) { + return t('sourceControl.changesHeader.notPushed', '{{count}} not pushed', { + count: upstream.ahead + }) + } + if (upstream.behind > 0) { + return t('sourceControl.changesHeader.behind', '{{count}} behind', { count: upstream.behind }) + } + return t('sourceControl.changesHeader.upToDate', 'Up to date') +} + +/** Above the Changes list: the branch and its sync state, the Commits row, and a paused merge. */ +export function MobileSourceControlChangesHeader({ + state, + onOpenCommits +}: { + state: HeaderState + onOpenCommits: () => void +}) { + useMobileColorScheme() + const t = useMobileTranslation() + const { status, busyAction } = state + const rawOperation = status?.conflictOperation ?? null + const operation = rawOperation === 'unknown' ? null : rawOperation + const meta = syncMeta(status, operation, t) + const commits = state.branchCompareResult?.summary.commitsAhead ?? 0 + const unresolved = + status?.entries.filter((entry) => entry.conflictStatus === 'unresolved').length ?? 0 + const aborting = isMobileConflictAborting(busyAction, operation) + return ( + + + [styles.branchButton, pressed && styles.sectionActionPressed]} + onPress={state.openBranchPicker} + disabled={busyAction !== null} + accessibilityRole="button" + accessibilityLabel={t( + 'sourceControl.changesHeader.switchBranch', + 'Switch branch from {{branch}}', + { + branch: state.branchLabel + } + )} + > + + + {state.branchLabel} + + + + {meta ? {meta} : null} + + {operation ? ( + + + + + {operation === 'merge' + ? t('sourceControl.changesHeader.mergeInProgress', 'Merge in progress') + : operation === 'rebase' + ? t('sourceControl.changesHeader.rebaseInProgress', 'Rebase in progress') + : t( + 'sourceControl.changesHeader.cherryPickInProgress', + 'Cherry-pick in progress' + )} + + {operation === 'merge' || operation === 'rebase' ? ( + [ + styles.abortButton, + busyAction !== null && styles.abortButtonDisabled, + pressed && busyAction === null && styles.abortPressed + ]} + disabled={busyAction !== null} + onPress={() => void state.abortConflictOperation(operation)} + accessibilityLabel={mobileConflictAbortLabel(operation, aborting)} + > + + {mobileConflictAbortLabel(operation, aborting)} + + + ) : null} + + + {unresolved > 0 + ? unresolved === 1 + ? t( + 'sourceControl.changesHeader.filesNeedDecision_one', + '{{count}} file needs a decision before you can finish.', + { count: unresolved } + ) + : t( + 'sourceControl.changesHeader.filesNeedDecision_other', + '{{count}} files need a decision before you can finish.', + { count: unresolved } + ) + : status?.didHitLimit === true + ? // A capped listing can hide the remaining conflicts; never claim they're done. + t( + 'sourceControl.changesHeader.conflictsMayRemain', + 'Not every file is listed, so conflicts may remain. Check on your computer.' + ) + : operation === 'merge' + ? t( + 'sourceControl.changesHeader.conflictsResolved', + 'Every conflict is resolved. Finish from the bar below.' + ) + : // Mobile has no continue for a rebase or cherry-pick. + t( + 'sourceControl.changesHeader.continueOnComputer', + 'Every conflict is resolved. Continue on your computer to finish.' + )} + + + ) : null} + {commits > 0 ? ( + [styles.commitsRow, pressed && styles.fileRowPressed]} + onPress={onOpenCommits} + accessibilityRole="button" + > + + + {commits === 1 + ? t('sourceControl.changesHeader.commits_one', '{{count}} commit', { count: commits }) + : t('sourceControl.changesHeader.commits_other', '{{count}} commits', { + count: commits + })} + + + + ) : null} + + ) +} diff --git a/mobile/src/source-control/MobileSourceControlContent.test.tsx b/mobile/src/source-control/MobileSourceControlContent.test.tsx index 9716e64b8b3..c40b3d2f57e 100644 --- a/mobile/src/source-control/MobileSourceControlContent.test.tsx +++ b/mobile/src/source-control/MobileSourceControlContent.test.tsx @@ -9,7 +9,6 @@ import type { MobileSourceControlState } from './use-mobile-source-control-state type MobileSourceControlContentState = Omit & { primaryAction: MobileSourceControlPrimaryAction } -import * as branchFormat from './mobile-branch-entry-format' import type { SectionListProps } from 'react-native' import type { MobileBranchEntryView, @@ -23,7 +22,8 @@ vi.mock('react-native', () => ({ SectionList: (props: SectionListProps) => createElement( 'SectionList', - props, + { ...props, ListHeaderComponent: undefined }, + typeof props.ListHeaderComponent === 'function' ? null : props.ListHeaderComponent, typeof props.ListFooterComponent === 'function' ? createElement(props.ListFooterComponent) : props.ListFooterComponent, @@ -53,16 +53,42 @@ vi.mock('react-native', () => ({ View: 'View' })) vi.mock('expo-clipboard', () => ({ setStringAsync: async () => {} })) +// Counts row renders: every file row draws its line stats once. +const rowRenders = vi.hoisted(() => ({ count: 0 })) +vi.mock('../components/MobileDiffReviewFileSummary', () => ({ + MobileDiffLineStats: () => { + rowRenders.count += 1 + return null + } +})) +vi.mock('react-native-gesture-handler', async () => { + const chain = new Proxy({}, { get: () => () => chain }) + return { + Gesture: { Pan: () => chain }, + GestureDetector: ({ children }: { children: unknown }) => children + } +}) +vi.mock('react-native-reanimated', () => ({ + default: { View: ({ children }: { children: unknown }) => children }, + Easing: { bezier: () => () => 0 }, + ReduceMotion: { Never: 'never' }, + useAnimatedStyle: () => ({}), + useReducedMotion: () => false, + useSharedValue: (value: number) => ({ value }), + withTiming: (value: number) => value +})) vi.mock('../components/ConfirmModal', () => ({ ConfirmModal: () => null })) vi.mock('lucide-react-native', () => ({ ArrowDown: 'ArrowDown', ArrowDownUp: 'ArrowDownUp', ArrowUp: 'ArrowUp', Check: 'Check', + ChevronDown: 'ChevronDown', ChevronRight: 'ChevronRight', CloudUpload: 'CloudUpload', FileText: 'FileText', GitBranch: 'GitBranch', + GitCommitHorizontal: 'GitCommitHorizontal', GitMerge: 'GitMerge', GitPullRequest: 'GitPullRequest', GitPullRequestArrow: 'GitPullRequestArrow', @@ -72,7 +98,9 @@ vi.mock('lucide-react-native', () => ({ Plus: 'Plus', RefreshCw: 'RefreshCw', Sparkles: 'Sparkles', - Trash2: 'Trash2' + Trash2: 'Trash2', + TriangleAlert: 'TriangleAlert', + Undo2: 'Undo2' })) const noop = () => {} @@ -154,7 +182,6 @@ function initialState(): MobileSourceControlContentState { status: null, sections: [], branchCompareResult, - branchCompareSummaryText: '20 files vs main', branchEntries, shouldShowBranchCompareSection: true, hasVisibleChanges: true, @@ -164,7 +191,6 @@ function initialState(): MobileSourceControlContentState { unstagedCount: 0, branchLabel: 'fixture', upstreamKnown: false, - syncLabel: null, primaryAction: { kind: 'commit', label: 'Commit', @@ -227,7 +253,11 @@ function Fixture({ initial }: { initial: MobileSourceControlContentState }) { return createElement( 'View', null, - createElement(MobileSourceControlContent, { state: liveState, bottomInset: 0 }), + createElement(MobileSourceControlContent, { + state: liveState, + bottomInset: 0, + onOpenCommits: noop + }), createElement(MobileSourceControlNextStepBar, { view: { kind: 'composer' }, notice: null, @@ -263,7 +293,10 @@ function rows() { ) } function text() { - return JSON.stringify(renderer?.toJSON()) + // Section headers carry React elements in props; serialize only what renders. + return JSON.stringify(renderer?.toJSON(), (key, value) => + key === 'sections' ? undefined : value + ) } afterEach(() => { @@ -279,10 +312,10 @@ describe('committed branch files during commit-message typing', () => { if (layout === 'dirty') { initial.sections = [{ id: 'staged', area: 'staged', title: 'Staged Changes', data: [] }] } - const format = vi.spyOn(branchFormat, 'formatMobileBranchEntryMeta') + rowRenders.count = 0 render(initial) - expect(format).toHaveBeenCalledTimes(20) - format.mockClear() + expect(rowRenders.count).toBe(20) + rowRenders.count = 0 for (let index = 0; index < 50; index++) { act(() => { const input = renderer?.root.find((node) => String(node.type) === 'TextInput') @@ -296,7 +329,7 @@ describe('committed branch files during commit-message typing', () => { 'message 49' ) expect(rows()).toHaveLength(20) - expect(format).not.toHaveBeenCalled() + expect(rowRenders.count).toBe(0) }) it.each([false, true])( @@ -324,10 +357,7 @@ describe('committed branch files during commit-message typing', () => { it('updates rows and section messages when their inputs change', () => { const initial = initialState() - const format = vi.spyOn(branchFormat, 'formatMobileBranchEntryMeta') render(initial) - patch({ branchCompareSummaryText: 'changed summary' }) - expect(text()).toContain('changed summary') const first = initial.branchEntries[0] if (!first) { throw new Error('missing fixture entry') @@ -370,14 +400,12 @@ describe('committed branch files during commit-message typing', () => { patch({ branchCompareResult: initial.branchCompareResult }) expect(rows()).toHaveLength(1) patch({ shouldShowBranchCompareSection: false }) - expect(text()).not.toContain('Committed on Branch') + expect(text()).not.toContain('On this branch') patch({ shouldShowBranchCompareSection: true }) expect(rows()).toHaveLength(1) - format.mockClear() - patch({ connState: 'disconnected', actionError: 'offline', keyboardLift: 100 }) + patch({ connState: 'disconnected', actionError: 'offline' }) expect(text()).toContain('Reconnecting to desktop') expect(text()).toContain('offline') - expect(format).not.toHaveBeenCalled() }) }) diff --git a/mobile/src/source-control/MobileSourceControlContent.tsx b/mobile/src/source-control/MobileSourceControlContent.tsx index 8742c66e25d..aace32ca2d9 100644 --- a/mobile/src/source-control/MobileSourceControlContent.tsx +++ b/mobile/src/source-control/MobileSourceControlContent.tsx @@ -1,11 +1,11 @@ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react' -import { ActivityIndicator, Animated, Easing, Pressable, Text, View } from 'react-native' -import { Minus, MoreHorizontal, Plus } from 'lucide-react-native' +import { ActivityIndicator, Animated, Easing, Text, View } from 'react-native' import { colors, spacing } from '../theme/mobile-theme' import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' import { useMobileTranslation } from '../i18n/use-mobile-translation' import { useReducedMotionEnabled } from '../platform/use-reduced-motion' import { SHEET_CURVE } from '../components/sheet-motion-values' +import { MobileSourceControlChangesHeader } from './MobileSourceControlChangesHeader' import { MobileCommitFailurePanel } from './MobileCommitFailurePanel' import { MobileSourceControlFileList } from './MobileSourceControlFileList' import type { MobileSourceControlState } from './use-mobile-source-control-state' @@ -44,31 +44,13 @@ type Props = { state: Omit /** Clears the next-step bar the panel floats over the list. */ bottomInset: number + onOpenCommits: () => void } -export function MobileSourceControlContent({ state, bottomInset }: Props) { +export function MobileSourceControlContent({ state, bottomInset, onOpenCommits }: Props) { useMobileColorScheme() const t = useMobileTranslation() - const { - connState, - busyAction, - setShowActionSheet, - setDiscardTarget, - actionError, - commitFailureRecovery, - commitFailureRecoveryAction, - sections, - hasVisibleChanges, - status, - stageablePaths, - unstageablePaths, - stageAll, - unstageAll, - openDiffForFile, - resolveConflict, - runGitAction - } = state - const ioBusy = busyAction !== null + const { busyAction, hasVisibleChanges } = state // Commit runners keep busyAction set ('commit…') until the refreshed status has landed. const [listShown, setListShown] = useState(hasVisibleChanges) const [cleanedByCommit, setCleanedByCommit] = useState(false) @@ -76,56 +58,111 @@ export function MobileSourceControlContent({ state, bottomInset }: Props) { setListShown(hasVisibleChanges) setCleanedByCommit(!hasVisibleChanges && busyAction?.startsWith('commit') === true) } - // A capped all-staged prefix can hide unstaged rows. - const canStageAll = stageablePaths.length > 0 || status?.didHitLimit === true - const listBottomInset = bottomInset + spacing.lg - const fileList = useMemo( + // Pick only what the list reads, so commit-message keystrokes never re-render its rows. + const { + connState, + status, + branchLabel, + branchCompareResult, + actionError, + commitFailureRecovery, + commitFailureRecoveryAction, + openBranchPicker, + abortConflictOperation, + sections, + shouldShowBranchCompareSection, + branchEntries, + branchCompareState, + stageablePaths, + unstageablePaths, + stageAll, + unstageAll, + openDiffForFile, + resolveConflict, + runGitAction, + setDiscardTarget + } = state + const header = useMemo( () => ( - ), [ - listBottomInset, - sections, + abortConflictOperation, + branchCompareResult, + branchLabel, + busyAction, + onOpenCommits, + openBranchPicker, + status + ] + ) + const list = useMemo( + () => ( + + ), + [ + bottomInset, + branchCompareResult, + branchCompareState, + branchEntries, + busyAction, + header, openDiffForFile, resolveConflict, runGitAction, + sections, setDiscardTarget, - state.shouldShowBranchCompareSection, - state.branchCompareSummaryText, - state.branchEntries, - state.branchCompareState, - state.branchCompareResult, - busyAction + shouldShowBranchCompareSection, + stageAll, + stageablePaths, + status, + unstageAll, + unstageablePaths ] ) - - return ( - <> - {connState !== 'connected' ? ( - // Retained rows look actionable offline; show reconnect state beside them. - - - - {t('sourceControl.content.reconnecting', 'Reconnecting to desktop...')} - - - ) : null} + // Outside the list, so a failure stays in view however far the rows are scrolled. + const feedback = + connState !== 'connected' || commitFailureRecovery || actionError ? ( + {connState !== 'connected' ? ( + // Retained rows look actionable offline; show reconnect state beside them. + + + + {t('sourceControl.content.reconnecting', 'Reconnecting to desktop...')} + + + ) : null} {commitFailureRecovery ? ( ) : null} - - [ - styles.bulkButton, - (!canStageAll || ioBusy) && styles.bulkButtonDisabled, - pressed && styles.bulkButtonPressed - ]} - onPress={() => void stageAll()} - disabled={ioBusy || !canStageAll} - > - {busyAction === 'stage-all' ? ( - - ) : ( - - )} - - {t('sourceControl.content.stageAll', 'Stage All')} - - - [ - styles.bulkButton, - (unstageablePaths.length === 0 || ioBusy) && styles.bulkButtonDisabled, - pressed && styles.bulkButtonPressed - ]} - onPress={() => void unstageAll()} - disabled={ioBusy || unstageablePaths.length === 0} - > - {busyAction === 'unstage-all' ? ( - - ) : ( - - )} - - {t('sourceControl.content.unstageAll', 'Unstage All')} - - - [ - styles.bulkMenuButton, - pressed && styles.bulkButtonPressed, - ioBusy && styles.bulkButtonDisabled - ]} - onPress={() => setShowActionSheet(true)} - disabled={ioBusy} - hitSlop={8} - accessibilityLabel={t( - 'sourceControl.content.openActions', - 'Open source control actions' - )} - > - - - - - {!hasVisibleChanges ? ( + ) : null + if (!hasVisibleChanges) { + return ( + + {feedback} + {header} {t('sourceControl.content.emptyTitle', 'No local changes')} @@ -203,9 +190,13 @@ export function MobileSourceControlContent({ state, bottomInset }: Props) { {t('sourceControl.content.emptyMessage', 'Working tree is clean.')} - ) : ( - fileList - )} - + + ) + } + return ( + + {feedback} + {list} + ) } diff --git a/mobile/src/source-control/MobileSourceControlFileList.tsx b/mobile/src/source-control/MobileSourceControlFileList.tsx index ed9807375f1..c416c93094d 100644 --- a/mobile/src/source-control/MobileSourceControlFileList.tsx +++ b/mobile/src/source-control/MobileSourceControlFileList.tsx @@ -1,10 +1,8 @@ -import { SectionList, View } from 'react-native' +import type { ReactNode } from 'react' +import { ActivityIndicator, Pressable, SectionList, Text, View } from 'react-native' import { SectionHeader } from '../components/list/ListGroup' -import { - BranchCompareHeader, - BranchFileRow, - makeRenderFileRow -} from './MobileSourceControlFileRows' +import { colors } from '../theme/mobile-theme' +import { BranchFileRow, ChangeFileRow, type FileRowState } from './MobileSourceControlFileRows' import type { MobileBranchEntryView, MobileGitStatusEntryView @@ -20,57 +18,124 @@ function isWorkingTreeEntry(entry: FileEntry): entry is MobileGitStatusEntryView return typeof entry.stageActionId === 'string' } -type FileSection = { key: string; title: string; data: readonly FileEntry[] } +type FileSection = { + key: string + title: string + trailing?: ReactNode + data: readonly FileEntry[] +} -export type FileListState = Pick< - MobileSourceControlState, - | 'sections' - | 'shouldShowBranchCompareSection' - | 'branchCompareSummaryText' - | 'branchEntries' - | 'branchCompareState' - | 'branchCompareResult' - | 'busyAction' - | 'openDiffForFile' - | 'resolveConflict' - | 'runGitAction' - | 'setDiscardTarget' -> +export type FileListState = FileRowState & + Pick< + MobileSourceControlState, + | 'sections' + | 'status' + | 'shouldShowBranchCompareSection' + | 'branchEntries' + | 'branchCompareState' + | 'branchCompareResult' + | 'stageablePaths' + | 'unstageablePaths' + | 'stageAll' + | 'unstageAll' + > export function MobileSourceControlFileList({ state, + header, bottomInset }: { state: FileListState - /** Clears the floating commit bar, which grows with its message. */ + /** The status line, Commits row and conflict banner above the sections. */ + header: ReactNode + /** Clears the next-step bar the panel floats over the list. */ bottomInset: number }) { const colorScheme = useMobileColorScheme() const t = useMobileTranslation() - const sections: FileSection[] = state.sections.map((section) => ({ - ...section, - key: section.id - })) + const busy = state.busyAction !== null + const merging = state.status?.conflictOperation === 'merge' + // A capped all-staged prefix can hide unstaged rows. + const canStageAll = state.stageablePaths.length > 0 || state.status?.didHitLimit === true + const sectionAction = (label: string, onPress: () => void, enabled: boolean) => ( + [ + (busy || !enabled) && styles.iconButtonDisabled, + pressed && styles.sectionActionPressed + ]} + > + {label} + + ) + // A capped listing can hide every unstaged file behind a staged prefix; keep Stage all reachable. + const cappedWithoutUnstaged = + state.status?.didHitLimit === true && + !state.sections.some((section) => section.id === 'unstaged') + const stageAllOnStaged = + cappedWithoutUnstaged && state.sections.some((section) => section.id === 'staged') + const stageAllOnConflicts = cappedWithoutUnstaged && !stageAllOnStaged + const stageAllAction = sectionAction( + t('sourceControl.fileList.stageAll', 'Stage all'), + () => void state.stageAll(), + canStageAll + ) + const sections: FileSection[] = state.sections.map((section) => { + const title = + merging && section.id === 'staged' + ? t('sourceControl.fileList.mergedCleanly', 'Merged cleanly') + : section.title + return { + key: section.id, + title: `${title} ${section.data.length}`, + trailing: + section.id === 'unstaged' || (section.id === 'conflicts' && stageAllOnConflicts) ? ( + stageAllAction + ) : section.id === 'staged' ? ( + + {stageAllOnStaged ? stageAllAction : null} + {merging + ? null + : sectionAction( + t('sourceControl.fileList.unstageAll', 'Unstage all'), + () => void state.unstageAll(), + state.unstageablePaths.length > 0 + )} + + ) : undefined, + data: section.data + } + }) + const compare = state.branchCompareResult if (state.shouldShowBranchCompareSection) { + const ready = state.branchCompareState.kind === 'ready' && compare?.summary.status === 'ready' sections.push({ key: 'committed', - title: t('sourceControl.fileList.committedSection', 'Committed on Branch'), - data: - state.branchCompareState.kind === 'ready' && - state.branchCompareResult?.summary.status === 'ready' - ? state.branchEntries - : [] + title: t('sourceControl.fileList.onThisBranch', 'On this branch {{count}}', { + count: state.branchEntries.length + }), + trailing: compare?.summary.baseRef ? ( + + {t('sourceControl.fileList.versusBase', 'vs {{base}}', { + base: compare.summary.baseRef.replace(/^origin\//, '') + })} + + ) : undefined, + data: ready ? state.branchEntries : [] }) } - const renderFileRow = makeRenderFileRow(state, t) return ( style={hubStyles.tabBody} sections={sections} extraData={colorScheme} + ListHeaderComponent={<>{header}} renderItem={({ item }) => isWorkingTreeEntry(item) ? ( - renderFileRow({ item }) + ) : ( ) @@ -78,17 +143,43 @@ export function MobileSourceControlFileList({ keyExtractor={(item) => `${isWorkingTreeEntry(item) ? item.area : 'committed'}:${item.path}:${item.oldPath ?? ''}` } - renderSectionHeader={({ section }) => - section.key === 'committed' ? ( - - ) : ( + renderSectionHeader={({ section }) => ( + - + - ) - } + {section.key === 'committed' ? : null} + + )} stickySectionHeadersEnabled={false} contentContainerStyle={[styles.listContent, { paddingBottom: bottomInset }]} /> ) } + +// Loading or failure under "On this branch", where its rows would be. +function BranchCompareState({ state }: { state: FileListState }) { + useMobileColorScheme() + const t = useMobileTranslation() + const { branchCompareState, branchCompareResult } = state + const message = + branchCompareState.kind === 'error' + ? branchCompareState.message + : branchCompareResult && branchCompareResult.summary.status !== 'ready' + ? (branchCompareResult.summary.errorMessage ?? + t('sourceControl.fileRows.committedUnavailable', 'Committed changes unavailable.')) + : null + if (branchCompareState.kind !== 'loading' && message === null) { + return null + } + return ( + + {branchCompareState.kind === 'loading' ? ( + + ) : null} + + {message ?? t('sourceControl.fileRows.loadingCommitted', 'Loading committed changes...')} + + + ) +} diff --git a/mobile/src/source-control/MobileSourceControlFileList.web.test.tsx b/mobile/src/source-control/MobileSourceControlFileList.web.test.tsx index 04a409dc2c7..b84e3071a72 100644 --- a/mobile/src/source-control/MobileSourceControlFileList.web.test.tsx +++ b/mobile/src/source-control/MobileSourceControlFileList.web.test.tsx @@ -5,7 +5,24 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { MobileSourceControlFileList, type FileListState } from './MobileSourceControlFileList' vi.mock('react-native', () => vi.importActual('react-native-web')) +vi.mock('react-native-gesture-handler', async () => { + const chain = new Proxy({}, { get: () => () => chain }) + return { + Gesture: { Pan: () => chain }, + GestureDetector: ({ children }: { children: unknown }) => children + } +}) +vi.mock('react-native-reanimated', () => ({ + default: { View: ({ children }: { children: unknown }) => children }, + Easing: { bezier: () => () => 0 }, + ReduceMotion: { Never: 'never' }, + useAnimatedStyle: () => ({}), + useReducedMotion: () => false, + useSharedValue: (value: number) => ({ value }), + withTiming: (value: number) => value +})) vi.mock('lucide-react-native', () => ({ + Undo2: () => null, ArrowDown: () => null, ArrowDownUp: () => null, ArrowUp: () => null, @@ -66,8 +83,12 @@ function fileListState(count: number, dirty: boolean): FileListState { } ] : [], + status: null, + stageablePaths: dirty ? ['src/uncommitted.ts'] : [], + unstageablePaths: [], + stageAll: vi.fn(async () => {}), + unstageAll: vi.fn(async () => {}), shouldShowBranchCompareSection: true, - branchCompareSummaryText: `${count} files vs main`, branchEntries, branchCompareState: { kind: 'ready', result: branchCompareResult }, branchCompareResult, @@ -102,14 +123,19 @@ describe('source-control file virtualization in the mobile web page', () => { const state = fileListState(count, dirty) await act(async () => { root.render( - createElement(MobileSourceControlFileList, { key: count, state, bottomInset: 0 }) + createElement(MobileSourceControlFileList, { + key: count, + state, + header: null, + bottomInset: 0 + }) ) }) const rows = container.querySelectorAll('[aria-label^="Open committed change "]') mountedCounts.push(rows.length) expect(rows.length).toBeGreaterThan(0) expect(rows.length).toBeLessThan(50) - expect(container.textContent).toContain(`${count} files vs main`) + expect(container.textContent).toContain(`On this branch ${count}`) expect( container.querySelector('[aria-label="Open committed change src/committed-999.ts"]') ).toBeNull() diff --git a/mobile/src/source-control/MobileSourceControlFileRows.tsx b/mobile/src/source-control/MobileSourceControlFileRows.tsx index 01b4bb26f1c..85cb7dbad3c 100644 --- a/mobile/src/source-control/MobileSourceControlFileRows.tsx +++ b/mobile/src/source-control/MobileSourceControlFileRows.tsx @@ -1,13 +1,23 @@ +import type { ReactNode } from 'react' import { ActivityIndicator, Pressable, Text, View } from 'react-native' -import { ChevronRight, FileText, Minus, Plus, Trash2 } from 'lucide-react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import Animated, { + Easing, + ReduceMotion, + useAnimatedStyle, + useSharedValue, + withTiming +} from 'react-native-reanimated' +import { SHEET_CURVE } from '../components/sheet-motion-values' +import { useDrawerReducedMotion } from '../components/bottom-drawer-constants' +import { Minus, Plus, Undo2 } from 'lucide-react-native' import { colors } from '../theme/mobile-theme' -import { SectionHeader } from '../components/list/ListGroup' import { STATUS_LABELS } from '../../../src/shared/git-status-labels' -import type { GitConflictKind } from '../../../src/shared/git-status-types' +import type { GitConflictKind, GitFileStatus } from '../../../src/shared/git-status-types' import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' import type { Translate } from '../i18n/mobile-i18n' import { useMobileTranslation } from '../i18n/use-mobile-translation' -import { formatMobileBranchEntryMeta } from './mobile-branch-entry-format' +import { MobileDiffLineStats } from '../components/MobileDiffReviewFileSummary' import type { MobileBranchEntryView, MobileGitStatusEntryView @@ -17,7 +27,7 @@ import { styles } from './mobile-source-control-styles' import { mobileConflictResolutionLabel } from './mobile-source-control-conflict-actions' import { getUnstageablePaths, statusColor } from './mobile-git-status' -type RowState = Pick< +export type FileRowState = Pick< MobileSourceControlState, 'busyAction' | 'openDiffForFile' | 'resolveConflict' | 'runGitAction' | 'setDiscardTarget' > @@ -43,227 +53,288 @@ function conflictKindLabel(kind: GitConflictKind | undefined, t: Translate): str } } -export function makeRenderFileRow(state: RowState, t: Translate) { - const { busyAction, openDiffForFile, resolveConflict, runGitAction, setDiscardTarget } = state - return function renderFileRow({ item }: { item: MobileGitStatusEntryView }) { - const rowBusy = - busyAction === item.stageActionId || - busyAction === item.unstageActionId || - busyAction === item.discardActionId || - busyAction === item.resolveActionId - const rowDisabled = !item.canOpen || busyAction !== null - const ioBusy = busyAction !== null - const hasConflictActions = item.conflictResolutions.length > 0 - return ( +// Git paths always use '/'; the name leads so it never truncates behind its folder. +function splitGitPath(path: string): { name: string; folder: string } { + const slash = path.lastIndexOf('/') + return slash === -1 + ? { name: path, folder: '' } + : { name: path.slice(slash + 1), folder: path.slice(0, slash) } +} + +/** Status letter, file name over its folder, then the row's stats and action. */ +function FileRowBody({ + status, + path, + detail, + added, + removed, + trailing, + actions +}: { + status: GitFileStatus + path: string + /** Leads the folder line, e.g. a conflict kind or a rename source. */ + detail?: string + added?: number + removed?: number + trailing?: ReactNode + /** Wrapping buttons under the name, e.g. a conflict's resolutions, so the name keeps its width. */ + actions?: ReactNode +}) { + useMobileColorScheme() + const { name, folder } = splitGitPath(path) + const subtitle = detail ? (folder ? `${detail} · ${folder}` : detail) : folder + return ( + <> + + + {STATUS_LABELS[status]} + + + + + {name} + + {subtitle ? ( + + {subtitle} + + ) : null} + {actions ? {actions} : null} + + + {trailing} + + ) +} + +// The width the Discard action uncovers; a swipe past half of it settles open. +const DISCARD_WIDTH = 84 +const SWIPE_SETTLE_MS = 250 +const DRAG_CLICK_WINDOW_MS = 400 + +/** Discard sits behind a left swipe and still asks first; it is also on the diff screen. */ +function SwipeToDiscard({ + available, + enabled, + label, + accessibilityLabel, + onDiscard, + children +}: { + /** Only discardable rows get the swipe; toggling it would remount the row. */ + available: boolean + enabled: boolean + label: string + accessibilityLabel: string + onDiscard: () => void + /** Gets a wrapper for the row's press: a tap on a swiped row closes it instead. */ + children: (guardPress: (onPress: () => void) => () => void) => ReactNode +}) { + useMobileColorScheme() + const reducedMotion = useDrawerReducedMotion() + const offset = useSharedValue(0) + const start = useSharedValue(0) + // When the last drag ended: on the web, releasing a mouse drag also clicks the row. + const dragEndedAt = useSharedValue(0) + // Built per render: a module-level easing would run before a test's Reanimated mock can serve it. + const settle = (to: number) => { + 'worklet' + return withTiming( + to, + reducedMotion + ? { duration: 0, reduceMotion: ReduceMotion.Never } + : { duration: SWIPE_SETTLE_MS, easing: Easing.bezier(...SHEET_CURVE) } + ) + } + // Horizontal only, so the list keeps scrolling under a vertical drag. + const pan = Gesture.Pan() + .enabled(enabled) + .activeOffsetX([-12, 12]) + .failOffsetY([-10, 10]) + .onBegin(() => { + start.value = offset.value + }) + .onUpdate((event) => { + offset.value = Math.min(0, Math.max(-DISCARD_WIDTH, start.value + event.translationX)) + }) + .onEnd(() => { + // oxlint-disable-next-line react/purity -- runs when the drag ends, never during render. + dragEndedAt.value = Date.now() + offset.value = settle(offset.value < -DISCARD_WIDTH / 2 ? -DISCARD_WIDTH : 0) + }) + // Web has no worklet plugin; mapper hooks need this explicit dependency list. + const rowStyle = useAnimatedStyle(() => ({ transform: [{ translateX: offset.value }] }), [offset]) + // A tap on an open row closes it rather than opening the diff. + const guardPress = (onPress: () => void) => () => { + // The click a drag release produces belongs to the drag, not to the row. + if (Date.now() - dragEndedAt.value < DRAG_CLICK_WINDOW_MS) { + return + } + if (offset.value !== 0) { + offset.value = settle(0) + return + } + onPress() + } + if (!available) { + return children((onPress) => onPress) + } + return ( + [ - styles.fileRow, - pressed && item.canOpen && styles.fileRowPressed, - // Conflict rows cannot open but carry live actions; their buttons dim themselves while busy. - rowDisabled && !hasConflictActions && styles.fileRowDisabled, - !item.canOpen && !hasConflictActions && styles.fileRowUnavailable - ]} + style={({ pressed }) => [styles.discardAction, pressed && styles.iconButtonPressed]} + disabled={!enabled} onPress={() => { - if (item.area !== undefined) { - openDiffForFile({ kind: 'changes', filePath: item.path, area: item.area }) - } + offset.value = settle(0) + onDiscard() }} - disabled={rowDisabled} - // An accessible row hides nested buttons from iOS VoiceOver; conflict rows cannot open anyway. - accessible={!hasConflictActions} - accessibilityLabel={t( - 'sourceControl.fileRows.openChangedFile', - 'Open changed file {{path}}', - { - path: item.path - } - )} + accessibilityRole="button" + accessibilityLabel={accessibilityLabel} > - - - {STATUS_LABELS[item.status]} - - - - - - {item.path} - - {item.oldPath ? ( - - {t('sourceControl.fileRows.renamedFrom', 'from {{path}}', { path: item.oldPath })} - - ) : item.conflictStatus === 'unresolved' ? ( - - {conflictKindLabel(item.conflictKind, t)} - - ) : null} - {!rowBusy && hasConflictActions ? ( - - {item.conflictResolutions.map((resolution) => ( - [ - styles.conflictActionButton, - ioBusy && styles.iconButtonDisabled, - pressed && styles.iconButtonPressed - ]} - disabled={ioBusy} - onPress={() => void resolveConflict(item, resolution)} - hitSlop={4} - accessibilityLabel={t( - 'sourceControl.conflictActions.resolveFile', - '{{action}}: {{path}}', - { - action: mobileConflictResolutionLabel(resolution, t), - path: item.path - } - )} - > - - {mobileConflictResolutionLabel(resolution, t)} - - - ))} - - ) : null} - - {rowBusy ? ( - - ) : item.canUnstage ? ( + + {label} + + {/* pan-y keeps the page scrolling vertically over the row on the web. */} + + {children(guardPress)} + + + ) +} + +export function ChangeFileRow({ + item, + state +}: { + item: MobileGitStatusEntryView + state: FileRowState +}) { + useMobileColorScheme() + const t = useMobileTranslation() + const { busyAction, openDiffForFile, resolveConflict, runGitAction, setDiscardTarget } = state + const rowBusy = + busyAction === item.stageActionId || + busyAction === item.unstageActionId || + busyAction === item.discardActionId || + busyAction === item.resolveActionId + const ioBusy = busyAction !== null + const hasConflictActions = item.conflictResolutions.length > 0 + const detail = item.oldPath + ? t('sourceControl.fileRows.renamedFrom', 'from {{path}}', { path: item.oldPath }) + : item.conflictStatus === 'unresolved' + ? conflictKindLabel(item.conflictKind, t) + : undefined + const stage = () => void runGitAction(item.stageActionId, 'git.stage', { filePath: item.path }) + const unstage = () => { + const filePaths = getUnstageablePaths([item]) + void (filePaths.length > 1 + ? runGitAction(item.unstageActionId, 'git.bulkUnstage', { filePaths }) + : runGitAction(item.unstageActionId, 'git.unstage', { filePath: item.path })) + } + // Every press in the row goes through the swipe's guard, so a drag never also presses a button. + const conflictActions = (guard: (onPress: () => void) => () => void) => + !rowBusy && hasConflictActions + ? item.conflictResolutions.map((resolution) => ( [ - styles.iconButton, + styles.conflictActionButton, ioBusy && styles.iconButtonDisabled, pressed && styles.iconButtonPressed ]} disabled={ioBusy} - onPress={() => { - const filePaths = getUnstageablePaths([item]) - void (filePaths.length > 1 - ? runGitAction(item.unstageActionId, 'git.bulkUnstage', { filePaths }) - : runGitAction(item.unstageActionId, 'git.unstage', { filePath: item.path })) - }} - hitSlop={8} - accessibilityLabel={t('sourceControl.fileRows.unstageFile', 'Unstage {{path}}', { - path: item.path - })} + onPress={guard(() => void resolveConflict(item, resolution))} + hitSlop={4} + accessibilityLabel={t( + 'sourceControl.conflictActions.resolveFile', + '{{action}}: {{path}}', + { + action: mobileConflictResolutionLabel(resolution, t), + path: item.path + } + )} > - + + {mobileConflictResolutionLabel(resolution, t)} + - ) : item.canStage || item.canDiscard ? ( - - {item.canStage ? ( - [ - styles.iconButton, - ioBusy && styles.iconButtonDisabled, - pressed && styles.iconButtonPressed - ]} - disabled={ioBusy} - onPress={() => - void runGitAction(item.stageActionId, 'git.stage', { filePath: item.path }) - } - hitSlop={8} - accessibilityLabel={t('sourceControl.fileRows.stageFile', 'Stage {{path}}', { - path: item.path - })} - > - - - ) : null} - {item.canDiscard ? ( - [ - styles.iconButton, - ioBusy && styles.iconButtonDisabled, - pressed && styles.iconButtonPressed - ]} - disabled={ioBusy} - onPress={() => setDiscardTarget(item)} - hitSlop={8} - accessibilityLabel={t('sourceControl.fileRows.discardFile', 'Discard {{path}}', { - path: item.path - })} - > - - - ) : null} - - ) : null} - {!rowBusy && item.canOpen ? ( - - ) : null} + )) + : null + const trailing = (guard: (onPress: () => void) => () => void) => + rowBusy ? ( + + ) : !hasConflictActions && (item.canUnstage || item.canStage) ? ( + [ + styles.iconButton, + ioBusy && styles.iconButtonDisabled, + pressed && styles.iconButtonPressed + ]} + disabled={ioBusy} + onPress={guard(item.canUnstage ? unstage : stage)} + hitSlop={8} + accessibilityLabel={ + item.canUnstage + ? t('sourceControl.fileRows.unstageFile', 'Unstage {{path}}', { path: item.path }) + : t('sourceControl.fileRows.stageFile', 'Stage {{path}}', { path: item.path }) + } + > + {item.canUnstage ? ( + + ) : ( + + )} - ) - } -} - -type BranchState = Pick< - MobileSourceControlState, - | 'shouldShowBranchCompareSection' - | 'branchCompareSummaryText' - | 'branchEntries' - | 'branchCompareState' - | 'branchCompareResult' - | 'busyAction' - | 'openDiffForFile' -> - -export function BranchCompareHeader({ state }: { state: BranchState }) { - useMobileColorScheme() - const t = useMobileTranslation() - const { - shouldShowBranchCompareSection, - branchCompareSummaryText, - branchEntries, - branchCompareState, - branchCompareResult - } = state - if (!shouldShowBranchCompareSection) { - return null - } - + ) : null return ( - - - - - {branchCompareSummaryText ? ( - - {branchCompareSummaryText} - - ) : null} - {branchCompareState.kind === 'loading' ? ( - - - - {t('sourceControl.fileRows.loadingCommitted', 'Loading committed changes...')} - - - ) : branchCompareState.kind === 'error' ? ( - - {branchCompareState.message} - - ) : branchCompareResult && branchCompareResult.summary.status !== 'ready' ? ( - - - {branchCompareResult.summary.errorMessage ?? - t('sourceControl.fileRows.committedUnavailable', 'Committed changes unavailable.')} - - - ) : null} - + setDiscardTarget(item)} + > + {(guardPress) => ( + [ + styles.fileRow, + pressed && item.canOpen && styles.fileRowPressed, + !item.canOpen && !hasConflictActions && styles.fileRowUnavailable + ]} + onPress={guardPress(() => { + if (item.area !== undefined) { + openDiffForFile({ kind: 'changes', filePath: item.path, area: item.area }) + } + })} + disabled={!item.canOpen || ioBusy} + // An accessible row hides nested buttons from iOS VoiceOver; conflict rows cannot open anyway. + accessible={!hasConflictActions} + accessibilityLabel={t( + 'sourceControl.fileRows.openChangedFile', + 'Open changed file {{path}}', + { + path: item.path + } + )} + > + + + )} + ) } @@ -272,55 +343,38 @@ export function BranchFileRow({ state }: { entry: MobileBranchEntryView - state: BranchState + state: Pick }) { useMobileColorScheme() const t = useMobileTranslation() - const { busyAction, openDiffForFile } = state - const rowDisabled = !entry.canOpen || busyAction !== null - const meta = formatMobileBranchEntryMeta(entry) return ( [ styles.fileRow, pressed && entry.canOpen && styles.fileRowPressed, - rowDisabled && styles.fileRowDisabled, !entry.canOpen && styles.fileRowUnavailable ]} - onPress={() => openDiffForFile({ kind: 'changes', filePath: entry.path, area: 'branch' })} - disabled={rowDisabled} + onPress={() => + state.openDiffForFile({ kind: 'changes', filePath: entry.path, area: 'branch' }) + } + disabled={!entry.canOpen || state.busyAction !== null} accessibilityLabel={t( 'sourceControl.fileRows.openCommittedChange', 'Open committed change {{path}}', - { - path: entry.path - } + { path: entry.path } )} > - - - {STATUS_LABELS[entry.status]} - - - - - - {entry.path} - - {meta ? ( - - {meta} - - ) : null} - - {entry.canOpen ? : null} ) } diff --git a/mobile/src/source-control/MobileSourceControlModals.tsx b/mobile/src/source-control/MobileSourceControlModals.tsx index 1d1c4686561..85dd3de09de 100644 --- a/mobile/src/source-control/MobileSourceControlModals.tsx +++ b/mobile/src/source-control/MobileSourceControlModals.tsx @@ -1,4 +1,3 @@ -import { ActionSheetModal, type ActionSheetAction } from '../components/ActionSheetModal' import { ConfirmModal } from '../components/ConfirmModal' import { PickerModal } from '../components/PickerModal' import { openMobilePrUrl } from '../components/mobile-pr-url' @@ -7,14 +6,11 @@ import type { MobileSourceControlState } from './use-mobile-source-control-state type Props = { state: MobileSourceControlState - actionSheetActions: ActionSheetAction[] } -export function MobileSourceControlModals({ state, actionSheetActions }: Props) { +export function MobileSourceControlModals({ state }: Props) { const t = useMobileTranslation() const { - showActionSheet, - setShowActionSheet, discardTarget, setDiscardTarget, showBranchPicker, @@ -24,21 +20,12 @@ export function MobileSourceControlModals({ state, actionSheetActions }: Props) setCreatedPrUrl, createdPrWarning, setCreatedPrWarning, - branchLabel, checkoutBranch, runGitAction } = state return ( <> - setShowActionSheet(false)} - /> - openHubScreen('commits'), [openHubScreen]) - const openPrTab = useCallback(() => selectTab('pr'), [selectTab]) // The PR tab owns the Create PR and Merge sheets; the bar and Changes reach them through this. const prOpeners = useRef({ createPr: () => {}, merge: () => {} }) const openCreatePr = useCallback(() => prOpeners.current.createPr(), []) @@ -99,13 +96,7 @@ export function MobileSourceControlPanel({ busyAction, loadStatus, status, - branchCompareResult, - branchLabel, - syncLabel, - unstagedCount, - stagedCount, - branchEntries, - abortConflictOperation + branchCompareResult } = state const ioBusy = busyAction !== null const ready = screenState.kind === 'ready' @@ -144,6 +135,18 @@ export function MobileSourceControlPanel({ ) const primaryActionArgs = state.primaryActionArgs const actionSheetActions = useMobileSourceControlActionSheet(state, review) + // The menu closes on a pick; the bar and the rows show the action's progress. + const gitMenu = useMemo( + () => + groupMobileSourceControlMenu( + actionSheetActions.map((action) => ({ + ...action, + id: action.id ?? '', + skipAutoClose: false + })) + ), + [actionSheetActions] + ) const primaryAction = useMemo( () => buildMobileSourceControlPrimaryAction({ ...primaryActionArgs, review }), [primaryActionArgs, review, t] @@ -197,18 +200,6 @@ export function MobileSourceControlPanel({ } }, [activeTab, isHostedRepo, prBranch, prSidebarKind, prDetailsMissingFor]) - const prChip = useMemo(() => { - // Detached HEAD never loads PR state; hide the chip instead of leaving it spinning. - if (!isHostedRepo || !prBranch) { - return null - } - const commentCount = - prController.prSidebarState.kind === 'ready' - ? countUnresolvedReviewThreads(prController.prSidebarState.data.details?.comments) - : null - return buildMobilePrChipSummary(prController.prSidebarState, commentCount) - }, [isHostedRepo, prBranch, prController.prSidebarState, t]) - const nextStep = useMobileSourceControlNextStep({ worktreeId, state, @@ -238,6 +229,23 @@ export function MobileSourceControlPanel({ } void refetchPr({ includeDetails: false }) }, [activeTab, isHostedRepo, loadStatus, refetchPr]) + // Changes: the rarer git actions, then Refresh, then Force push last and red. + const changesMenuGroups = useMemo( + () => [ + ...gitMenu.groups, + [ + { + id: 'refresh', + label: t('sourceControl.header.refresh', 'Refresh source control'), + icon: RefreshCw, + disabled: ioBusy, + onPress: onRefresh + } + ], + ...(gitMenu.forcePush ? [[{ ...gitMenu.forcePush, destructive: true }]] : []) + ], + [gitMenu, ioBusy, onRefresh, t] + ) const { prTab, prMenuGroups, branchCompare } = useMobileSourceControlPrTab({ state, activeTab, @@ -273,7 +281,13 @@ export function MobileSourceControlPanel({ onBack={onBack} onClose={onClose} onRefresh={onRefresh} - menuGroups={activeTab === 'pr' ? prMenuGroups : null} + menuGroups={ + activeTab === 'pr' + ? prMenuGroups + : activeTab === 'changes' && ready + ? changesMenuGroups + : null + } onOpenPrWeb={prWebUrl ? () => openMobilePrUrl(prWebUrl) : undefined} prNumber={prWebNumber} accessory={ @@ -317,9 +331,6 @@ export function MobileSourceControlPanel({ const showChanges = ready && (activeTab === 'changes' || visitedTabs.has('changes')) // Unmount inactive PR content to release Mermaid WebViews and avoid comment rerenders per keystroke. const showPrBody = ready && activeTab === 'pr' - const conflictOperation = status?.conflictOperation ?? null - const hasActiveConflict = conflictOperation != null && conflictOperation !== 'unknown' - const conflictAborting = isMobileConflictAborting(busyAction, conflictOperation) const hub = ( @@ -331,26 +342,13 @@ export function MobileSourceControlPanel({ )} - {/* Ready PRs duplicate the branch card; retain it only when needed to abort a conflict. */} - {ready && (activeTab !== 'pr' || hasActiveConflict) ? ( - void abortConflictOperation(operation)} - prChip={prChip} - onOpenPr={openPrTab} - /> - ) : null} - {showChanges ? ( - + ) : activeTab === 'changes' ? ( statusGate @@ -418,7 +416,7 @@ export function MobileSourceControlPanel({ ) : null } /> - + ) diff --git a/mobile/src/source-control/MobileSourceControlPrChip.tsx b/mobile/src/source-control/MobileSourceControlPrChip.tsx deleted file mode 100644 index 32318b52eb3..00000000000 --- a/mobile/src/source-control/MobileSourceControlPrChip.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { ActivityIndicator, Pressable, Text, View } from 'react-native' -import { - AlertTriangle, - Check, - ChevronRight, - CircleDot, - GitPullRequest, - MessageSquare, - X -} from 'lucide-react-native' -import { colors } from '../theme/mobile-theme' -import { useMobileColorScheme } from '../theme/use-mobile-color-scheme' -import type { Translate } from '../i18n/mobile-i18n' -import { useMobileTranslation } from '../i18n/use-mobile-translation' -import { statusColor } from '../components/pr-sidebar/pr-sidebar-status-color' -import { hubStyles } from './mobile-source-control-hub-styles' -import type { MobilePrChipRollup, MobilePrChipSummary } from './mobile-pr-chip-summary' - -type Props = { - summary: MobilePrChipSummary - onPress: () => void -} - -export function MobileSourceControlPrChip({ summary, onPress }: Props) { - useMobileColorScheme() - const t = useMobileTranslation() - return ( - [hubStyles.chip, pressed && hubStyles.chipPressed]} - onPress={onPress} - accessibilityRole="button" - accessibilityLabel={chipAccessibilityLabel(summary, t)} - > - - - - {summary.kind === 'loading' ? ( - <> - - - {t('sourceControl.prChip.loading', 'Loading pull request…')} - - - ) : summary.kind === 'none' ? ( - <> - - {t('sourceControl.prChip.createPr', 'Create pull request')} - - - - - ) : summary.kind === 'unavailable' ? ( - <> - - {summary.message} - - - - ) : ( - <> - #{summary.number} - - - {summary.stateLabel} - - - - {summary.commentCount != null && summary.commentCount > 0 ? ( - - - {summary.commentCount} - - ) : null} - - - - )} - - ) -} - -function ChipRollup({ rollup }: { rollup: MobilePrChipRollup }) { - useMobileColorScheme() - const color = statusColor(rollup.token) - return ( - - - {rollup.text} - - ) -} - -function RollupIcon({ kind, color }: { kind: MobilePrChipRollup['kind']; color: string }) { - const size = 13 - const strokeWidth = 2.3 - switch (kind) { - case 'conflict': - return - case 'failing': - return - case 'running': - return - case 'passed': - return - case 'none': - return null - } -} - -function chipAccessibilityLabel(summary: MobilePrChipSummary, t: Translate): string { - switch (summary.kind) { - case 'loading': - return t('sourceControl.prChip.loadingLabel', 'Loading pull request') - case 'none': - return t('sourceControl.prChip.createPr', 'Create pull request') - case 'unavailable': - return t('sourceControl.prChip.unavailableLabel', 'Pull request unavailable: {{message}}', { - message: summary.message - }) - case 'ready': { - const count = summary.commentCount - const details = { - number: summary.number, - state: summary.stateLabel, - checks: summary.rollup.text - } - if (count != null && count > 0) { - return count === 1 - ? t( - 'sourceControl.prChip.openWithComments_one', - 'Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request.', - { ...details, count } - ) - : t( - 'sourceControl.prChip.openWithComments_other', - 'Pull request #{{number}}, {{state}}, {{checks}}, {{count}} unresolved comments. Open pull request.', - { ...details, count } - ) - } - return t( - 'sourceControl.prChip.openLabel', - 'Pull request #{{number}}, {{state}}, {{checks}}. Open pull request.', - details - ) - } - } -} diff --git a/mobile/src/source-control/mobile-branch-compare.test.ts b/mobile/src/source-control/mobile-branch-compare.test.ts index 9e85f3d3938..e1a9095560b 100644 --- a/mobile/src/source-control/mobile-branch-compare.test.ts +++ b/mobile/src/source-control/mobile-branch-compare.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it } from 'vitest' import { buildMobileBranchCompareSection, - canOpenMobileBranchCompareDiff, - formatMobileBranchCompareSummary + canOpenMobileBranchCompareDiff } from './mobile-branch-compare' describe('mobile branch compare helpers', () => { @@ -16,22 +15,6 @@ describe('mobile branch compare helpers', () => { expect(section?.data.map((entry) => entry.path)).toEqual(['alpha.ts', 'zeta.ts']) }) - it('summarizes ready branch compares', () => { - expect( - formatMobileBranchCompareSummary({ - baseRef: 'origin/main', - baseOid: 'a'.repeat(40), - compareRef: 'HEAD', - headOid: 'b'.repeat(40), - mergeBase: 'c'.repeat(40), - changedFiles: 2, - commitsAhead: 1, - errorMessage: undefined, - status: 'ready' - }) - ).toBe('2 files - 1 commit - vs origin/main') - }) - it('only opens branch diffs when compare object ids are available', () => { expect( canOpenMobileBranchCompareDiff({ diff --git a/mobile/src/source-control/mobile-branch-compare.ts b/mobile/src/source-control/mobile-branch-compare.ts index 298a41f3e81..428ff6720d1 100644 --- a/mobile/src/source-control/mobile-branch-compare.ts +++ b/mobile/src/source-control/mobile-branch-compare.ts @@ -30,38 +30,6 @@ export function buildMobileBranchCompareSection { it('builds sections in the mobile source control order', () => { const sections = buildMobileSourceControlSections(entries, 'changes-first') - expect(sections.map((section) => section.title)).toEqual(['Changes', 'Staged Changes']) + expect(sections.map((section) => section.title)).toEqual(['Uncommitted', 'Staged']) }) it('computes actionable path sets', () => { @@ -106,7 +106,7 @@ describe('mobile source control status helpers', () => { sections.map((section) => [section.title, section.data.map((entry) => entry.path)]) ).toEqual([ ['Conflicts', ['alpha.ts', 'beta.ts']], - ['Changes', ['aardvark.ts', 'zeta.ts']] + ['Uncommitted', ['aardvark.ts', 'zeta.ts']] ]) }) diff --git a/mobile/src/source-control/mobile-git-status.ts b/mobile/src/source-control/mobile-git-status.ts index 45239451ac4..7f3fea4c54d 100644 --- a/mobile/src/source-control/mobile-git-status.ts +++ b/mobile/src/source-control/mobile-git-status.ts @@ -50,8 +50,8 @@ export type MobileSourceControlSection { return { conflicts: translate('sourceControl.gitStatus.conflicts', 'Conflicts'), - unstaged: translate('sourceControl.gitStatus.changes', 'Changes'), - staged: translate('sourceControl.gitStatus.stagedChanges', 'Staged Changes') + unstaged: translate('sourceControl.gitStatus.uncommitted', 'Uncommitted'), + staged: translate('sourceControl.gitStatus.staged', 'Staged') } } diff --git a/mobile/src/source-control/mobile-pr-chip-summary.test.ts b/mobile/src/source-control/mobile-pr-chip-summary.test.ts deleted file mode 100644 index d8b97d85e91..00000000000 --- a/mobile/src/source-control/mobile-pr-chip-summary.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { describe, it, expect } from 'vitest' -import type { PRCheckDetail } from '../../../src/shared/github/check-types' -import type { PRComment } from '../../../src/shared/github/comment-types' -import type { PRInfo } from '../../../src/shared/github/pull-request-types' -import type { PrSidebarState } from '../session/mobile-pr-sidebar-state' -import { buildMobilePrChipSummary, countUnresolvedReviewThreads } from './mobile-pr-chip-summary' - -function pr(overrides: Partial = {}): PRInfo { - return { - number: 7701, - title: 'Make center split more visible', - state: 'open', - url: 'https://example.test/pr/7701', - checksStatus: 'success', - updatedAt: '2026-07-07T00:00:00Z', - mergeable: 'MERGEABLE', - ...overrides - } -} - -function check( - conclusion: PRCheckDetail['conclusion'], - status: PRCheckDetail['status'] = 'completed' -): PRCheckDetail { - return { name: `check-${conclusion}-${status}`, status, conclusion, url: null } -} - -function ready(prInfo: PRInfo, checks: PRCheckDetail[]): PrSidebarState { - return { - kind: 'ready', - data: { pr: prInfo, checks, details: null, checksError: null, pushTarget: null } - } -} - -describe('buildMobilePrChipSummary', () => { - it('maps non-ready states', () => { - expect(buildMobilePrChipSummary({ kind: 'hidden' })).toEqual({ kind: 'loading' }) - expect(buildMobilePrChipSummary({ kind: 'loading' })).toEqual({ kind: 'loading' }) - expect(buildMobilePrChipSummary({ kind: 'none' })).toEqual({ kind: 'none' }) - expect(buildMobilePrChipSummary({ kind: 'error', message: 'net' })).toEqual({ - kind: 'unavailable', - message: 'net' - }) - expect(buildMobilePrChipSummary({ kind: 'blocked', message: 'auth' })).toEqual({ - kind: 'unavailable', - message: 'auth' - }) - }) - - it('surfaces the PR number and state badge', () => { - const summary = buildMobilePrChipSummary(ready(pr({ state: 'draft' }), [check('success')])) - expect(summary.kind).toBe('ready') - if (summary.kind !== 'ready') { - return - } - expect(summary.number).toBe(7701) - expect(summary.stateLabel).toBe('Draft') - }) - - // Skipped checks count as satisfied, matching desktop and the tasks grid. - it('rolls up passed checks as passed/total, counting skipped as passed', () => { - const summary = buildMobilePrChipSummary( - ready(pr(), [check('success'), check('success'), check('skipped')]) - ) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ kind: 'passed', text: '3/3', token: 'statusGreen' }) - }) - - it('treats a merge-blocking action_required gate as failing, not passing', () => { - const summary = buildMobilePrChipSummary( - ready(pr(), [check('success'), check('action_required')]) - ) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ kind: 'failing', text: '1 failing', token: 'statusRed' }) - }) - - it('distinguishes checks that resolved to nothing actionable from having no checks', () => { - const summary = buildMobilePrChipSummary(ready(pr(), [check('neutral')])) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ - kind: 'none', - text: 'Unresolved checks', - token: 'textSecondary' - }) - }) - - it('prefers failing over running and passing', () => { - const summary = buildMobilePrChipSummary( - ready(pr(), [check('success'), check('failure'), check(null, 'in_progress')]) - ) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ kind: 'failing', text: '1 failing', token: 'statusRed' }) - }) - - it('shows running when nothing has failed yet', () => { - const summary = buildMobilePrChipSummary(ready(pr(), [check('success'), check(null, 'queued')])) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ kind: 'running', text: '1 running', token: 'statusAmber' }) - }) - - it('lets a merge conflict win over green checks', () => { - const summary = buildMobilePrChipSummary( - ready(pr({ mergeable: 'CONFLICTING' }), [check('success')]) - ) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup.kind).toBe('conflict') - }) - - it('reports no checks when the list is empty', () => { - const summary = buildMobilePrChipSummary(ready(pr(), [])) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.rollup).toEqual({ kind: 'none', text: 'No checks', token: 'textSecondary' }) - }) - - it('passes through the unresolved comment count', () => { - const summary = buildMobilePrChipSummary(ready(pr(), [check('success')]), 3) - if (summary.kind !== 'ready') { - throw new Error('expected ready') - } - expect(summary.commentCount).toBe(3) - }) -}) - -describe('countUnresolvedReviewThreads', () => { - function comment(overrides: Partial): PRComment { - return { - id: 1, - author: 'octocat', - authorAvatarUrl: '', - body: 'x', - createdAt: '2026-07-07T00:00:00Z', - url: '', - ...overrides - } - } - - it('returns null when details have not loaded', () => { - expect(countUnresolvedReviewThreads(null)).toBeNull() - expect(countUnresolvedReviewThreads(undefined)).toBeNull() - }) - - it('counts each unresolved thread once and ignores resolved threads', () => { - const comments = [ - comment({ id: 1, threadId: 't1', isResolved: false }), - comment({ id: 2, threadId: 't1', isResolved: false }), - comment({ id: 3, threadId: 't2', isResolved: true }), - comment({ id: 4, threadId: 't3' }), - comment({ id: 5 }) - ] - expect(countUnresolvedReviewThreads(comments)).toBe(2) - }) -}) diff --git a/mobile/src/source-control/mobile-pr-chip-summary.ts b/mobile/src/source-control/mobile-pr-chip-summary.ts deleted file mode 100644 index 9d62b270f22..00000000000 --- a/mobile/src/source-control/mobile-pr-chip-summary.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { PRComment } from '../../../src/shared/github/comment-types' -import type { PrSidebarState } from '../session/mobile-pr-sidebar-state' -import { summarizeProviderChecks } from '../../../src/shared/provider-check-summary' -import { - prStateBadge, - type MobileStatusToken -} from '../components/pr-sidebar/pr-checks-presentation' - -// Use the sidebar’s same state so the chip and linked checks list cannot disagree. - -export type MobilePrChipRollup = - | { kind: 'conflict'; text: string; token: MobileStatusToken } - | { kind: 'failing'; text: string; token: MobileStatusToken } - | { kind: 'running'; text: string; token: MobileStatusToken } - | { kind: 'passed'; text: string; token: MobileStatusToken } - | { kind: 'none'; text: string; token: MobileStatusToken } - -export type MobilePrChipSummary = - | { kind: 'loading' } - | { kind: 'none' } - | { kind: 'unavailable'; message: string } - | { - kind: 'ready' - number: number - stateLabel: string - stateToken: MobileStatusToken - rollup: MobilePrChipRollup - commentCount: number | null - } - -// Count unresolved inline threads once per threadId; conversation comments are not review threads. -export function countUnresolvedReviewThreads( - comments: PRComment[] | null | undefined -): number | null { - if (!comments) { - return null - } - const unresolved = new Set() - for (const comment of comments) { - if (comment.threadId && comment.isResolved !== true) { - unresolved.add(comment.threadId) - } - } - return unresolved.size -} - -export function buildMobilePrChipSummary( - state: PrSidebarState, - commentCount: number | null = null -): MobilePrChipSummary { - switch (state.kind) { - case 'hidden': - case 'loading': - return { kind: 'loading' } - case 'none': - return { kind: 'none' } - case 'error': - case 'blocked': - return { kind: 'unavailable', message: state.message } - case 'ready': { - const badge = prStateBadge(state.data.pr.state) - return { - kind: 'ready', - number: state.data.pr.number, - stateLabel: badge.label, - stateToken: badge.token, - rollup: buildChipRollup(state), - commentCount - } - } - } -} - -function buildChipRollup(state: Extract): MobilePrChipRollup { - if (state.data.pr.mergeable === 'CONFLICTING') { - return { kind: 'conflict', text: 'Conflicts', token: 'statusAmber' } - } - // Share classification with the checks list and tasks grid. - const checks = summarizeProviderChecks(state.data.checks) - if (checks.failed > 0) { - return { kind: 'failing', text: `${checks.failed} failing`, token: 'statusRed' } - } - if (checks.pending > 0) { - return { kind: 'running', text: `${checks.pending} running`, token: 'statusAmber' } - } - if (checks.passed > 0) { - return { kind: 'passed', text: `${checks.passed}/${checks.total}`, token: 'statusGreen' } - } - // Existing checks with no actionable outcome are still distinct from no checks. - return { - kind: 'none', - text: checks.total === 0 ? 'No checks' : 'Unresolved checks', - token: 'textSecondary' - } -} diff --git a/mobile/src/source-control/mobile-source-control-actions.ts b/mobile/src/source-control/mobile-source-control-actions.ts index 8540fe62d3b..936eff41189 100644 --- a/mobile/src/source-control/mobile-source-control-actions.ts +++ b/mobile/src/source-control/mobile-source-control-actions.ts @@ -145,12 +145,12 @@ export function buildMobileSourceControlActions( label: t('sourceControl.actions.switchBranch', 'Switch branch'), iconKey: 'branch', disabled: busy, - skipAutoClose: true, + closeBeforePress: true, onPress: handlers.checkout }, { id: 'history', - label: t('sourceControl.actions.commits', 'Commits'), + label: t('sourceControl.actions.openCommits', 'Open commits'), iconKey: 'history', disabled: busy, onPress: handlers.history @@ -158,6 +158,27 @@ export function buildMobileSourceControlActions( ] } +// The ⋯ menu's git actions, grouped; Create PR lives in the next-step bar and the PR tab. +const MENU_GROUPS: readonly (readonly string[])[] = [ + ['commit-push', 'commit-sync'], + // Push and Publish stay here too: the bar shows the commit box while anything is uncommitted. + ['push', 'publish', 'fetch', 'pull', 'fast-forward', 'sync', 'rebase'], + ['checkout', 'history'] +] + +export function groupMobileSourceControlMenu( + actions: readonly T[] +): { groups: T[][]; forcePush: T | null } { + const byId = new Map(actions.map((action) => [action.id, action])) + const groups = MENU_GROUPS.map((ids) => + ids.flatMap((id) => { + const action = byId.get(id) + return action ? [action] : [] + }) + ).filter((group) => group.length > 0) + return { groups, forcePush: byId.get('force-push') ?? null } +} + export function getMobilePrimaryActionHint( decision: SourceControlCommitAreaPrimaryActionDecision ): string { diff --git a/mobile/src/source-control/mobile-source-control-list-styles.ts b/mobile/src/source-control/mobile-source-control-list-styles.ts index 04fed32b715..6b43b3a73e7 100644 --- a/mobile/src/source-control/mobile-source-control-list-styles.ts +++ b/mobile/src/source-control/mobile-source-control-list-styles.ts @@ -11,13 +11,21 @@ export const listStyles = themedStyleSheet(() => ({ sectionHeaderBleed: { marginHorizontal: -spacing.lg }, - branchCompareBlock: { - paddingBottom: spacing.sm + sectionAction: { + color: colors.textPrimary, + fontSize: 13, + fontWeight: '600' }, - branchSectionSubtitle: { - color: colors.textMuted, - fontSize: typography.metaSize, - paddingBottom: spacing.xs + sectionActions: { + flexDirection: 'row', + gap: spacing.lg + }, + sectionActionPressed: { + opacity: 0.6 + }, + sectionMeta: { + color: colors.textSecondary, + fontSize: 13 }, branchStateRow: { minHeight: 44, @@ -34,21 +42,20 @@ export const listStyles = themedStyleSheet(() => ({ fontSize: typography.metaSize, lineHeight: 18 }, + // Opaque so a left swipe uncovers Discard only as the row slides. fileRow: { minHeight: 50, flexDirection: 'row', alignItems: 'center', gap: spacing.sm, paddingVertical: spacing.sm, + backgroundColor: colors.bgBase, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.borderSubtle }, fileRowPressed: { backgroundColor: colors.bgPanel }, - fileRowDisabled: { - opacity: 0.78 - }, fileRowUnavailable: { opacity: 0.72 }, @@ -66,23 +73,19 @@ export const listStyles = themedStyleSheet(() => ({ flex: 1, minWidth: 0 }, - filePath: { + fileName: { color: colors.textPrimary, fontSize: typography.bodySize }, - filePathDisabled: { - color: colors.textSecondary + fileNameGone: { + color: colors.textSecondary, + textDecorationLine: 'line-through' }, fileMeta: { color: colors.textMuted, fontSize: typography.metaSize, marginTop: 2 }, - rowActions: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs - }, iconButton: { width: 32, height: 32, @@ -90,6 +93,23 @@ export const listStyles = themedStyleSheet(() => ({ alignItems: 'center', justifyContent: 'center' }, + // Behind the row's right end; the row slides left to uncover it. + discardAction: { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + width: 84, + alignItems: 'center', + justifyContent: 'center', + gap: 2, + backgroundColor: colors.statusRed + }, + discardActionText: { + color: colors.onStatusRed, + fontSize: typography.metaSize, + fontWeight: '600' + }, conflictActions: { flexDirection: 'row', flexWrap: 'wrap', diff --git a/mobile/src/source-control/mobile-source-control-styles.ts b/mobile/src/source-control/mobile-source-control-styles.ts index 4068ae43a78..5c84eaf35f2 100644 --- a/mobile/src/source-control/mobile-source-control-styles.ts +++ b/mobile/src/source-control/mobile-source-control-styles.ts @@ -13,62 +13,67 @@ const baseStyles = themedStyleSheet(() => ({ header: { backgroundColor: colors.bgBase }, - summaryCard: { - margin: spacing.lg, - marginBottom: spacing.sm, + statusLine: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingTop: spacing.md, + paddingBottom: spacing.xs + }, + branchButton: { + flexShrink: 1, + minHeight: 32, + flexDirection: 'row', + alignItems: 'center', + gap: 6 + }, + branchText: { + flexShrink: 1, + color: colors.textPrimary, + fontSize: typography.bodySize, + fontWeight: '600' + }, + conflictBanner: { + marginTop: spacing.sm, padding: spacing.md, + gap: spacing.sm, borderRadius: radii.surface, backgroundColor: colors.bgPanel, borderWidth: StyleSheet.hairlineWidth, - borderColor: colors.borderSubtle + borderColor: colors.statusAmber }, - summaryHeader: { + conflictBannerTitleRow: { flexDirection: 'row', alignItems: 'center', - justifyContent: 'space-between', - gap: spacing.md + gap: spacing.sm }, - branchLine: { - flex: 1, - minWidth: 0, - flexDirection: 'row', - alignItems: 'center', - gap: spacing.xs - }, - branchText: { + conflictBannerTitle: { flex: 1, color: colors.textPrimary, fontSize: typography.bodySize, fontWeight: '600' }, - syncText: { + conflictBannerText: { color: colors.textSecondary, fontSize: typography.metaSize }, - countRow: { + commitsRow: { + minHeight: 48, flexDirection: 'row', - flexWrap: 'wrap', - gap: spacing.md, - marginTop: spacing.sm - }, - countText: { - color: colors.textSecondary, - fontSize: typography.metaSize - }, - // Keep Abort below counts so narrow cards cannot overflow. - conflictRow: { - flexDirection: 'row', - flexWrap: 'wrap', alignItems: 'center', gap: spacing.sm, - marginTop: spacing.sm, - alignSelf: 'flex-start', - maxWidth: '100%' + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: colors.borderSubtle }, - conflictText: { - color: colors.statusAmber, - fontSize: typography.metaSize, - textTransform: 'capitalize' + commitsRowText: { + flex: 1, + color: colors.textPrimary, + fontSize: typography.bodySize + }, + syncText: { + marginLeft: 'auto', + color: colors.textSecondary, + fontSize: typography.metaSize }, abortButton: { minHeight: 32, @@ -125,40 +130,6 @@ const baseStyles = themedStyleSheet(() => ({ color: colors.textPrimary, fontSize: typography.metaSize, lineHeight: 16 - }, - bulkRow: { - flexDirection: 'row', - gap: spacing.sm, - marginTop: spacing.md - }, - bulkButton: { - flex: 1, - minHeight: 36, - borderRadius: radii.control, - backgroundColor: colors.bgRaised, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - gap: spacing.xs - }, - bulkMenuButton: { - width: 42, - minHeight: 36, - borderRadius: radii.control, - backgroundColor: colors.bgRaised, - alignItems: 'center', - justifyContent: 'center' - }, - bulkButtonDisabled: { - opacity: 0.45 - }, - bulkButtonPressed: { - opacity: 0.75 - }, - bulkButtonText: { - color: colors.textPrimary, - fontSize: typography.bodySize, - fontWeight: '600' } })) diff --git a/mobile/src/source-control/source-control-status-retry.test.tsx b/mobile/src/source-control/source-control-status-retry.test.tsx index 0ae212e0ef9..a212a66c54b 100644 --- a/mobile/src/source-control/source-control-status-retry.test.tsx +++ b/mobile/src/source-control/source-control-status-retry.test.tsx @@ -20,6 +20,7 @@ vi.mock('react-native-safe-area-context', () => ({ SafeAreaView: 'SafeAreaView' vi.mock('lucide-react-native', () => ({ CircleAlert: 'Icon', GitBranch: 'Icon', + RefreshCw: 'Icon', WifiOff: 'Icon' })) vi.mock('./MobileSourceControlHeader', () => ({ MobileSourceControlHeader: () => null })) @@ -55,7 +56,7 @@ vi.mock('../components/pr-sidebar/use-mobile-pr-tab', () => ({ vi.mock('../components/pr-sidebar/use-pr-tab-menu', () => ({ usePrTabMenuGroups: () => null })) vi.mock('../navigation/use-back-claim', () => ({ useBackClaim: () => {} })) vi.mock('./use-mobile-source-control-action-sheet', () => ({ - useMobileSourceControlActionSheet: () => ({}) + useMobileSourceControlActionSheet: () => [] })) vi.mock('../session/use-mobile-pr-sidebar-controller', () => ({ useMobilePrSidebarController: () => ({ @@ -82,7 +83,6 @@ vi.mock('./use-mobile-source-control-state', () => ({ status: null, branchCompareResult: null, branchLabel: '', - syncLabel: '', unstagedCount: 0, stagedCount: 0, branchEntries: [], diff --git a/mobile/src/source-control/use-mobile-source-control-loaders.ts b/mobile/src/source-control/use-mobile-source-control-loaders.ts index 4a2b821dce9..97913c55776 100644 --- a/mobile/src/source-control/use-mobile-source-control-loaders.ts +++ b/mobile/src/source-control/use-mobile-source-control-loaders.ts @@ -185,10 +185,9 @@ export function useMobileSourceControlLoaders(params: Params): MobileSourceContr setScreenState((prev) => (prev.kind === 'ready' ? prev : { kind: 'loading' })) try { for (let attempt = 0; attempt <= SELECTOR_RETRY_COUNT; attempt += 1) { + // Changes rows show each file's +/- counts, so ask the host for line stats. const reply = await gitStatusHostPayloadRead.request(client, { - worktree: `id:${worktreeId}`, - // Changes rows never show line counts; older hosts strip the unknown key. - includeLineStats: false + worktree: `id:${worktreeId}` }) if (!isCurrentLoad()) { return false diff --git a/mobile/src/source-control/use-mobile-source-control-state.ts b/mobile/src/source-control/use-mobile-source-control-state.ts index 0caa6fdf156..5206db5c678 100644 --- a/mobile/src/source-control/use-mobile-source-control-state.ts +++ b/mobile/src/source-control/use-mobile-source-control-state.ts @@ -15,8 +15,7 @@ import { useMobileSourceControlKeyboardLift } from './use-mobile-source-control- import type { RuntimeGitLocalBranches } from '../../../src/shared/runtime-types' import { buildMobileBranchCompareSection, - canOpenMobileBranchCompareDiff, - formatMobileBranchCompareSummary + canOpenMobileBranchCompareDiff } from './mobile-branch-compare' import { @@ -89,9 +88,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara () => buildMobileBranchCompareSection(branchCompareResult?.entries ?? []), [branchCompareResult, t] ) - const branchCompareSummaryText = branchCompareResult - ? formatMobileBranchCompareSummary(branchCompareResult.summary) - : null const branchCompareCanOpen = branchCompareResult ? canOpenMobileBranchCompareDiff(branchCompareResult.summary) : false @@ -124,12 +120,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara const branchLabel = formatBranchLabel(status?.branch, status?.head) const upstream = status?.upstreamStatus const upstreamKnown = upstream !== undefined - const syncLabel = - upstream && upstream.hasUpstream - ? t('sourceControl.state.syncStatus', '{{ahead}} ahead, {{behind}} behind', upstream) - : upstream && !upstream.hasUpstream - ? t('sourceControl.state.noUpstream', 'No upstream') - : null const { sendGitRequest, sendCommitRequest, runGitSyncSteps } = useMobileGitRequests({ client, @@ -239,7 +229,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara status, sections, branchCompareResult, - branchCompareSummaryText, branchEntries, shouldShowBranchCompareSection, hasVisibleChanges, @@ -249,7 +238,6 @@ export function useMobileSourceControlState(params: MobileSourceControlStatePara unstagedCount, branchLabel, upstreamKnown, - syncLabel, primaryActionArgs, createPrAction, loadStatus,