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.
This commit is contained in:
Kelvin Amoaba
2026-11-01 23:17:10 +00:00
committed by GitHub
parent a7d6926f77
commit f7047a8fc7
42 changed files with 1837 additions and 2400 deletions
@@ -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"
@@ -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"
},
@@ -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"
@@ -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"
@@ -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"
+1 -2
View File
@@ -20348,8 +20348,7 @@
{
"complete": "git.status#1",
"params": {
"worktree": "id:repo42::/p",
"includeLineStats": false
"worktree": "id:repo42::/p"
},
"reply": {
"ok": true,
+62 -39
View File
@@ -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) => (
<View key={groupIndex} style={groupIndex > 0 && styles.groupGap}>
{group.map((action, index) => {
const Icon = action.icon
const tint = action.destructive ? colors.statusRed : colors.textPrimary
return (
<Pressable
key={action.id ?? action.label}
style={({ pressed }) => [
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 }
}
>
<Text style={[styles.label, { color: tint }]} numberOfLines={1}>
{action.label}
</Text>
{action.checked ? (
<Check size={17} color={tint} strokeWidth={2.2} />
) : action.renderIcon ? (
action.renderIcon()
) : (
Icon && <Icon size={17} color={tint} strokeWidth={2} />
)}
</Pressable>
)
})}
</View>
))}
<ScrollView bounces={false} showsVerticalScrollIndicator={false}>
{shown.map((group, groupIndex) => (
<View key={groupIndex} style={groupIndex > 0 && styles.groupGap}>
{group.map((action, index) => {
const Icon = action.icon
const tint = action.destructive ? colors.statusRed : colors.textPrimary
return (
<Pressable
key={action.id ?? action.label}
style={({ pressed }) => [
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 }
}
>
<View style={styles.labelBlock}>
<Text style={[styles.label, { color: tint }]} numberOfLines={1}>
{action.label}
</Text>
{/* No tooltips on a phone: a disabled item says why, a destructive one what it replaces. */}
{(action.disabled || action.destructive) && action.hint ? (
<Text style={styles.hint} numberOfLines={2}>
{action.hint}
</Text>
) : null}
</View>
{action.checked ? (
<Check size={17} color={tint} strokeWidth={2.2} />
) : action.renderIcon ? (
action.renderIcon()
) : (
Icon && <Icon size={17} color={tint} strokeWidth={2} />
)}
</Pressable>
)
})}
</View>
))}
</ScrollView>
</Animated.View>
) : null}
</Modal>
@@ -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
}
}))
+3 -7
View File
@@ -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",
+3 -37
View File
@@ -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": {
+35 -37
View File
@@ -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": {
+3 -40
View File
@@ -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": {
+3 -40
View File
@@ -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": {
+3 -37
View File
@@ -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": {
+3 -37
View File
@@ -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": {
+3 -40
View File
@@ -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": {
+3 -43
View File
@@ -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": {
+3 -37
View File
@@ -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": {
+3 -37
View File
@@ -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": {
+3 -37
View File
@@ -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": {
@@ -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 (
<View style={styles.summaryCard}>
<View style={styles.summaryHeader}>
<View style={styles.branchLine}>
<GitBranch size={15} color={colors.textSecondary} strokeWidth={2.1} />
<Text style={styles.branchText} numberOfLines={1}>
{branchLabel}
</Text>
</View>
{syncLabel ? <Text style={styles.syncText}>{syncLabel}</Text> : null}
</View>
<View style={styles.countRow}>
<Text style={styles.countText}>
{t('sourceControl.branchCard.changedCount', '{{count}} changed', {
count: unstagedCount
})}
</Text>
<Text style={styles.countText}>
{t('sourceControl.branchCard.stagedCount', '{{count}} staged', { count: stagedCount })}
</Text>
{branchCount > 0 ? (
<Text style={styles.countText}>
{t('sourceControl.branchCard.branchCount', '{{count}} on branch', {
count: branchCount
})}
</Text>
) : null}
</View>
{/* Keep Abort on its own row so long counts cannot overflow the card. */}
{showConflict ? (
<View style={styles.conflictRow}>
<Text style={styles.conflictText}>{conflictLabel}</Text>
{conflictOperation === 'merge' || conflictOperation === 'rebase' ? (
<Pressable
style={({ pressed }) => [
styles.abortButton,
conflictBusy && styles.abortButtonDisabled,
pressed && !conflictBusy && styles.abortPressed
]}
disabled={conflictBusy}
onPress={() => onAbortConflict(conflictOperation)}
>
<Text style={styles.abortText}>
{mobileConflictAbortLabel(conflictOperation, conflictAborting)}
</Text>
</Pressable>
) : null}
</View>
) : null}
{prChip ? <MobileSourceControlPrChip summary={prChip} onPress={onOpenPr} /> : null}
</View>
)
}
@@ -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 (
<View>
<View style={styles.statusLine}>
<Pressable
style={({ pressed }) => [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
}
)}
>
<GitBranch size={15} color={colors.textSecondary} strokeWidth={2.1} />
<Text style={styles.branchText} numberOfLines={1}>
{state.branchLabel}
</Text>
<ChevronDown size={14} color={colors.textMuted} strokeWidth={2.2} />
</Pressable>
{meta ? <Text style={styles.syncText}>{meta}</Text> : null}
</View>
{operation ? (
<View style={styles.conflictBanner}>
<View style={styles.conflictBannerTitleRow}>
<TriangleAlert size={17} color={colors.statusAmber} strokeWidth={2.1} />
<Text style={styles.conflictBannerTitle}>
{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'
)}
</Text>
{operation === 'merge' || operation === 'rebase' ? (
<Pressable
style={({ pressed }) => [
styles.abortButton,
busyAction !== null && styles.abortButtonDisabled,
pressed && busyAction === null && styles.abortPressed
]}
disabled={busyAction !== null}
onPress={() => void state.abortConflictOperation(operation)}
accessibilityLabel={mobileConflictAbortLabel(operation, aborting)}
>
<Text style={styles.abortText}>
{mobileConflictAbortLabel(operation, aborting)}
</Text>
</Pressable>
) : null}
</View>
<Text style={styles.conflictBannerText}>
{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.'
)}
</Text>
</View>
) : null}
{commits > 0 ? (
<Pressable
style={({ pressed }) => [styles.commitsRow, pressed && styles.fileRowPressed]}
onPress={onOpenCommits}
accessibilityRole="button"
>
<GitCommitHorizontal size={16} color={colors.textSecondary} strokeWidth={2.1} />
<Text style={styles.commitsRowText}>
{commits === 1
? t('sourceControl.changesHeader.commits_one', '{{count}} commit', { count: commits })
: t('sourceControl.changesHeader.commits_other', '{{count}} commits', {
count: commits
})}
</Text>
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.2} />
</Pressable>
) : null}
</View>
)
}
@@ -9,7 +9,6 @@ import type { MobileSourceControlState } from './use-mobile-source-control-state
type MobileSourceControlContentState = Omit<MobileSourceControlState, 'primaryActionArgs'> & {
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<MobileBranchEntryView | MobileGitStatusEntryView>) =>
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()
})
})
@@ -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<MobileSourceControlState, 'primaryActionArgs'>
/** 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(
() => (
<MobileSourceControlFileList
bottomInset={listBottomInset}
<MobileSourceControlChangesHeader
state={{
sections,
openDiffForFile,
resolveConflict,
runGitAction,
setDiscardTarget,
shouldShowBranchCompareSection: state.shouldShowBranchCompareSection,
branchCompareSummaryText: state.branchCompareSummaryText,
branchEntries: state.branchEntries,
branchCompareState: state.branchCompareState,
branchCompareResult: state.branchCompareResult,
busyAction
status,
branchLabel,
branchCompareResult,
busyAction,
openBranchPicker,
abortConflictOperation
}}
onOpenCommits={onOpenCommits}
/>
),
[
listBottomInset,
sections,
abortConflictOperation,
branchCompareResult,
branchLabel,
busyAction,
onOpenCommits,
openBranchPicker,
status
]
)
const list = useMemo(
() => (
<MobileSourceControlFileList
state={{
sections,
status,
shouldShowBranchCompareSection,
branchEntries,
branchCompareState,
branchCompareResult,
stageablePaths,
unstageablePaths,
stageAll,
unstageAll,
busyAction,
openDiffForFile,
resolveConflict,
runGitAction,
setDiscardTarget
}}
header={header}
bottomInset={bottomInset + spacing.lg}
/>
),
[
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.
<View style={styles.reconnectBanner}>
<ActivityIndicator size="small" color={colors.statusAmber} />
<Text style={styles.reconnectBannerText}>
{t('sourceControl.content.reconnecting', 'Reconnecting to desktop...')}
</Text>
</View>
) : null}
// Outside the list, so a failure stays in view however far the rows are scrolled.
const feedback =
connState !== 'connected' || commitFailureRecovery || actionError ? (
<View style={hubStyles.changesControls}>
{connState !== 'connected' ? (
// Retained rows look actionable offline; show reconnect state beside them.
<View style={styles.reconnectBanner}>
<ActivityIndicator size="small" color={colors.statusAmber} />
<Text style={styles.reconnectBannerText}>
{t('sourceControl.content.reconnecting', 'Reconnecting to desktop...')}
</Text>
</View>
) : null}
{commitFailureRecovery ? (
<MobileCommitFailurePanel
failure={commitFailureRecovery}
@@ -138,63 +175,13 @@ export function MobileSourceControlContent({ state, bottomInset }: Props) {
</Text>
</View>
) : null}
<View style={styles.bulkRow}>
<Pressable
style={({ pressed }) => [
styles.bulkButton,
(!canStageAll || ioBusy) && styles.bulkButtonDisabled,
pressed && styles.bulkButtonPressed
]}
onPress={() => void stageAll()}
disabled={ioBusy || !canStageAll}
>
{busyAction === 'stage-all' ? (
<ActivityIndicator size="small" color={colors.textPrimary} />
) : (
<Plus size={15} color={colors.textPrimary} strokeWidth={2.2} />
)}
<Text style={styles.bulkButtonText}>
{t('sourceControl.content.stageAll', 'Stage All')}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.bulkButton,
(unstageablePaths.length === 0 || ioBusy) && styles.bulkButtonDisabled,
pressed && styles.bulkButtonPressed
]}
onPress={() => void unstageAll()}
disabled={ioBusy || unstageablePaths.length === 0}
>
{busyAction === 'unstage-all' ? (
<ActivityIndicator size="small" color={colors.textPrimary} />
) : (
<Minus size={15} color={colors.textPrimary} strokeWidth={2.2} />
)}
<Text style={styles.bulkButtonText}>
{t('sourceControl.content.unstageAll', 'Unstage All')}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [
styles.bulkMenuButton,
pressed && styles.bulkButtonPressed,
ioBusy && styles.bulkButtonDisabled
]}
onPress={() => setShowActionSheet(true)}
disabled={ioBusy}
hitSlop={8}
accessibilityLabel={t(
'sourceControl.content.openActions',
'Open source control actions'
)}
>
<MoreHorizontal size={18} color={colors.textPrimary} strokeWidth={2.1} />
</Pressable>
</View>
</View>
{!hasVisibleChanges ? (
) : null
if (!hasVisibleChanges) {
return (
<View style={hubStyles.tabBody}>
{feedback}
<View style={hubStyles.changesControls}>{header}</View>
<CleanStateFade animate={cleanedByCommit}>
<Text style={styles.stateTitle}>
{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.')}
</Text>
</CleanStateFade>
) : (
fileList
)}
</>
</View>
)
}
return (
<View style={hubStyles.tabBody}>
{feedback}
{list}
</View>
)
}
@@ -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) => (
<Pressable
onPress={onPress}
disabled={busy || !enabled}
hitSlop={8}
accessibilityRole="button"
style={({ pressed }) => [
(busy || !enabled) && styles.iconButtonDisabled,
pressed && styles.sectionActionPressed
]}
>
<Text style={styles.sectionAction}>{label}</Text>
</Pressable>
)
// 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' ? (
<View style={styles.sectionActions}>
{stageAllOnStaged ? stageAllAction : null}
{merging
? null
: sectionAction(
t('sourceControl.fileList.unstageAll', 'Unstage all'),
() => void state.unstageAll(),
state.unstageablePaths.length > 0
)}
</View>
) : 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 ? (
<Text style={styles.sectionMeta}>
{t('sourceControl.fileList.versusBase', 'vs {{base}}', {
base: compare.summary.baseRef.replace(/^origin\//, '')
})}
</Text>
) : undefined,
data: ready ? state.branchEntries : []
})
}
const renderFileRow = makeRenderFileRow(state, t)
return (
<SectionList<FileEntry, FileSection>
style={hubStyles.tabBody}
sections={sections}
extraData={colorScheme}
ListHeaderComponent={<>{header}</>}
renderItem={({ item }) =>
isWorkingTreeEntry(item) ? (
renderFileRow({ item })
<ChangeFileRow item={item} state={state} />
) : (
<BranchFileRow entry={item} state={state} />
)
@@ -78,17 +143,43 @@ export function MobileSourceControlFileList({
keyExtractor={(item) =>
`${isWorkingTreeEntry(item) ? item.area : 'committed'}:${item.path}:${item.oldPath ?? ''}`
}
renderSectionHeader={({ section }) =>
section.key === 'committed' ? (
<BranchCompareHeader state={state} />
) : (
renderSectionHeader={({ section }) => (
<View>
<View style={styles.sectionHeaderBleed}>
<SectionHeader title={section.title} trailing={String(section.data.length)} />
<SectionHeader title={section.title} trailing={section.trailing} />
</View>
)
}
{section.key === 'committed' ? <BranchCompareState state={state} /> : null}
</View>
)}
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 (
<View style={styles.branchStateRow}>
{branchCompareState.kind === 'loading' ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : null}
<Text style={styles.branchStateText}>
{message ?? t('sourceControl.fileRows.loadingCommitted', 'Loading committed changes...')}
</Text>
</View>
)
}
@@ -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<typeof import('react-native')>('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()
@@ -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 (
<>
<View style={styles.statusBadge}>
<Text style={[styles.statusBadgeText, { color: statusColor(status) }]}>
{STATUS_LABELS[status]}
</Text>
</View>
<View style={styles.fileTextBlock}>
<Text
style={[styles.fileName, status === 'deleted' && styles.fileNameGone]}
numberOfLines={1}
>
{name}
</Text>
{subtitle ? (
<Text style={styles.fileMeta} numberOfLines={1}>
{subtitle}
</Text>
) : null}
{actions ? <View style={styles.conflictActions}>{actions}</View> : null}
</View>
<MobileDiffLineStats added={added} removed={removed} />
{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 (
<View>
<Pressable
style={({ pressed }) => [
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}
>
<View style={styles.statusBadge}>
<Text style={[styles.statusBadgeText, { color: statusColor(item.status) }]}>
{STATUS_LABELS[item.status]}
</Text>
</View>
<FileText
size={16}
color={item.canOpen ? colors.textSecondary : colors.textMuted}
strokeWidth={2.1}
/>
<View style={styles.fileTextBlock}>
<Text
style={[styles.filePath, !item.canOpen && styles.filePathDisabled]}
numberOfLines={1}
>
{item.path}
</Text>
{item.oldPath ? (
<Text style={styles.fileMeta} numberOfLines={1}>
{t('sourceControl.fileRows.renamedFrom', 'from {{path}}', { path: item.oldPath })}
</Text>
) : item.conflictStatus === 'unresolved' ? (
<Text style={styles.fileMeta} numberOfLines={1}>
{conflictKindLabel(item.conflictKind, t)}
</Text>
) : null}
{!rowBusy && hasConflictActions ? (
<View style={styles.conflictActions}>
{item.conflictResolutions.map((resolution) => (
<Pressable
key={resolution}
style={({ pressed }) => [
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
}
)}
>
<Text style={styles.conflictActionText}>
{mobileConflictResolutionLabel(resolution, t)}
</Text>
</Pressable>
))}
</View>
) : null}
</View>
{rowBusy ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : item.canUnstage ? (
<Undo2 size={17} color={colors.onStatusRed} strokeWidth={2.2} />
<Text style={styles.discardActionText}>{label}</Text>
</Pressable>
{/* pan-y keeps the page scrolling vertically over the row on the web. */}
<GestureDetector gesture={pan} touchAction="pan-y">
<Animated.View style={rowStyle}>{children(guardPress)}</Animated.View>
</GestureDetector>
</View>
)
}
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) => (
<Pressable
key={resolution}
style={({ pressed }) => [
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
}
)}
>
<Minus size={16} color={colors.textSecondary} strokeWidth={2.2} />
<Text style={styles.conflictActionText}>
{mobileConflictResolutionLabel(resolution, t)}
</Text>
</Pressable>
) : item.canStage || item.canDiscard ? (
<View style={styles.rowActions}>
{item.canStage ? (
<Pressable
style={({ pressed }) => [
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
})}
>
<Plus size={16} color={colors.textSecondary} strokeWidth={2.2} />
</Pressable>
) : null}
{item.canDiscard ? (
<Pressable
style={({ pressed }) => [
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
})}
>
<Trash2 size={16} color={colors.statusRed} strokeWidth={2.1} />
</Pressable>
) : null}
</View>
) : null}
{!rowBusy && item.canOpen ? (
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
) : null}
))
: null
const trailing = (guard: (onPress: () => void) => () => void) =>
rowBusy ? (
<ActivityIndicator size="small" color={colors.textSecondary} />
) : !hasConflictActions && (item.canUnstage || item.canStage) ? (
<Pressable
style={({ pressed }) => [
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 ? (
<Minus size={18} color={colors.textSecondary} strokeWidth={2.2} />
) : (
<Plus size={18} color={colors.textSecondary} strokeWidth={2.2} />
)}
</Pressable>
)
}
}
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 (
<View style={styles.branchCompareBlock}>
<View style={styles.sectionHeaderBleed}>
<SectionHeader
title={t('sourceControl.fileRows.committedSection', 'Committed on Branch')}
trailing={String(branchEntries.length)}
/>
</View>
{branchCompareSummaryText ? (
<Text style={styles.branchSectionSubtitle} numberOfLines={1}>
{branchCompareSummaryText}
</Text>
) : null}
{branchCompareState.kind === 'loading' ? (
<View style={styles.branchStateRow}>
<ActivityIndicator size="small" color={colors.textSecondary} />
<Text style={styles.branchStateText}>
{t('sourceControl.fileRows.loadingCommitted', 'Loading committed changes...')}
</Text>
</View>
) : branchCompareState.kind === 'error' ? (
<View style={styles.branchStateRow}>
<Text style={styles.branchStateText}>{branchCompareState.message}</Text>
</View>
) : branchCompareResult && branchCompareResult.summary.status !== 'ready' ? (
<View style={styles.branchStateRow}>
<Text style={styles.branchStateText}>
{branchCompareResult.summary.errorMessage ??
t('sourceControl.fileRows.committedUnavailable', 'Committed changes unavailable.')}
</Text>
</View>
) : null}
</View>
<SwipeToDiscard
available={item.canDiscard}
enabled={!ioBusy}
label={t('sourceControl.fileRows.discard', 'Discard')}
accessibilityLabel={t('sourceControl.fileRows.discardFile', 'Discard {{path}}', {
path: item.path
})}
onDiscard={() => setDiscardTarget(item)}
>
{(guardPress) => (
<Pressable
style={({ pressed }) => [
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
}
)}
>
<FileRowBody
status={item.status}
path={item.path}
detail={detail}
added={item.added}
removed={item.removed}
trailing={trailing(guardPress)}
actions={conflictActions(guardPress)}
/>
</Pressable>
)}
</SwipeToDiscard>
)
}
@@ -272,55 +343,38 @@ export function BranchFileRow({
state
}: {
entry: MobileBranchEntryView
state: BranchState
state: Pick<FileRowState, 'busyAction' | 'openDiffForFile'>
}) {
useMobileColorScheme()
const t = useMobileTranslation()
const { busyAction, openDiffForFile } = state
const rowDisabled = !entry.canOpen || busyAction !== null
const meta = formatMobileBranchEntryMeta(entry)
return (
<Pressable
style={({ pressed }) => [
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 }
)}
>
<View style={styles.statusBadge}>
<Text style={[styles.statusBadgeText, { color: statusColor(entry.status) }]}>
{STATUS_LABELS[entry.status]}
</Text>
</View>
<FileText
size={16}
color={entry.canOpen ? colors.textSecondary : colors.textMuted}
strokeWidth={2.1}
<FileRowBody
status={entry.status}
path={entry.path}
detail={
entry.oldPath
? t('sourceControl.fileRows.renamedFrom', 'from {{path}}', { path: entry.oldPath })
: undefined
}
added={entry.added}
removed={entry.removed}
/>
<View style={styles.fileTextBlock}>
<Text
style={[styles.filePath, !entry.canOpen && styles.filePathDisabled]}
numberOfLines={1}
>
{entry.path}
</Text>
{meta ? (
<Text style={styles.fileMeta} numberOfLines={1}>
{meta}
</Text>
) : null}
</View>
{entry.canOpen ? <ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} /> : null}
</Pressable>
)
}
@@ -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 (
<>
<ActionSheetModal
visible={showActionSheet}
title={t('sourceControl.modals.actionsTitle', 'Source Control')}
message={branchLabel}
actions={actionSheetActions}
onClose={() => setShowActionSheet(false)}
/>
<ConfirmModal
visible={discardTarget !== null}
title={t('sourceControl.modals.discardTitle', 'Discard Change')}
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { View } from 'react-native'
import { GitBranch } from 'lucide-react-native'
import { GitBranch, RefreshCw } from 'lucide-react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import {
MobileLoadErrorState,
@@ -20,7 +20,6 @@ import {
} from './mobile-source-control-primary-action'
import { MobileSourceControlModals } from './MobileSourceControlModals'
import { MobileSegmentedControl } from '../components/MobileSegmentedControl'
import { MobileSourceControlBranchCard } from './MobileSourceControlBranchCard'
import { MobileSourceControlHubCommitsPage } from './MobileSourceControlHubCommitsPage'
import { MobileSourceControlNextStepBar } from './MobileSourceControlNextStepBar'
import { useMobileSourceControlNextStep } from './use-mobile-source-control-next-step'
@@ -33,8 +32,7 @@ import {
SOURCE_CONTROL_HUB_TAB_LABELS,
type SourceControlHubRouteTab
} from './mobile-source-control-hub-tab'
import { buildMobilePrChipSummary, countUnresolvedReviewThreads } from './mobile-pr-chip-summary'
import { isMobileConflictAborting } from './mobile-source-control-conflict-abort'
import { groupMobileSourceControlMenu } from './mobile-source-control-actions'
import { useMobilePrSidebarController } from '../session/use-mobile-pr-sidebar-controller'
import { prSidebarDetailsNeedFetch } from '../session/mobile-pr-sidebar-state'
import { MobilePrTabBody } from '../components/MobilePRSidebar'
@@ -74,7 +72,6 @@ export function MobileSourceControlPanel({
const [barHeight, setBarHeight] = useState(0)
const openHubScreen = hubScreens.open
const openCommitsScreen = useCallback(() => 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<MobilePrTabOpeners>({ 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 = (
<View style={styles.container}>
@@ -331,26 +342,13 @@ export function MobileSourceControlPanel({
</SafeAreaView>
)}
{/* Ready PRs duplicate the branch card; retain it only when needed to abort a conflict. */}
{ready && (activeTab !== 'pr' || hasActiveConflict) ? (
<MobileSourceControlBranchCard
branchLabel={branchLabel}
syncLabel={syncLabel}
unstagedCount={unstagedCount}
stagedCount={stagedCount}
branchCount={branchEntries.length}
conflictOperation={conflictOperation}
conflictBusy={busyAction !== null}
conflictAborting={conflictAborting}
onAbortConflict={(operation) => void abortConflictOperation(operation)}
prChip={prChip}
onOpenPr={openPrTab}
/>
) : null}
{showChanges ? (
<View style={activeTab === 'changes' ? hubStyles.tabBody : hubStyles.tabBodyHidden}>
<MobileSourceControlContent state={state} bottomInset={barInset} />
<MobileSourceControlContent
state={state}
bottomInset={barInset}
onOpenCommits={openCommitsScreen}
/>
</View>
) : activeTab === 'changes' ? (
statusGate
@@ -418,7 +416,7 @@ export function MobileSourceControlPanel({
) : null
}
/>
<MobileSourceControlModals state={state} actionSheetActions={actionSheetActions} />
<MobileSourceControlModals state={state} />
<MobilePrTabSheets tab={prTab} />
</View>
)
@@ -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 (
<Pressable
style={({ pressed }) => [hubStyles.chip, pressed && hubStyles.chipPressed]}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={chipAccessibilityLabel(summary, t)}
>
<View style={hubStyles.chipIcon}>
<GitPullRequest size={15} color={colors.textSecondary} strokeWidth={2.1} />
</View>
{summary.kind === 'loading' ? (
<>
<ActivityIndicator size="small" color={colors.textSecondary} />
<Text style={hubStyles.chipMutedText} numberOfLines={1}>
{t('sourceControl.prChip.loading', 'Loading pull request…')}
</Text>
</>
) : summary.kind === 'none' ? (
<>
<Text style={hubStyles.chipCreateText}>
{t('sourceControl.prChip.createPr', 'Create pull request')}
</Text>
<View style={hubStyles.chipSpacer} />
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
</>
) : summary.kind === 'unavailable' ? (
<>
<Text style={hubStyles.chipMutedText} numberOfLines={1}>
{summary.message}
</Text>
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
</>
) : (
<>
<Text style={hubStyles.chipNumber}>#{summary.number}</Text>
<View style={[hubStyles.statePill, { borderColor: statusColor(summary.stateToken) }]}>
<Text style={[hubStyles.statePillText, { color: statusColor(summary.stateToken) }]}>
{summary.stateLabel}
</Text>
</View>
<ChipRollup rollup={summary.rollup} />
{summary.commentCount != null && summary.commentCount > 0 ? (
<View style={hubStyles.comment}>
<MessageSquare size={13} color={colors.textSecondary} strokeWidth={2.1} />
<Text style={hubStyles.commentText}>{summary.commentCount}</Text>
</View>
) : null}
<View style={hubStyles.chipSpacer} />
<ChevronRight size={16} color={colors.textMuted} strokeWidth={2.1} />
</>
)}
</Pressable>
)
}
function ChipRollup({ rollup }: { rollup: MobilePrChipRollup }) {
useMobileColorScheme()
const color = statusColor(rollup.token)
return (
<View style={hubStyles.rollup}>
<RollupIcon kind={rollup.kind} color={color} />
<Text style={[hubStyles.rollupText, { color }]}>{rollup.text}</Text>
</View>
)
}
function RollupIcon({ kind, color }: { kind: MobilePrChipRollup['kind']; color: string }) {
const size = 13
const strokeWidth = 2.3
switch (kind) {
case 'conflict':
return <AlertTriangle size={size} color={color} strokeWidth={strokeWidth} />
case 'failing':
return <X size={size} color={color} strokeWidth={strokeWidth} />
case 'running':
return <CircleDot size={size} color={color} strokeWidth={strokeWidth} />
case 'passed':
return <Check size={size} color={color} strokeWidth={strokeWidth} />
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
)
}
}
}
@@ -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({
@@ -30,38 +30,6 @@ export function buildMobileBranchCompareSection<TEntry extends MobileGitBranchCh
}
}
export function formatMobileBranchCompareSummary(
summary: MobileGitBranchCompareSummary
): string | null {
if (summary.status !== 'ready') {
return summary.errorMessage ?? null
}
const files = branchCompareFiles(summary.changedFiles)
if (summary.commitsAhead === undefined) {
return translate('sourceControl.branchCompare.summary', '{{files}} - vs {{baseRef}}', {
files,
baseRef: summary.baseRef
})
}
return translate(
'sourceControl.branchCompare.summaryWithCommits',
'{{files}} - {{commits}} - vs {{baseRef}}',
{ files, commits: branchCompareCommits(summary.commitsAhead), baseRef: summary.baseRef }
)
}
function branchCompareFiles(count: number): string {
return count === 1
? translate('sourceControl.branchCompare.files_one', '{{count}} file', { count })
: translate('sourceControl.branchCompare.files_other', '{{count}} files', { count })
}
function branchCompareCommits(count: number): string {
return count === 1
? translate('sourceControl.branchCompare.commits_one', '{{count}} commit', { count })
: translate('sourceControl.branchCompare.commits_other', '{{count}} commits', { count })
}
export function canOpenMobileBranchCompareDiff(summary: MobileGitBranchCompareSummary): boolean {
return summary.status === 'ready' && Boolean(summary.headOid && summary.mergeBase)
}
@@ -1,12 +0,0 @@
import type { MobileGitBranchChangeEntry } from './mobile-branch-compare'
export function formatMobileBranchEntryMeta(entry: MobileGitBranchChangeEntry): string | null {
const stats =
entry.added !== undefined || entry.removed !== undefined
? `+${entry.added ?? 0} -${entry.removed ?? 0}`
: null
if (entry.oldPath) {
return stats ? `from ${entry.oldPath}; ${stats}` : `from ${entry.oldPath}`
}
return stats
}
@@ -23,7 +23,7 @@ describe('mobile source control status helpers', () => {
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']]
])
})
@@ -50,8 +50,8 @@ export type MobileSourceControlSection<TEntry extends MobileGitStatusEntry = Mob
function sectionTitles(): Record<SourceControlDisplaySectionId, string> {
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')
}
}
@@ -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> = {}): 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>): 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)
})
})
@@ -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<string>()
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<PrSidebarState, { kind: 'ready' }>): 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'
}
}
@@ -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<T extends { id: string }>(
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 {
@@ -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',
@@ -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'
}
}))
@@ -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: [],
@@ -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
@@ -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,